logicaffeine_compile/
loader.rs1use std::collections::HashMap;
39use std::fs;
40use std::path::{Path, PathBuf};
41
42#[derive(Debug, Clone)]
44pub struct ModuleSource {
45 pub content: String,
47 pub path: PathBuf,
49}
50
51pub struct Loader {
56 cache: HashMap<String, ModuleSource>,
58 root_path: PathBuf,
60}
61
62impl Loader {
63 pub fn new(root_path: PathBuf) -> Self {
65 Loader {
66 cache: HashMap::new(),
67 root_path,
68 }
69 }
70
71 pub fn resolve(&mut self, base_path: &Path, uri: &str) -> Result<&ModuleSource, String> {
78 let cache_key = self.normalize_uri(base_path, uri)?;
80
81 if self.cache.contains_key(&cache_key) {
83 return Ok(&self.cache[&cache_key]);
84 }
85
86 let source = if uri.starts_with("file:") {
88 self.load_file(base_path, uri)?
89 } else if uri.starts_with("logos:") {
90 self.load_intrinsic(uri)?
91 } else if uri.starts_with("https://") || uri.starts_with("http://") {
92 return Err(format!(
94 "Remote module loading not supported for '{}'. \
95 Use the CLI's 'logos fetch' command to download dependencies locally.",
96 uri
97 ));
98 } else {
99 self.load_file(base_path, &format!("file:{}", uri))?
101 };
102
103 self.cache.insert(cache_key.clone(), source);
105 Ok(&self.cache[&cache_key])
106 }
107
108 fn normalize_uri(&self, base_path: &Path, uri: &str) -> Result<String, String> {
110 if uri.starts_with("file:") {
111 let path_str = uri.trim_start_matches("file:");
112 let base_dir = base_path.parent().unwrap_or(&self.root_path);
113 let resolved = base_dir.join(path_str);
114 Ok(format!("file:{}", resolved.display()))
115 } else {
116 Ok(uri.to_string())
117 }
118 }
119
120 fn load_file(&self, base_path: &Path, uri: &str) -> Result<ModuleSource, String> {
122 let path_str = uri.trim_start_matches("file:");
123
124 let base_dir = base_path.parent().unwrap_or(&self.root_path);
126 let resolved_path = base_dir.join(path_str);
127
128 let canonical_root = self.root_path.canonicalize()
130 .unwrap_or_else(|_| self.root_path.clone());
131
132 let content = fs::read_to_string(&resolved_path)
134 .map_err(|e| format!("Failed to read '{}': {}", resolved_path.display(), e))?;
135
136 if let Ok(canonical_path) = resolved_path.canonicalize() {
138 if !canonical_path.starts_with(&canonical_root) {
139 return Err(format!(
140 "Security: Cannot load '{}' - path escapes project root",
141 uri
142 ));
143 }
144 }
145
146 Ok(ModuleSource {
147 content,
148 path: resolved_path,
149 })
150 }
151
152 fn load_intrinsic(&self, uri: &str) -> Result<ModuleSource, String> {
154 let name = uri.trim_start_matches("logos:");
155
156 match name {
157 "std" => Ok(ModuleSource {
158 content: include_str!("../assets/std/std.md").to_string(),
159 path: PathBuf::from("logos:std"),
160 }),
161 "core" => Ok(ModuleSource {
162 content: include_str!("../assets/std/core.md").to_string(),
163 path: PathBuf::from("logos:core"),
164 }),
165 _ => Err(format!("Unknown intrinsic module: '{}'", uri)),
166 }
167 }
168
169 pub fn is_loaded(&self, uri: &str) -> bool {
171 self.cache.contains_key(uri)
172 }
173
174 pub fn loaded_modules(&self) -> Vec<&str> {
176 self.cache.keys().map(|s| s.as_str()).collect()
177 }
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183 use tempfile::tempdir;
184
185 #[test]
186 fn test_file_scheme_resolution() {
187 let temp_dir = tempdir().unwrap();
188 let geo_path = temp_dir.path().join("geo.md");
189 fs::write(&geo_path, "## Definition\nA Point has:\n an x, which is Int.\n").unwrap();
190
191 let mut loader = Loader::new(temp_dir.path().to_path_buf());
192 let result = loader.resolve(&temp_dir.path().join("main.md"), "file:./geo.md");
193
194 assert!(result.is_ok(), "Should resolve file: scheme: {:?}", result);
195 assert!(result.unwrap().content.contains("Point"));
196 }
197
198 #[test]
199 fn test_logos_std_scheme() {
200 let mut loader = Loader::new(PathBuf::from("."));
201 let result = loader.resolve(&PathBuf::from("main.md"), "logos:std");
202
203 assert!(result.is_ok(), "Should resolve logos:std: {:?}", result);
204 }
205
206 #[test]
207 fn test_logos_core_scheme() {
208 let mut loader = Loader::new(PathBuf::from("."));
209 let result = loader.resolve(&PathBuf::from("main.md"), "logos:core");
210
211 assert!(result.is_ok(), "Should resolve logos:core: {:?}", result);
212 }
213
214 #[test]
215 fn test_unknown_intrinsic() {
216 let mut loader = Loader::new(PathBuf::from("."));
217 let result = loader.resolve(&PathBuf::from("main.md"), "logos:unknown");
218
219 assert!(result.is_err());
220 assert!(result.unwrap_err().contains("Unknown intrinsic"));
221 }
222
223 #[test]
224 fn test_caching() {
225 let temp_dir = tempdir().unwrap();
226 let geo_path = temp_dir.path().join("geo.md");
227 fs::write(&geo_path, "content").unwrap();
228
229 let mut loader = Loader::new(temp_dir.path().to_path_buf());
230
231 let _ = loader.resolve(&temp_dir.path().join("main.md"), "file:./geo.md");
233
234 assert!(loader.loaded_modules().len() == 1);
236 }
237
238 #[test]
239 fn test_missing_file() {
240 let temp_dir = tempdir().unwrap();
241 let mut loader = Loader::new(temp_dir.path().to_path_buf());
242
243 let result = loader.resolve(&temp_dir.path().join("main.md"), "file:./nonexistent.md");
244
245 assert!(result.is_err());
246 assert!(result.unwrap_err().contains("Failed to read"));
247 }
248}