1use crate::errors::{Result, RunjucksError};
4use std::collections::HashMap;
5use std::path::{Component, Path, PathBuf};
6use std::sync::Arc;
7
8pub trait TemplateLoader: Send + Sync {
12 fn load(&self, name: &str) -> Result<String>;
13
14 fn cache_key(&self, name: &str) -> Option<String> {
17 let _ = name;
18 None
19 }
20}
21
22impl TemplateLoader for HashMap<String, String> {
23 fn load(&self, name: &str) -> Result<String> {
24 self.get(name)
25 .cloned()
26 .ok_or_else(|| RunjucksError::new(format!("template not found: {name}")))
27 }
28
29 fn cache_key(&self, name: &str) -> Option<String> {
30 let _ = self;
31 Some(name.to_string())
32 }
33}
34
35pub struct FnLoader<F>(pub F);
37
38impl<F> TemplateLoader for FnLoader<F>
39where
40 F: Fn(&str) -> Result<String> + Send + Sync,
41{
42 fn load(&self, name: &str) -> Result<String> {
43 (self.0)(name)
44 }
45}
46
47pub fn map_loader(map: HashMap<String, String>) -> Arc<dyn TemplateLoader + Send + Sync> {
49 Arc::new(map)
50}
51
52#[derive(Debug)]
56pub struct FileSystemLoader {
57 root: PathBuf,
58}
59
60impl FileSystemLoader {
61 pub fn new(root: impl AsRef<Path>) -> Result<Self> {
63 let root = root.as_ref().canonicalize().map_err(|e| {
64 RunjucksError::new(format!(
65 "filesystem loader: cannot access root {}: {e}",
66 root.as_ref().display()
67 ))
68 })?;
69 Ok(Self { root })
70 }
71
72 fn resolve_safe(&self, name: &str) -> Result<PathBuf> {
73 let path = Path::new(name);
74 if path.is_absolute() {
75 return Err(RunjucksError::new(format!(
76 "template name must be relative, got {name:?}"
77 )));
78 }
79 let mut out = self.root.clone();
80 for c in path.components() {
81 match c {
82 Component::Normal(s) => out.push(s),
83 Component::CurDir => {}
84 Component::ParentDir | Component::RootDir | Component::Prefix(_) => {
85 return Err(RunjucksError::new(format!(
86 "invalid template path (no parent segments): {name:?}"
87 )));
88 }
89 }
90 }
91 let canon = out
92 .canonicalize()
93 .map_err(|_| RunjucksError::new(format!("template not found: {name}")))?;
94 if !canon.starts_with(&self.root) {
95 return Err(RunjucksError::new(format!(
96 "template path escapes loader root: {name}"
97 )));
98 }
99 Ok(canon)
100 }
101}
102
103impl TemplateLoader for FileSystemLoader {
104 fn load(&self, name: &str) -> Result<String> {
105 let path = self.resolve_safe(name)?;
106 std::fs::read_to_string(&path).map_err(|e| {
107 RunjucksError::new(format!("failed to read template {}: {e}", path.display()))
108 })
109 }
110
111 fn cache_key(&self, name: &str) -> Option<String> {
112 self.resolve_safe(name)
113 .ok()
114 .map(|p| p.to_string_lossy().into_owned())
115 }
116}
117
118pub fn file_system_loader(root: impl AsRef<Path>) -> Result<Arc<dyn TemplateLoader + Send + Sync>> {
120 Ok(Arc::new(FileSystemLoader::new(root)?))
121}
122