Skip to main content

tatara_lisp_script/stdlib/
uuid.rs

1//! UUID generation — small, no `uuid` crate dep.
2//!
3//!   (uuid-v4)            → "550e8400-e29b-41d4-a716-446655440000"
4//!
5//! Uses the OS RNG via `/dev/urandom` on Unix or `getrandom` syscall on
6//! recent Linux. Falls back to `std::time::SystemTime`-derived nonce
7//! plus a counter on systems where neither is available — sufficient
8//! for log correlation IDs but not cryptographically secure on those
9//! platforms; the implementation prefers the OS RNG everywhere it can.
10
11use std::sync::Arc;
12
13use tatara_lisp_eval::{Arity, EvalError, Interpreter, Value};
14
15use crate::script_ctx::ScriptCtx;
16
17pub fn install(interp: &mut Interpreter<ScriptCtx>) {
18    interp.register_fn(
19        "uuid-v4",
20        Arity::Exact(0),
21        |_args: &[Value], _ctx: &mut ScriptCtx, sp| {
22            let mut buf = [0u8; 16];
23            fill_random(&mut buf)
24                .map_err(|e| EvalError::native_fn("uuid-v4", format!("rng: {e}"), sp))?;
25            // Set version (4) + variant (RFC 4122).
26            buf[6] = (buf[6] & 0x0f) | 0x40;
27            buf[8] = (buf[8] & 0x3f) | 0x80;
28            Ok(Value::Str(Arc::from(format_uuid(&buf))))
29        },
30    );
31
32    interp.register_fn(
33        "random-bytes",
34        Arity::Exact(1),
35        |args: &[Value], _ctx: &mut ScriptCtx, sp| {
36            let n: usize = match &args[0] {
37                Value::Int(n) if *n >= 0 => usize::try_from(*n).map_err(|_| {
38                    EvalError::native_fn("random-bytes", format!("size {n} too large"), sp)
39                })?,
40                v => {
41                    return Err(EvalError::native_fn(
42                        "random-bytes",
43                        format!("size must be non-negative int, got {v:?}"),
44                        sp,
45                    ))
46                }
47            };
48            let mut buf = vec![0u8; n];
49            fill_random(&mut buf)
50                .map_err(|e| EvalError::native_fn("random-bytes", format!("rng: {e}"), sp))?;
51            Ok(Value::Str(Arc::from(hex_lower(&buf))))
52        },
53    );
54}
55
56fn fill_random(buf: &mut [u8]) -> std::io::Result<()> {
57    // Best path: /dev/urandom (works on every Unix incl. macOS).
58    use std::io::Read;
59    let mut f = std::fs::File::open("/dev/urandom")?;
60    f.read_exact(buf)?;
61    Ok(())
62}
63
64fn format_uuid(b: &[u8; 16]) -> String {
65    // 8-4-4-4-12 grouping.
66    format!(
67        "{:02x}{:02x}{:02x}{:02x}-\
68         {:02x}{:02x}-{:02x}{:02x}-\
69         {:02x}{:02x}-\
70         {:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
71        b[0],
72        b[1],
73        b[2],
74        b[3],
75        b[4],
76        b[5],
77        b[6],
78        b[7],
79        b[8],
80        b[9],
81        b[10],
82        b[11],
83        b[12],
84        b[13],
85        b[14],
86        b[15],
87    )
88}
89
90fn hex_lower(bytes: &[u8]) -> String {
91    let mut s = String::with_capacity(bytes.len() * 2);
92    for b in bytes {
93        s.push_str(&format!("{b:02x}"));
94    }
95    s
96}