1use Result::{Err, Ok};
2use itertools::*;
3use lazy_static::*;
4use rand::RngExt;
5use rand::distr::Uniform;
6use rand::{CryptoRng, Rng};
7use sha2::digest::Update;
8use sha2::{Digest, Sha256};
9use std::collections::HashSet;
10use std::ops::DerefMut;
11use std::sync::{Mutex, MutexGuard};
12use unicode_segmentation::UnicodeSegmentation;
13use unidecode::unidecode_char;
14
15const FILTERED_CHARS: [char; 31] = [
16 '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '{', '}', '_', '<', '>', ':', ';', ',', '.',
17 '"', '\'', '`', '|', '+', '=', '/', '~', '[', ']', '\\', '-',
18];
19
20fn should_keep_char(c: &char) -> bool {
22 !FILTERED_CHARS.contains(c)
23}
24lazy_static! {
25 static ref ALL_U32: Uniform<u32> = Uniform::new_inclusive(0u32, u32::MAX).unwrap(); static ref ONE_TO_TWO_HUNDRED: Uniform<u8> = Uniform::new_inclusive(1, 200).unwrap(); }
30
31const MAX_STRING_LEN: usize = 200;
33
34pub fn generate_hashes_for_string_with_padding<R: Rng + CryptoRng>(
40 s: &str,
41 partition_id: Option<&str>,
42 salt: &[u8],
43 rng: &Mutex<R>,
44) -> Result<HashSet<u32>, String> {
45 let mut hashes = generate_hashes_for_string(s, partition_id, salt)?;
46
47 let prob = take_lock(rng).deref_mut().sample(*ONE_TO_TWO_HUNDRED);
48 let to_add: u8 = {
49 let r = &mut *take_lock(rng);
51 if prob <= 1 {
52 r.random_range(1..200)
53 } else if prob <= 5 {
54 r.random_range(1..30)
55 } else if prob <= 50 {
56 r.random_range(1..10)
57 } else {
58 r.random_range(1..5)
59 }
60 };
61 let pad_len = std::cmp::min(MAX_STRING_LEN - hashes.len(), to_add as usize);
64 hashes.extend(
65 take_lock(rng)
66 .deref_mut()
67 .sample_iter(*ALL_U32)
68 .take(pad_len),
69 );
70 Ok(hashes)
71}
72
73pub fn generate_hashes_for_string(
79 s: &str,
80 partition_id: Option<&str>,
81 salt: &[u8],
82) -> Result<HashSet<u32>, String> {
83 if s.len() > MAX_STRING_LEN {
84 Err(format!(
85 "The input string is too long. This function only supports strings that are no longer than {} chars.",
86 MAX_STRING_LEN
87 ))
88 } else {
89 let partial_sha256 = partition_id
91 .map(|k| k.as_bytes())
92 .iter()
93 .chain([salt].iter())
94 .fold(Sha256::new(), |hasher, k| hasher.chain(k));
95
96 let short_hash = |word: &[u8]| -> u32 {
97 let sha256_hash = partial_sha256.clone().chain(word);
98 as_u32_be(&sha256_hash.finalize().into())
99 };
100
101 let result: HashSet<_> = make_tri_grams(s)
102 .iter()
103 .map(|tri_gram| short_hash(tri_gram.as_bytes()))
104 .collect();
105 Ok(result)
106 }
107}
108
109pub fn transliterate_string(s: &str) -> String {
112 s.chars()
113 .filter(should_keep_char)
114 .map(char_to_trans)
115 .collect()
116}
117
118fn make_tri_grams(s: &str) -> HashSet<String> {
122 let converted_string = transliterate_string(s);
123 converted_string
124 .unicode_words()
125 .map(|short_word| {
126 let short_word_len = short_word.chars().count();
127 if short_word_len < 3 {
128 format!("{:-<3}", short_word)
130 } else {
131 short_word.to_string()
132 }
133 })
134 .flat_map(|word| word_to_trigrams(&word))
135 .collect()
136}
137
138fn word_to_trigrams(s: &str) -> HashSet<String> {
139 s.chars()
140 .tuple_windows()
141 .map(|(c1, c2, c3)| format!("{}{}{}", c1, c2, c3))
142 .collect()
143}
144
145fn char_to_trans(c: char) -> String {
147 let trans_string = unidecode_char(c);
148 if trans_string.is_empty() {
149 format!("{}", c)
150 } else {
151 trans_string.to_lowercase()
152 }
153}
154
155#[inline]
157fn as_u32_be(slice: &[u8; 32]) -> u32 {
158 ((slice[0] as u32) << 24)
159 + ((slice[1] as u32) << 16)
160 + ((slice[2] as u32) << 8)
161 + (slice[3] as u32)
162}
163
164fn take_lock<T>(m: &Mutex<T>) -> MutexGuard<'_, T> {
183 m.lock().unwrap_or_else(|e| {
184 let error = format!("Error when acquiring lock: {}", e);
185 panic!("{}", error);
186 })
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192 use rand::{distr::Alphanumeric, rngs::ThreadRng};
193
194 fn make_set(array: &[&str]) -> HashSet<String> {
195 array
196 .into_iter()
197 .map(|&s| From::from(s))
198 .collect::<HashSet<_>>()
199 }
200
201 #[test]
202 fn as_u32_be_known_result() {
203 let known_result = 16909060u32; let mut input = [0u8; 32];
205 input[0] = 1;
206 input[1] = 2;
207 input[2] = 3;
208 input[3] = 4;
209 let result = as_u32_be(&input);
210 assert_eq!(result, known_result);
211 }
212
213 #[test]
214 fn string_transliterated() {
215 assert_eq!(transliterate_string("Gumby, dammit!"), "gumby dammit");
216 assert_eq!(transliterate_string("北亰"), "bei jing ");
217 assert_eq!(transliterate_string("Æneid"), "aeneid");
218 }
219
220 #[test]
221 fn word_to_trigrams_known() {
222 let result = word_to_trigrams("five");
223 assert_eq!(result, make_set(&["fiv", "ive"]));
224 }
225
226 #[test]
227 fn make_tri_grams_works_multi_word() {
228 assert_eq!(
229 make_tri_grams("123 José Núñez 812-111-7654"),
230 make_set(&[
231 "123", "jos", "ose", "nun", "une", "nez", "812", "121", "211", "111", "117", "176",
232 "765", "654",
233 ])
234 );
235 }
236
237 #[test]
238 fn make_tri_grams_works_non_ascii() {
239 assert_eq!(
240 make_tri_grams("TİRYAKİ"),
241 make_set(&["tir", "iry", "rya", "yak", "aki"])
242 );
243 }
244
245 #[test]
246 fn make_tri_grams_eliminates_duplicates() {
247 assert_eq!(
248 make_tri_grams("TİRYAKİ TİRYAKİ"),
249 make_set(&["tir", "iry", "rya", "yak", "aki"])
250 );
251 }
252
253 #[test]
254 fn make_tri_grams_works_short_non_ascii() {
255 assert_eq!(make_tri_grams("Tİ"), make_set(&["ti-"]));
256 }
257
258 #[test]
259 fn make_tri_grams_works_multichar_translate() {
260 assert_eq!(
261 make_tri_grams("志 豪 İ"),
262 make_set(&["zhi", "hao", "i--"])
263 );
264 }
265
266 #[test]
267 fn make_tri_grams_works_arabic() {
268 assert_eq!(
269 make_tri_grams("شريط فو"),
270 make_set(&["shr", "hry", "ryt", "fw-"])
271 );
272 }
273 #[test]
274 fn make_tri_grams_works_short_multibyte() {
275 assert_eq!(
276 make_tri_grams("\u{102AE}\u{102AF}"),
277 make_set(&["\u{102AE}\u{102AF}-"])
278 );
279 }
280
281 #[test]
282 fn char_to_trans_latinizable() {
283 assert_eq!(char_to_trans('İ'), "i")
284 }
285
286 #[test]
287 fn char_to_trans_not_latinizable() {
288 let c = "\u{102AE}".chars().nth(0).unwrap();
289 assert_eq!(char_to_trans(c), "\u{102AE}")
290 }
291 #[test]
292 fn generate_hashes_for_string_compute_known_value() -> Result<(), String> {
293 let result = generate_hashes_for_string("123", Some("foo"), &[0u8; 1])?;
294 let expected_result = {
296 let mut hasher = Sha256::new();
297 sha2::Digest::update(&mut hasher, "foo".as_bytes());
298 sha2::Digest::update(&mut hasher, [0u8; 1]);
299 sha2::Digest::update(&mut hasher, "123".as_bytes());
300 as_u32_be(&(hasher.finalize().into()))
301 };
302 assert_eq!(result, [expected_result].iter().map(|x| *x).collect());
303 Ok(())
304 }
305
306 #[test]
307 fn generate_hashes_for_string_with_padding_adds_at_least_one() -> Result<(), String> {
308 let rng = Mutex::new(ThreadRng::default());
309 let result = generate_hashes_for_string_with_padding("123", Some("foo"), &[0u8; 1], &rng)?;
310 assert!(result.len() > 1);
311 Ok(())
312 }
313
314 #[test]
315 fn generate_hashes_for_string_with_padding_empty_string() -> Result<(), String> {
316 let rng = Mutex::new(ThreadRng::default());
317 let result = generate_hashes_for_string_with_padding("", Some("foo"), &[0u8; 1], &rng)?;
318 assert!(result.len() >= 1);
319 Ok(())
320 }
321
322 #[test]
323 fn generate_hashes_for_string_too_long_errors() -> Result<(), String> {
324 let rng = ThreadRng::default();
325 let input: Vec<u8> = rng.sample_iter(Alphanumeric).take(201).collect();
326 generate_hashes_for_string(std::str::from_utf8(&input).unwrap(), Some("foo"), &[0u8; 1])
327 .unwrap_err();
328 Ok(())
329 }
330}