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