Skip to main content

tatara_lisp_script/stdlib/
encoding.rs

1//! Common text encodings.
2//!
3//!   (base64-encode STR)     → standard base64 (with padding)
4//!   (base64-decode STR)     → decoded string (non-utf8 = error)
5//!   (base64url-encode STR)  → URL-safe base64, no padding
6//!   (base64url-decode STR)  → decoded
7//!   (url-encode STR)        → percent-encoded (RFC 3986 unreserved)
8//!   (url-decode STR)        → percent-decoded
9//!   (hex-encode STR)        → lowercase hex (bytes of STR)
10//!   (hex-decode STR)        → string (non-utf8 = error)
11
12use std::sync::Arc;
13
14use tatara_lisp_eval::{Arity, EvalError, Interpreter, Value};
15
16use crate::script_ctx::ScriptCtx;
17use crate::stdlib::env::str_arg;
18
19pub fn install(interp: &mut Interpreter<ScriptCtx>) {
20    use base64::Engine;
21
22    interp.register_fn(
23        "base64-encode",
24        Arity::Exact(1),
25        |args: &[Value], _ctx: &mut ScriptCtx, sp| {
26            let engine = base64::engine::general_purpose::STANDARD;
27            let s = str_arg(&args[0], "base64-encode", sp)?;
28            Ok(Value::Str(Arc::from(engine.encode(s.as_bytes()))))
29        },
30    );
31
32    interp.register_fn(
33        "base64-decode",
34        Arity::Exact(1),
35        |args: &[Value], _ctx: &mut ScriptCtx, sp| {
36            let engine = base64::engine::general_purpose::STANDARD;
37            let s = str_arg(&args[0], "base64-decode", sp)?;
38            let bytes = engine
39                .decode(s.as_bytes())
40                .map_err(|e| EvalError::native_fn("base64-decode", e.to_string(), sp))?;
41            Ok(Value::Str(Arc::from(String::from_utf8(bytes).map_err(
42                |e| EvalError::native_fn("base64-decode", format!("not utf8: {e}"), sp),
43            )?)))
44        },
45    );
46
47    interp.register_fn(
48        "base64url-encode",
49        Arity::Exact(1),
50        |args: &[Value], _ctx: &mut ScriptCtx, sp| {
51            let engine = base64::engine::general_purpose::URL_SAFE_NO_PAD;
52            let s = str_arg(&args[0], "base64url-encode", sp)?;
53            Ok(Value::Str(Arc::from(engine.encode(s.as_bytes()))))
54        },
55    );
56
57    interp.register_fn(
58        "base64url-decode",
59        Arity::Exact(1),
60        |args: &[Value], _ctx: &mut ScriptCtx, sp| {
61            let engine = base64::engine::general_purpose::URL_SAFE_NO_PAD;
62            let s = str_arg(&args[0], "base64url-decode", sp)?;
63            let bytes = engine
64                .decode(s.as_bytes())
65                .map_err(|e| EvalError::native_fn("base64url-decode", e.to_string(), sp))?;
66            Ok(Value::Str(Arc::from(String::from_utf8(bytes).map_err(
67                |e| EvalError::native_fn("base64url-decode", format!("not utf8: {e}"), sp),
68            )?)))
69        },
70    );
71
72    interp.register_fn(
73        "url-encode",
74        Arity::Exact(1),
75        |args: &[Value], _ctx: &mut ScriptCtx, sp| {
76            let s = str_arg(&args[0], "url-encode", sp)?;
77            Ok(Value::Str(Arc::from(url_pct_encode(&s))))
78        },
79    );
80
81    interp.register_fn(
82        "url-decode",
83        Arity::Exact(1),
84        |args: &[Value], _ctx: &mut ScriptCtx, sp| {
85            let s = str_arg(&args[0], "url-decode", sp)?;
86            Ok(Value::Str(Arc::from(
87                url_pct_decode(&s).map_err(|e| EvalError::native_fn("url-decode", e, sp))?,
88            )))
89        },
90    );
91
92    interp.register_fn(
93        "hex-encode",
94        Arity::Exact(1),
95        |args: &[Value], _ctx: &mut ScriptCtx, sp| {
96            let s = str_arg(&args[0], "hex-encode", sp)?;
97            Ok(Value::Str(Arc::from(hex::encode(s.as_bytes()))))
98        },
99    );
100
101    interp.register_fn(
102        "hex-decode",
103        Arity::Exact(1),
104        |args: &[Value], _ctx: &mut ScriptCtx, sp| {
105            let s = str_arg(&args[0], "hex-decode", sp)?;
106            let bytes = hex::decode(s.as_bytes())
107                .map_err(|e| EvalError::native_fn("hex-decode", e.to_string(), sp))?;
108            Ok(Value::Str(Arc::from(String::from_utf8(bytes).map_err(
109                |e| EvalError::native_fn("hex-decode", format!("not utf8: {e}"), sp),
110            )?)))
111        },
112    );
113}
114
115fn url_pct_encode(s: &str) -> String {
116    let mut out = String::with_capacity(s.len());
117    for &b in s.as_bytes() {
118        if matches!(b, b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~') {
119            out.push(b as char);
120        } else {
121            out.push_str(&format!("%{b:02X}"));
122        }
123    }
124    out
125}
126
127fn url_pct_decode(s: &str) -> Result<String, String> {
128    let bytes = s.as_bytes();
129    let mut out = Vec::with_capacity(bytes.len());
130    let mut i = 0;
131    while i < bytes.len() {
132        if bytes[i] == b'%' {
133            if i + 2 >= bytes.len() {
134                return Err(format!("truncated percent-escape at {i}"));
135            }
136            let hex_str = std::str::from_utf8(&bytes[i + 1..i + 3])
137                .map_err(|e| format!("bad utf8 in escape: {e}"))?;
138            let byte =
139                u8::from_str_radix(hex_str, 16).map_err(|e| format!("bad hex {hex_str:?}: {e}"))?;
140            out.push(byte);
141            i += 3;
142        } else {
143            out.push(bytes[i]);
144            i += 1;
145        }
146    }
147    String::from_utf8(out).map_err(|e| format!("non-utf8 result: {e}"))
148}