1use std::collections::BTreeMap;
4
5const ID_LENGTH: usize = 8;
6const BASE64URL: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
7const K: [u32; 64] = [
8 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
9 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
10 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
11 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
12 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
13 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
14 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
15 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
16];
17
18#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct MessageIdInput {
21 pub message: String,
23 pub context: Option<String>,
25}
26
27impl MessageIdInput {
28 #[must_use]
30 pub fn new(message: impl Into<String>, context: Option<impl Into<String>>) -> Self {
31 Self {
32 message: message.into(),
33 context: context.map(Into::into),
34 }
35 }
36}
37
38#[must_use]
40pub fn generate_message_id(message: &str, context: Option<&str>) -> String {
41 let mut input = String::new();
42 if let Some(context) = context {
43 input.push_str(context);
44 }
45 input.push_str(message);
46
47 let digest = sha256(input.as_bytes());
48 bytes_to_base64url(&digest)
49}
50
51#[must_use]
53pub fn generate_message_ids(inputs: &[MessageIdInput]) -> BTreeMap<String, String> {
54 let mut results = BTreeMap::new();
55
56 for input in inputs {
57 let id = generate_message_id(&input.message, input.context.as_deref());
58 let key = match &input.context {
59 Some(context) => format!("{}\u{0004}{context}", input.message),
60 None => input.message.clone(),
61 };
62 results.insert(key, id);
63 }
64
65 results
66}
67
68fn bytes_to_base64url(bytes: &[u8]) -> String {
69 let mut result = String::with_capacity(ID_LENGTH);
70 let mut bits = 0u32;
71 let mut value = 0u32;
72
73 for byte in bytes {
74 if result.len() == ID_LENGTH {
75 break;
76 }
77
78 value = (value << 8) | u32::from(*byte);
79 bits += 8;
80
81 while bits >= 6 && result.len() < ID_LENGTH {
82 bits -= 6;
83 let index = ((value >> bits) & 0x3f) as usize;
84 result.push(char::from(BASE64URL[index]));
85 }
86 }
87
88 result
89}
90
91fn sha256(bytes: &[u8]) -> [u8; 32] {
92 let mut padded = bytes.to_vec();
93 let bit_length = (padded.len() as u64) * 8;
94 padded.push(0x80);
95 while (padded.len() % 64) != 56 {
96 padded.push(0);
97 }
98 padded.extend_from_slice(&bit_length.to_be_bytes());
99
100 let mut state = [
101 0x6a09e667u32,
102 0xbb67ae85,
103 0x3c6ef372,
104 0xa54ff53a,
105 0x510e527f,
106 0x9b05688c,
107 0x1f83d9ab,
108 0x5be0cd19,
109 ];
110
111 for chunk in padded.chunks_exact(64) {
112 let mut w = [0u32; 64];
113 for (index, word) in chunk.chunks_exact(4).enumerate().take(16) {
114 w[index] = u32::from_be_bytes([word[0], word[1], word[2], word[3]]);
115 }
116
117 for index in 16..64 {
118 let s0 = w[index - 15].rotate_right(7)
119 ^ w[index - 15].rotate_right(18)
120 ^ (w[index - 15] >> 3);
121 let s1 = w[index - 2].rotate_right(17)
122 ^ w[index - 2].rotate_right(19)
123 ^ (w[index - 2] >> 10);
124 w[index] = w[index - 16]
125 .wrapping_add(s0)
126 .wrapping_add(w[index - 7])
127 .wrapping_add(s1);
128 }
129
130 let mut a = state[0];
131 let mut b = state[1];
132 let mut c = state[2];
133 let mut d = state[3];
134 let mut e = state[4];
135 let mut f = state[5];
136 let mut g = state[6];
137 let mut h = state[7];
138
139 for index in 0..64 {
140 let sum1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25);
141 let choice = (e & f) ^ ((!e) & g);
142 let temp1 = h
143 .wrapping_add(sum1)
144 .wrapping_add(choice)
145 .wrapping_add(K[index])
146 .wrapping_add(w[index]);
147 let sum0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22);
148 let majority = (a & b) ^ (a & c) ^ (b & c);
149 let temp2 = sum0.wrapping_add(majority);
150
151 h = g;
152 g = f;
153 f = e;
154 e = d.wrapping_add(temp1);
155 d = c;
156 c = b;
157 b = a;
158 a = temp1.wrapping_add(temp2);
159 }
160
161 state[0] = state[0].wrapping_add(a);
162 state[1] = state[1].wrapping_add(b);
163 state[2] = state[2].wrapping_add(c);
164 state[3] = state[3].wrapping_add(d);
165 state[4] = state[4].wrapping_add(e);
166 state[5] = state[5].wrapping_add(f);
167 state[6] = state[6].wrapping_add(g);
168 state[7] = state[7].wrapping_add(h);
169 }
170
171 let mut output = [0u8; 32];
172 for (index, word) in state.iter().enumerate() {
173 output[index * 4..index * 4 + 4].copy_from_slice(&word.to_be_bytes());
174 }
175 output
176}
177
178#[cfg(test)]
179mod tests {
180 use std::collections::BTreeMap;
181
182 use super::{generate_message_id, generate_message_ids, MessageIdInput};
183
184 #[test]
185 fn generate_message_id_matches_current_node_implementation() {
186 assert_eq!(generate_message_id("Hello", None), "GF-NsyJx");
187 assert_eq!(generate_message_id("Hello World", None), "pZGm1Av0");
188 assert_eq!(generate_message_id("Open", Some("menu.file")), "QWh_hL4_");
189 assert_eq!(generate_message_id("Привет мир 🌍", None), "Xp6zAZTu");
190 }
191
192 #[test]
193 fn generate_message_ids_batches_results() {
194 let ids = generate_message_ids(&[
195 MessageIdInput::new("Hello", None::<String>),
196 MessageIdInput::new("Open", Some("menu.file")),
197 ]);
198
199 assert_eq!(
200 ids,
201 BTreeMap::from([
202 (String::from("Hello"), String::from("GF-NsyJx")),
203 (
204 String::from("Open\u{0004}menu.file"),
205 String::from("QWh_hL4_"),
206 ),
207 ])
208 );
209 }
210}