1#[cfg(feature = "crypto")] use crate::hashes::Argon;
11use crate::{Error, Result, hbs_json};
12#[cfg(feature = "rhai")] use crate::{RhaiRes, rhai_err};
13use base64::{Engine as _, engine::general_purpose::STANDARD};
14use handlebars::{Handlebars, handlebars_helper};
15use handlebars_misc_helpers::new_hbs;
16use regex::Regex;
17use serde_json::Value;
18use std::{fs, path::PathBuf};
19use tracing::*;
20use url::form_urlencoded;
21
22pub const CORE_HBS_HELPERS: &[&str] = &[
24 "if",
26 "unless",
27 "each",
28 "with",
29 "lookup",
30 "raw",
31 "log",
32 "inline",
33 "eq",
34 "ne",
35 "gt",
36 "gte",
37 "lt",
38 "lte",
39 "and",
40 "or",
41 "not",
42 "len",
43 "lowerCamelCase",
45 "upperCamelCase",
46 "snakeCase",
47 "kebabCase",
48 "shoutySnakeCase",
49 "shoutyKebabCase",
50 "titleCase",
51 "trainCase",
52 "read_to_str",
54 "parent",
56 "file_name",
57 "extension",
58 "canonicalize",
59 "env_var",
61 "to_lower_case",
63 "to_upper_case",
64 "trim",
65 "trim_start",
66 "trim_end",
67 "replace",
68 "quote",
69 "unquote",
70 "first_non_empty",
71 "json_to_str",
73 "str_to_json",
74 "from_json",
75 "to_json",
76 "json_query",
77 "json_str_query",
78 "jsonnet",
80 "regex_captures",
82 "regex_is_match",
83 "uuid_new_v4",
85 "uuid_new_v7",
86 "base64_encode",
88 "base64_decode",
89 "url_encode",
90 "to_decimal",
91 "header_basic",
92 #[cfg(feature = "crypto")]
93 "argon_hash",
94 #[cfg(feature = "crypto")]
95 "bcrypt_hash",
96 "crc32_hash",
97 #[cfg(feature = "password")]
98 "gen_password",
99 #[cfg(feature = "password")]
100 "gen_password_alphanum",
101 #[cfg(feature = "crypto")]
102 "gen_private_key",
103 "concat",
104];
105
106handlebars_helper!(base64_decode: |arg:Value| String::from_utf8(STANDARD.decode(arg.as_str().unwrap_or_else(|| {
107 warn!("handlebars::base64_decode received a non-string parameter: {:?}",arg);
108 ""
109})).unwrap_or_else(|e| {
110 warn!("handlebars::base64_decode failed to decode with: {e:?}");
111 vec![]
112})).unwrap_or_else(|e| {
113 warn!("handlebars::base64_decode failed to convert to string with: {e:?}");
114 String::new()
115}));
116handlebars_helper!(base64_encode: |arg:Value| STANDARD.encode(arg.as_str().unwrap_or_else(|| {
117 warn!("handlebars::base64_encode received a non-string parameter: {:?}",arg);
118 ""
119})));
120handlebars_helper!(url_encode: |arg:Value| form_urlencoded::byte_serialize(arg.as_str().unwrap_or_else(|| {
121 warn!("handlebars::url_encode received a non-string parameter: {:?}",arg);
122 ""
123}).as_bytes()).collect::<String>());
124handlebars_helper!(to_decimal: |arg:Value| format!("{}", u32::from_str_radix(arg.as_str().unwrap_or_else(|| {
125 warn!("handlebars::to_decimal received a non-string parameter: {:?}",arg);
126 ""
127}), 8).unwrap_or_else(|_| {
128 warn!("handlebars::to_decimal received a non-string parameter: {:?}",arg);
129 0
130})));
131handlebars_helper!(header_basic: |username:Value, password:Value| format!("Basic {}",STANDARD.encode(format!("{}:{}",username.as_str().unwrap_or_else(|| {
132 warn!("handlebars::header_basic received a non-string username: {:?}",username);
133 ""
134}),password.as_str().unwrap_or_else(|| {
135 warn!("handlebars::header_basic received a non-string password: {:?}",password);
136 ""
137})))));
138#[cfg(feature = "crypto")]
139handlebars_helper!(argon_hash: |password:Value| Argon::new().hash(password.as_str().unwrap_or_else(|| {
140 warn!("handlebars::argon_hash received a non-string password: {:?}",password);
141 ""
142}).to_string()).unwrap_or_else(|e| {
143 warn!("handlebars::argon_hash failed to convert to string with: {e:?}");
144 String::new()
145}));
146#[cfg(feature = "crypto")]
147handlebars_helper!(bcrypt_hash: |password:Value| crate::hashes::bcrypt_hash(password.as_str().unwrap_or_else(|| {
148 warn!("handlebars::bcrypt_hash received a non-string password: {:?}",password);
149 ""
150}).to_string()).unwrap_or_else(|e| {
151 warn!("handlebars::bcrypt_hash failed to convert to string with: {e:?}");
152 String::new()
153}));
154handlebars_helper!(crc32_hash: |password:Value| crate::hashes::crc32_hash(password.as_str().unwrap_or_else(|| {
155 warn!("handlebars::crc32_hash received a non-string password: {:?}",password);
156 ""
157}).to_string()));
158#[cfg(feature = "password")]
159handlebars_helper!(gen_password: |len:u32, {lower:u32=1, upper:u32=1, digits:u32=1, symbols:u32=1}| crate::password::generate(len as usize, lower as usize, upper as usize, digits as usize, symbols as usize).unwrap_or_else(|e| {
160 warn!("handlebars::gen_password failed with: {e:?}");
161 String::new()
162}));
163#[cfg(feature = "password")]
164handlebars_helper!(gen_password_alphanum: |len:u32| crate::password::generate(len as usize, 1, 1, 1, 0).unwrap_or_else(|e| {
165 warn!("handlebars::gen_password_alphanum failed with: {e:?}");
166 String::new()
167}));
168#[cfg(feature = "crypto")]
169handlebars_helper!(gen_private_key: |algo:str, {bits:u32=4096}| crate::key::gen_private_key(algo, bits).unwrap_or_else(|e| {
170 warn!("handlebars::gen_private_key failed with: {e:?}");
171 String::new()
172}));
173handlebars_helper!(concat: |a: Value, b: Value| format!("{}{}", a.as_str().unwrap_or_else(|| {
174 warn!("handlebars::concat received a non-string parameter: {:?}", a);
175 ""
176}),b.as_str().unwrap_or_else(|| {
177 warn!("handlebars::concat received a non-string parameter: {:?}", b);
178 ""
179})));
180
181#[derive(Clone, Debug)]
185pub struct HandleBars<'a> {
186 engine: Handlebars<'a>,
187}
188impl<'a> HandleBars<'a> {
189 #[must_use]
191 pub fn new() -> HandleBars<'static> {
192 let mut engine = new_hbs();
193 hbs_json::register(&mut engine);
194 engine.register_helper("concat", Box::new(concat));
195 engine.register_helper("to_decimal", Box::new(to_decimal));
196 engine.register_helper("base64_decode", Box::new(base64_decode));
197 engine.register_helper("base64_encode", Box::new(base64_encode));
198 engine.register_helper("header_basic", Box::new(header_basic));
199 #[cfg(feature = "crypto")]
200 {
201 engine.register_helper("argon_hash", Box::new(argon_hash));
202 engine.register_helper("bcrypt_hash", Box::new(bcrypt_hash));
203 engine.register_helper("gen_private_key", Box::new(gen_private_key));
204 }
205 engine.register_helper("url_encode", Box::new(url_encode));
206 #[cfg(feature = "password")]
207 engine.register_helper("gen_password", Box::new(gen_password));
208 #[cfg(feature = "password")]
209 engine.register_helper("gen_password_alphanum", Box::new(gen_password_alphanum));
210 engine.register_helper("crc32_hash", Box::new(crc32_hash));
211 HandleBars { engine }
212 }
213
214 #[must_use]
216 pub fn engine_mut(&mut self) -> &mut Handlebars<'a> {
217 &mut self.engine
218 }
219
220 pub fn register_template(&mut self, name: &str, template: &str) -> Result<()> {
222 self.engine
223 .register_template_string(name, template)
224 .map_err(Error::HbsTemplateError)
225 }
226
227 #[cfg(feature = "rhai")]
228 pub fn rhai_register_template(&mut self, name: String, template: String) -> RhaiRes<()> {
229 self.register_template(name.as_str(), template.as_str())
230 .map_err(|e| format!("{e}").into())
231 }
232
233 #[cfg(feature = "hbs-scripting")]
236 pub fn register_helper_dir(&mut self, directory: PathBuf) -> Result<()> {
237 if std::path::Path::new(&directory).is_dir() {
238 let re_rhai = Regex::new(r"\.rhai$").unwrap();
239 for file in fs::read_dir(directory).unwrap() {
240 let path = file.unwrap().path();
241 let filename = path.file_name().unwrap().to_str().unwrap();
242 if re_rhai.is_match(filename) {
243 let name = filename[0..(filename.len() - 5)].to_string();
244 self.engine
245 .register_script_helper_file(&name, path)
246 .map_err(|e| Error::Other(format!("{:?}", e)))?;
247 }
248 }
249 Ok(())
250 } else {
251 Ok(())
252 }
253 }
254
255 #[cfg(feature = "hbs-scripting")]
257 pub fn rhai_register_helper_dir(&mut self, directory: String) -> RhaiRes<()> {
258 self.register_helper_dir(PathBuf::from(directory))
259 .map_err(rhai_err)
260 }
261
262 pub fn register_partial_dir(&mut self, directory: PathBuf) -> Result<()> {
264 if std::path::Path::new(&directory).is_dir() {
265 let re_rhai = Regex::new(r"\.hbs$").unwrap();
266 for file in fs::read_dir(directory).unwrap() {
267 let path = file.unwrap().path();
268 let filename = path.file_name().unwrap().to_str().unwrap();
269 if re_rhai.is_match(filename) {
270 let name = filename[0..(filename.len() - 4)].to_string();
271 let tmpl = std::fs::read_to_string(path).map_err(Error::Stdio)?;
272 tracing::debug!("registering {}", name);
273 self.register_template(&name, &tmpl)?;
274 }
275 }
276 Ok(())
277 } else {
278 Ok(())
279 }
280 }
281
282 #[cfg(feature = "rhai")]
283 pub fn rhai_register_partial_dir(&mut self, directory: String) -> RhaiRes<()> {
284 self.register_partial_dir(PathBuf::from(directory))
285 .map_err(rhai_err)
286 }
287
288 pub fn render(&mut self, template: &str, data: &serde_json::Value) -> Result<String> {
290 self.engine
291 .render_template(template, data)
292 .map_err(Error::HbsRenderError)
293 }
294
295 #[cfg(feature = "rhai")]
296 pub fn rhai_render(&mut self, template: String, data: rhai::Map) -> RhaiRes<String> {
297 let json_data: serde_json::Value =
298 serde_json::from_str(&serde_json::to_string(&data).map_err(|e| format!("{e}"))?)
299 .map_err(|e| format!("{e}"))?;
300 self.engine
301 .render_template(template.as_str(), &json_data)
302 .map_err(|e| format!("{e}").into())
303 }
304
305 pub fn render_named(&mut self, name: &str, template: &str, data: &serde_json::Value) -> Result<String> {
307 self.engine
308 .register_template_string(name, template)
309 .map_err(Error::HbsTemplateError)?;
310 self.engine.render(name, data).map_err(Error::HbsRenderError)
311 }
312
313 #[cfg(feature = "rhai")]
314 pub fn rhai_render_named(&mut self, name: String, template: String, data: rhai::Map) -> RhaiRes<String> {
315 let json_data: serde_json::Value =
316 serde_json::from_str(&serde_json::to_string(&data).map_err(|e| format!("{e}"))?)
317 .map_err(|e| format!("{e}"))?;
318 self.engine
319 .register_template_string(name.as_str(), template)
320 .map_err(Error::HbsTemplateError)
321 .map_err(rhai_err)?;
322 self.engine
323 .render(name.as_str(), &json_data)
324 .map_err(|e| format!("{e}").into())
325 }
326}