atmos/os_lib/aura/builtins/
template.rs1use super::*;
4
5pub(crate) fn parse_format_spec(spec: &str) -> FormatSpec {
6 let mut f = FormatSpec {
7 alternate: false,
8 zero_pad: false,
9 width: 0,
10 comma: false,
11 precision: None,
12 fmt_type: None,
13 };
14
15 let mut chars = spec.chars().peekable();
16
17 if chars.peek() == Some(&'#') {
18 f.alternate = true;
19 chars.next();
20 }
21
22 if chars.peek() == Some(&'0') {
23 f.zero_pad = true;
24 chars.next();
25 }
26
27 let mut width_str = String::new();
28 while let Some(&c) = chars.peek() {
29 if c.is_ascii_digit() {
30 width_str.push(c);
31 chars.next();
32 } else {
33 break;
34 }
35 }
36 if !width_str.is_empty() {
37 f.width = width_str.parse().unwrap_or(0);
38 }
39
40 if chars.peek() == Some(&',') {
41 f.comma = true;
42 chars.next();
43 }
44
45 if chars.peek() == Some(&'.') {
46 chars.next();
47 let mut prec_str = String::new();
48 while let Some(&c) = chars.peek() {
49 if c.is_ascii_digit() {
50 prec_str.push(c);
51 chars.next();
52 } else {
53 break;
54 }
55 }
56 if !prec_str.is_empty() {
57 f.precision = Some(prec_str.parse().unwrap_or(0));
58 }
59 }
60
61 if let Some(&c) = chars.peek() {
62 f.fmt_type = Some(c);
63 chars.next();
64 }
65
66 f
67}
68
69pub(crate) fn bigint_to_bin(mut val: BigInt) -> String {
70 if val.is_zero() {
71 return String::from("0");
72 }
73 let mut bin = String::new();
74 let two = BigInt::from_i64(2);
75 let sign = val.sign;
76 val.sign = true;
77 while !val.is_zero() {
78 if let Some((q, r)) = val.div_rem(&two) {
79 let rem = r.to_i64().unwrap_or(0);
80 bin.push(if rem == 0 { '0' } else { '1' });
81 val = q;
82 } else {
83 break;
84 }
85 }
86 let mut s = String::new();
87 if !sign {
88 s.push('-');
89 }
90 s.extend(bin.chars().rev());
91 s
92}
93
94pub(crate) fn bigint_to_hex(mut val: BigInt, uppercase: bool) -> String {
95 if val.is_zero() {
96 return String::from("0");
97 }
98 let mut hex = String::new();
99 let sixteen = BigInt::from_i64(16);
100 let sign = val.sign;
101 val.sign = true;
102 let hex_chars = if uppercase {
103 "0123456789ABCDEF"
104 } else {
105 "0123456789abcdef"
106 };
107 while !val.is_zero() {
108 if let Some((q, r)) = val.div_rem(&sixteen) {
109 let rem = r.to_i64().unwrap_or(0) as usize;
110 hex.push(hex_chars.chars().nth(rem).unwrap_or('0'));
111 val = q;
112 } else {
113 break;
114 }
115 }
116 let mut s = String::new();
117 if !sign {
118 s.push('-');
119 }
120 s.extend(hex.chars().rev());
121 s
122}
123
124pub(crate) fn format_comma(s: &str) -> String {
125 let mut res = String::new();
126 let mut count = 0;
127 for c in s.chars().rev() {
128 if c.is_ascii_digit() {
129 if count > 0 && count % 3 == 0 {
130 res.push(',');
131 }
132 count += 1;
133 }
134 res.push(c);
135 }
136 res.chars().rev().collect()
137}
138
139pub(crate) fn apply_padding(val_str: String, spec: &FormatSpec, prefix: &str, is_negative: bool) -> String {
140 let mut clean_str = val_str;
141 let mut needed_len = spec.width;
142 if is_negative {
143 needed_len = needed_len.saturating_sub(1);
144 }
145 needed_len = needed_len.saturating_sub(prefix.len());
146
147 if clean_str.len() < needed_len && spec.zero_pad {
148 let pad_len = needed_len - clean_str.len();
149 let mut padded = String::new();
150 for _ in 0..pad_len {
151 padded.push('0');
152 }
153 padded.push_str(&clean_str);
154 clean_str = padded;
155 }
156
157 let mut final_str = String::new();
158 if is_negative {
159 final_str.push('-');
160 }
161 final_str.push_str(prefix);
162 final_str.push_str(&clean_str);
163 final_str
164}
165
166pub(crate) fn format_value(val: &Value, spec_str: &str) -> Result<String, String> {
167 if spec_str.is_empty() {
168 return Ok(match val {
169 Value::Str(s) => s.clone(),
170 _ => val.to_string(),
171 });
172 }
173
174 let spec = parse_format_spec(spec_str);
175
176 match val {
177 Value::Num(n) => {
178 let mut prefix = "";
179 let mut val_str;
180 let is_negative = !n.sign && !n.is_zero();
181 let mut abs_n = n.clone();
182 abs_n.sign = true;
183
184 match spec.fmt_type {
185 Some('b') => {
186 val_str = bigint_to_bin(abs_n);
187 if spec.alternate {
188 prefix = "0b";
189 }
190 }
191 Some('x') => {
192 val_str = bigint_to_hex(abs_n, false);
193 if spec.alternate {
194 prefix = "0x";
195 }
196 }
197 Some('X') => {
198 val_str = bigint_to_hex(abs_n, true);
199 if spec.alternate {
200 prefix = "0X";
201 }
202 }
203 _ => {
204 val_str = abs_n.to_string();
205 if spec.comma {
206 val_str = format_comma(&val_str);
207 }
208 }
209 }
210
211 Ok(apply_padding(val_str, &spec, prefix, is_negative))
212 }
213 Value::Ratio(r) => {
214 let val_f64 = r.to_f64();
215 let prec = spec.precision.unwrap_or(6);
216 let mut val_str = match spec.fmt_type {
217 Some('f') => alloc::format!("{:.1$}", val_f64, prec),
218 Some('e') => alloc::format!("{:.1$e}", val_f64, prec),
219 Some('E') => alloc::format!("{:.1$E}", val_f64, prec),
220 _ => {
221 let mut s = r.to_string();
222 if spec.comma {
223 s = format_comma(&s);
224 }
225 s
226 }
227 };
228 if val_str.len() < spec.width && spec.zero_pad {
229 let is_negative = val_f64 < 0.0;
230 let clean_str = if is_negative {
231 val_str.trim_start_matches('-')
232 } else {
233 &val_str
234 };
235 let pad_len = spec
236 .width
237 .saturating_sub(clean_str.len() + if is_negative { 1 } else { 0 });
238 let mut padded = String::new();
239 if is_negative {
240 padded.push('-');
241 }
242 for _ in 0..pad_len {
243 padded.push('0');
244 }
245 padded.push_str(clean_str);
246 val_str = padded;
247 }
248 Ok(val_str)
249 }
250 Value::Complex(c) => Ok(c.to_string()),
251 Value::Str(s) => Ok(s.clone()),
252 _ => Ok(val.to_string()),
253 }
254}
255
256#[allow(clippy::collapsible_if, clippy::collapsible_match)]
259pub(crate) fn builtin_os_template_eval(env: &mut Env, args: &[AST]) -> Result<Value, String> {
260 let vals = eval_args(env, args)?;
261 if vals.len() != 3 {
262 return Err(String::from("os.template-eval requires exactly 3 arguments: (os.template-eval template values formats)"));
263 }
264 let template = match &vals[0] {
265 Value::Str(s) => s,
266 _ => {
267 return Err(String::from(
268 "os.template-eval first argument must be a string",
269 ))
270 }
271 };
272 let values = match &vals[1] {
273 Value::List(l) => l,
274 _ => {
275 return Err(String::from(
276 "os.template-eval second argument must be a list",
277 ))
278 }
279 };
280 let formats = match &vals[2] {
281 Value::List(l) => l,
282 _ => {
283 return Err(String::from(
284 "os.template-eval third argument must be a list",
285 ))
286 }
287 };
288
289 if values.len() != formats.len() {
290 return Err(String::from(
291 "os.template-eval error: values list and formats list must have the same length",
292 ));
293 }
294
295 {
297 let guard_opt = TEMPLATE_GUARD.lock();
298 if let Some(guard) = guard_opt.as_ref() {
299 if let Value::Lambda(arg_names, body_ast, closure_env) = guard {
300 if arg_names.len() == 2 {
301 let mut local_env = Env::with_parent(*closure_env.clone());
302 local_env.set(arg_names[0].clone(), Value::Str(template.clone()));
303 local_env.set(arg_names[1].clone(), Value::List(values.clone()));
304
305 let res = eval(&mut local_env, body_ast)?;
306 let res = force_eval(&mut local_env, res)?;
307
308 if !super::super::eval::is_truthy(&res)? {
309 return Err(String::from(
310 "Security Error: Template evaluation blocked by pre-guard",
311 ));
312 }
313 }
314 }
315 }
316 }
317
318 let mut result = String::new();
320 let mut val_iter = values.iter();
321 let mut fmt_iter = formats.iter();
322 let mut chars = template.chars().peekable();
323
324 while let Some(c) = chars.next() {
325 if c == '$' && chars.peek() == Some(&'{') {
326 chars.next(); if chars.next() == Some('}') {
328 if let (Some(val), Some(fmt_val)) = (val_iter.next(), fmt_iter.next()) {
329 let spec_str = match fmt_val {
330 Value::Str(s) => s,
331 _ => {
332 return Err(String::from(
333 "os.template-eval error: format specifier must be a string",
334 ))
335 }
336 };
337 result.push_str(&format_value(val, spec_str)?);
338 } else {
339 return Err(String::from(
340 "os.template-eval error: Not enough values for placeholders",
341 ));
342 }
343 } else {
344 return Err(String::from(
345 "os.template-eval error: Invalid template placeholder structure",
346 ));
347 }
348 } else {
349 result.push(c);
350 }
351 }
352
353 {
355 let guard_opt = TEMPLATE_POST_GUARD.lock();
356 if let Some(guard) = guard_opt.as_ref() {
357 if let Value::Lambda(arg_names, body_ast, closure_env) = guard {
358 if arg_names.len() == 1 {
359 let mut local_env = Env::with_parent(*closure_env.clone());
360 local_env.set(arg_names[0].clone(), Value::Str(result.clone()));
361
362 let res = eval(&mut local_env, body_ast)?;
363 let res = force_eval(&mut local_env, res)?;
364
365 if !super::super::eval::is_truthy(&res)? {
366 return Err(String::from(
367 "Security Error: Template evaluation blocked by post-guard",
368 ));
369 }
370
371 if let Value::Str(sanitized) = res {
372 result = sanitized;
373 }
374 }
375 }
376 }
377 }
378
379 Ok(Value::Str(result))
380}
381
382pub(crate) fn builtin_os_set_template_guard(env: &mut Env, args: &[AST]) -> Result<Value, String> {
383 let vals = eval_args(env, args)?;
384 if vals.len() != 1 {
385 return Err(String::from(
386 "os.set_template_guard requires exactly 1 argument",
387 ));
388 }
389 let guard = vals[0].clone();
390 if !matches!(guard, Value::Lambda(_, _, _)) && !matches!(guard, Value::Nil) {
391 return Err(String::from(
392 "os.set_template_guard argument must be a lambda or nil",
393 ));
394 }
395 *TEMPLATE_GUARD.lock() = if matches!(guard, Value::Nil) {
396 None
397 } else {
398 Some(guard)
399 };
400 Ok(Value::Nil)
401}
402
403pub(crate) fn builtin_os_set_template_post_guard(env: &mut Env, args: &[AST]) -> Result<Value, String> {
404 let vals = eval_args(env, args)?;
405 if vals.len() != 1 {
406 return Err(String::from(
407 "os.set_template_post_guard requires exactly 1 argument",
408 ));
409 }
410 let guard = vals[0].clone();
411 if !matches!(guard, Value::Lambda(_, _, _)) && !matches!(guard, Value::Nil) {
412 return Err(String::from(
413 "os.set_template_post_guard argument must be a lambda or nil",
414 ));
415 }
416 *TEMPLATE_POST_GUARD.lock() = if matches!(guard, Value::Nil) {
417 None
418 } else {
419 Some(guard)
420 };
421 Ok(Value::Nil)
422}