rsnip/infrastructure/
minijinja.rs1use crate::domain::{
3 content::SnippetContent,
4 template::{
5 errors::TemplateError,
6 interface::{ShellCommandExecutor, TemplateEngine},
7 },
8};
9use chrono::{DateTime, Utc};
10use minijinja::{Environment, Error, ErrorKind, Value};
11use std::collections::HashMap;
12use std::process::Command;
13use tracing::{info};
14
15pub struct MiniJinjaEngine {
16 env: Environment<'static>,
17}
18
19impl MiniJinjaEngine {
20 pub fn new(shell_executor: Box<dyn ShellCommandExecutor>) -> Self {
21 let mut env = Environment::new();
22
23 env.add_filter("strftime", date_format);
25 env.add_filter("subtract_days", subtract_days);
26 env.add_filter("add_days", add_days);
27
28 let shell_executor_clone = shell_executor.box_clone();
30 env.add_filter("shell", move |value: Value| {
31 let cmd = value.as_str().ok_or_else(|| {
32 Error::new(ErrorKind::InvalidOperation, "Expected string command")
33 })?;
34
35 match shell_executor_clone.execute(cmd) {
36 Ok(result) => Ok(Value::from(result)),
37 Err(e) => Err(Error::new(ErrorKind::InvalidOperation, e.to_string())),
38 }
39 });
40
41 Self {
42 env,
43 }
44 }
45
46 fn create_context(&self) -> HashMap<String, Value> {
47 let mut context = HashMap::new();
48
49 context.insert(
51 "current_date".to_string(),
52 Value::from(Utc::now().to_rfc3339()),
53 );
54
55 for (key, value) in std::env::vars() {
57 context.insert(format!("env_{}", key), Value::from(value));
58 }
59
60 context
61 }
62}
63
64impl TemplateEngine for MiniJinjaEngine {
65 fn render(&self, content: &SnippetContent) -> Result<String, TemplateError> {
66 match content {
67 SnippetContent::Static(s) => Ok(s.clone()),
68 SnippetContent::Template { source, .. } => {
69 let template = self
70 .env
71 .template_from_str(source)
72 .map_err(|e| TemplateError::Syntax(e.to_string()))?;
73
74 let context = self.create_context();
75
76 template
77 .render(context)
78 .map_err(|e| TemplateError::Rendering(e.to_string()))
79 }
80 }
81 }
82}
83
84#[derive(Clone, Debug)]
86pub struct SafeShellExecutor;
87
88impl SafeShellExecutor {
89 pub fn new() -> Self {
90 Self
91 }
92
93 fn is_command_safe(&self, cmd: &str) -> bool {
94 let dangerous_patterns = [
95 ";", "|", "&", ">", "<", "`", "$", "(", ")", "{", "}", "[", "]", "sudo", "rm", "mv",
96 "cp", "dd", "mkfs", "fork", "kill",
97 ];
98
99 !dangerous_patterns
100 .iter()
101 .any(|pattern| cmd.contains(pattern))
102 }
103}
104
105impl ShellCommandExecutor for SafeShellExecutor {
106 fn execute(&self, cmd: &str) -> Result<String, TemplateError> {
107 info!("Executing shell command: {}", cmd);
108
109 if !self.is_command_safe(cmd) {
110 return Err(TemplateError::Shell(
111 "Command contains forbidden patterns".to_string(),
112 ));
113 }
114
115 let output = Command::new("sh")
116 .arg("-c")
117 .arg(cmd)
118 .output()
119 .map_err(|e| TemplateError::Shell(format!("Failed to execute command: {}", e)))?;
120
121 if !output.status.success() {
122 let error = String::from_utf8_lossy(&output.stderr);
123 return Err(TemplateError::Shell(format!("Command failed: {}", error)));
124 }
125
126 Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
127 }
128 fn box_clone(&self) -> Box<dyn ShellCommandExecutor> {
129 Box::new(self.clone())
130 }
131}
132
133fn date_format(value: Value, args: &[Value]) -> Result<Value, Error> {
135 let date_str = value
136 .as_str()
137 .ok_or_else(|| Error::new(ErrorKind::InvalidOperation, "Expected string date"))?;
138 let format = args.first().and_then(|v| v.as_str()).unwrap_or("%Y-%m-%d");
139
140 let date = DateTime::parse_from_rfc3339(date_str)
141 .map_err(|e| Error::new(ErrorKind::InvalidOperation, format!("Invalid date: {}", e)))?
142 .with_timezone(&Utc);
143
144 Ok(Value::from(date.format(format).to_string()))
145}
146
147fn subtract_days(value: Value, args: &[Value]) -> Result<Value, Error> {
148 let date_str = value
149 .as_str()
150 .ok_or_else(|| Error::new(ErrorKind::InvalidOperation, "Expected date string"))?;
151
152 let date = DateTime::parse_from_rfc3339(date_str)
153 .map_err(|e| Error::new(ErrorKind::InvalidOperation, format!("Invalid date: {}", e)))?;
154
155 let days = args.first().and_then(|v| v.as_i64()).unwrap_or(0);
156 let new_date = date - chrono::Duration::days(days);
157
158 Ok(Value::from(new_date.to_rfc3339()))
159}
160
161fn add_days(value: Value, args: &[Value]) -> Result<Value, Error> {
162 let date_str = value
163 .as_str()
164 .ok_or_else(|| Error::new(ErrorKind::InvalidOperation, "Expected date string"))?;
165
166 let date = DateTime::parse_from_rfc3339(date_str)
167 .map_err(|e| Error::new(ErrorKind::InvalidOperation, format!("Invalid date: {}", e)))?;
168
169 let days = args.first().and_then(|v| v.as_i64()).unwrap_or(0);
170 let new_date = date + chrono::Duration::days(days);
171
172 Ok(Value::from(new_date.to_rfc3339()))
173}
174