Skip to main content

polydat_nodes/
digest.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Cryptographic digest and base encoding nodes.
5
6// Alias the upstream digest types so the macro-generated
7// `Sha256` / `Md5` structs don't collide.
8use md5::Md5 as Md5Hasher;
9use sha2::{Digest as Sha2Digest, Sha256 as Sha2_256};
10
11// Bytes travel as native Rust types:
12//   - input  Bytes:  `&[u8]` (borrow, zero-alloc)
13//   - output Bytes:  `Vec<u8>` (owned, becomes `Arc<[u8]>` through the
14//     `Wire` impl's `inject`)
15
16#[polydat::polydat_node(category = Digest)]
17fn sha256(input: &[u8]) -> Vec<u8> {
18    let mut hasher = Sha2_256::new();
19    hasher.update(input);
20    hasher.finalize().to_vec()
21}
22
23#[polydat::polydat_node(category = Digest)]
24fn md5(input: &[u8]) -> Vec<u8> {
25    let mut hasher = Md5Hasher::new();
26    hasher.update(input);
27    hasher.finalize().to_vec()
28}
29
30#[polydat::polydat_node(category = Digest)]
31fn to_base64(input: &[u8]) -> String {
32    use base64::Engine;
33    base64::engine::general_purpose::STANDARD.encode(input)
34}
35
36#[polydat::polydat_node(category = Digest)]
37fn from_base64(input: &str) -> Vec<u8> {
38    use base64::Engine;
39    base64::engine::general_purpose::STANDARD
40        .decode(input)
41        .unwrap_or_default()
42}
43
44#[polydat::polydat_node(category = Digest)]
45fn to_base32(input: &[u8]) -> String {
46    data_encoding::BASE32.encode(input)
47}
48
49#[polydat::polydat_node(category = Digest)]
50fn from_base32(input: &str) -> Vec<u8> {
51    data_encoding::BASE32
52        .decode(input.as_bytes())
53        .unwrap_or_default()
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59    use polydat::ast::{PolydatNode, Value};
60
61    #[test]
62    fn sha256_known() {
63        let node = Sha256::default();
64        let mut out = [Value::None];
65        // SHA-256 of empty string
66        node.eval(&[Value::Bytes(vec![].into())], &mut out[..]);
67        let bytes = out[0].as_bytes();
68        assert_eq!(bytes.len(), 32);
69        // Known hash of empty: e3b0c44298fc1c14...
70        assert_eq!(bytes[0], 0xe3);
71        assert_eq!(bytes[1], 0xb0);
72    }
73
74    #[test]
75    fn sha256_deterministic() {
76        let node = Sha256::default();
77        let mut out1 = [Value::None];
78        let mut out2 = [Value::None];
79        let input = Value::Bytes(b"hello world".to_vec().into());
80        node.eval(std::slice::from_ref(&input), &mut out1);
81        node.eval(&[input], &mut out2);
82        assert_eq!(out1[0].as_bytes(), out2[0].as_bytes());
83    }
84
85    #[test]
86    fn md5_known() {
87        let node = Md5::default();
88        let mut out = [Value::None];
89        node.eval(&[Value::Bytes(vec![].into())], &mut out[..]);
90        let bytes = out[0].as_bytes();
91        assert_eq!(bytes.len(), 16);
92        // Known MD5 of empty: d41d8cd98f00b204...
93        assert_eq!(bytes[0], 0xd4);
94    }
95
96    #[test]
97    fn base64_roundtrip() {
98        let enc = ToBase64::default();
99        let dec = FromBase64::default();
100        let mut mid = [Value::None];
101        let mut out = [Value::None];
102        let input = vec![0xDE, 0xAD, 0xBE, 0xEF, 0x42];
103        enc.eval(&[Value::Bytes(input.clone().into())], &mut mid[..]);
104        dec.eval(&[mid[0].clone()], &mut out);
105        assert_eq!(out[0].as_bytes(), &input);
106    }
107
108    #[test]
109    fn base32_roundtrip() {
110        let enc = ToBase32::default();
111        let dec = FromBase32::default();
112        let mut mid = [Value::None];
113        let mut out = [Value::None];
114        let input = vec![0xDE, 0xAD, 0xBE, 0xEF, 0x42];
115        enc.eval(&[Value::Bytes(input.clone().into())], &mut mid[..]);
116        dec.eval(&[mid[0].clone()], &mut out);
117        assert_eq!(out[0].as_bytes(), &input);
118    }
119}