├── .gitignore ├── .travis.yml ├── Cargo.toml ├── LICENSE.md ├── README.md ├── benches ├── compare_strings1.rs └── random_data1.rs ├── examples ├── example1.rs └── example2.rs ├── src ├── blockhash.rs ├── compare.rs ├── constants.rs ├── error.rs ├── hasher.rs ├── lib.rs └── roll.rs └── tests ├── compare_empty.rs ├── hash_file.rs ├── integration1.rs ├── more.rs ├── random_data1.rs └── test_data.bin /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | **/*.rs.bk 3 | *.swp 4 | .idea 5 | Cargo.lock 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - stable 4 | - beta 5 | - nightly 6 | matrix: 7 | allow_failures: 8 | - rust: nightly 9 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fuzzyhash" 3 | version = "0.2.2" 4 | authors = ["Russ Morris "] 5 | license = "MIT" 6 | description = "Pure Rust fuzzy hash implementation" 7 | homepage = "https://github.com/rustysec/fuzzyhash-rs" 8 | repository = "https://github.com/rustysec/fuzzyhash-rs" 9 | keywords = ["ssdeep", "fuzzy", "hash", "fuzzyhash"] 10 | edition = "2018" 11 | readme = "README.md" 12 | 13 | [lib] 14 | 15 | [dependencies] 16 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Russ 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | fuzzyhash-rs 2 | ============ 3 | [![Build Status](https://travis-ci.org/rustysec/fuzzyhash-rs.svg?branch=master)](https://travis-ci.org/rustysec/fuzzyhash-rs) 4 | [![Documentation](https://docs.rs/fuzzyhash/badge.svg)](https://docs.rs/fuzzyhash) 5 | 6 | Pure Rust fuzzy hash implementation. 7 | 8 | ### Usage 9 | 10 | **Hash A File** 11 | ```rust 12 | use fuzzyhash::FuzzyHash; 13 | 14 | let fuzzy = FuzzyHash::file("/path/to/file").unwrap(); 15 | 16 | // `FuzzyHash` implements `Display` so this works: 17 | 18 | println!("fuzzy hash of file: {}", fuzzy); 19 | ``` 20 | 21 | **Hash Data** 22 | ```rust 23 | use fuzzyhash::FuzzyHash; 24 | 25 | // Anything that implements `AsRef<[u8]>` can be immediately hashed 26 | 27 | let data = vec![1,2,3,4,5,6,7,8,9,10]; 28 | 29 | let fuzzy = FuzzyHash::new(data); 30 | ``` 31 | 32 | **Anything that implements `std::io::Read`** 33 | ```rust 34 | use fuzzyhash::FuzzyHash; 35 | use std::io::{Cursor, Read}; 36 | 37 | let mut cursor = Cursor::new(vec![1,2,3,4,5]); 38 | let fuzzy = FuzzyHash::read(&mut cursor); 39 | ``` 40 | 41 | **Build a fuzzy hash from blocks of data manually** 42 | ```rust 43 | use fuzzyhash::FuzzyHash; 44 | use std::io::Read; 45 | 46 | let mut file = std::fs::File::open("/path/to/my/file").unwrap(); 47 | let mut fuzzy_hash = FuzzyHash::default(); 48 | 49 | loop { 50 | let mut buffer = vec![0; 1024]; 51 | let count = file.read(&mut buffer).unwrap(); 52 | 53 | fuzzy_hash.update(buffer); 54 | 55 | if count < 1024 { 56 | break; 57 | } 58 | } 59 | 60 | fuzzy_hash.finalize(); 61 | 62 | println!("Fuzzy hash of data: {}", fuzzy_hash); 63 | ``` 64 | 65 | **FFI Compatibility** 66 | Two functions provide entry points for FFI usage of this library. 67 | 68 | ```c 69 | // hashing some data 70 | unsigned char *data = (unsigned char*)malloc(256); 71 | // fill this buffer... 72 | int fuzzy = fuzzyhash(data, 256); 73 | ``` 74 | 75 | ```c 76 | // compare two fuzzyhashes 77 | char *first = "96:U57GjXnLt9co6pZwvLhJluvrszNgMFwO6MFG8SvkpjTWf:Hj3BeoEcNJ0TspgIG8SvkpjTg"; 78 | char *second = "96:U57GjXnLt9co6pZwvLhJluvrs1eRTxYARdEallia:Hj3BeoEcNJ0TsI9xYeia3R"; 79 | int compared = fuzzyhash_compare(first, second); 80 | ``` 81 | 82 | ### Status 83 | Currently this library only supports the `None` mode of the ssdeep fuzzy hashing algorithm, 84 | `EliminateSequences` and `DoNotTruncate` will be implemented eventually. 85 | 86 | ### Run the example 87 | ```shell 88 | $ cargo run -q --example example1 /bin/bash 89 | 24576:z0wp2rLW2W2iYQK+q/VjsFEDe866QHX4kC:rp2rLW2W2iYJ+FEg6QHX 90 | ``` 91 | ### 0.2.0 API Changes 92 | The public API for the library has been largely re-imagined and is full of breaking changes. 93 | 94 | ### 0.1.3 Updates 95 | Fixed performance bottlenecks with cloning large buffers unnecessarily (~22% faster). 96 | 97 | 1000 iterations of large random buffer 98 | 99 | 0.1.2: 100 | ```sh 101 | $ time cargo bench 102 | Finished release [optimized] target(s) in 0.0 secs 103 | Running target/release/deps/fuzzyhash-a709fbd8d1125c4f 104 | 105 | running 0 tests 106 | 107 | test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out 108 | 109 | Running target/release/deps/random_data1-6d3edf5ebe8a1b5f 110 | 111 | running 1 test 112 | test hashing_bench ... bench: 111,144,101 ns/iter (+/- 2,712,598) 113 | 114 | test result: ok. 0 passed; 0 failed; 0 ignored; 1 measured; 0 filtered out 115 | 116 | 117 | real 0m33.786s 118 | user 0m33.757s 119 | sys 0m0.030s 120 | ``` 121 | 122 | vs 123 | 124 | 0.1.3: 125 | ```sh 126 | $ time cargo bench 127 | Finished release [optimized] target(s) in 0.0 secs 128 | Running target/release/deps/fuzzyhash-9ad0dfdb1b3b0386 129 | 130 | running 0 tests 131 | 132 | test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out 133 | 134 | Running target/release/deps/random_data1-3bec1fdd42a47a95 135 | 136 | running 1 test 137 | test hashing_bench ... bench: 87,273,582 ns/iter (+/- 2,535,966) 138 | 139 | test result: ok. 0 passed; 0 failed; 0 ignored; 1 measured; 0 filtered out 140 | 141 | 142 | real 0m26.525s 143 | user 0m26.515s 144 | sys 0m0.011s 145 | ``` 146 | 147 | ### Acknowledgements 148 | I previously ported the [algorithm to C++](https://github.com/rustysec/fuzzypp) and couldn't find 149 | a version in Rust, so here we are! I definitely need to mention 150 | [kolos450's work](https://github.com/kolos450/SsdeepNET) porting the algorithm to C#, which was 151 | a great jumping off point for both of my implementations. 152 | -------------------------------------------------------------------------------- /benches/compare_strings1.rs: -------------------------------------------------------------------------------- 1 | #![feature(test)] 2 | extern crate test; 3 | 4 | use fuzzyhash::FuzzyHash; 5 | use test::Bencher; 6 | 7 | #[bench] 8 | fn compare_bench(b: &mut Bencher) { 9 | let string1 = 10 | "96:U57GjXnLt9co6pZwvLhJluvrszNgMFwO6MFG8SvkpjTWf:Hj3BeoEcNJ0TspgIG8SvkpjTg".to_string(); 11 | let string2 = "96:U57GjXnLt9co6pZwvLhJluvrs1eRTxYARdEallia:Hj3BeoEcNJ0TsI9xYeia3R".to_string(); 12 | 13 | b.iter(|| { 14 | FuzzyHash::compare(&string1, &string2); 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /benches/random_data1.rs: -------------------------------------------------------------------------------- 1 | #![feature(test)] 2 | extern crate test; 3 | 4 | use fuzzyhash::FuzzyHash; 5 | use test::Bencher; 6 | 7 | #[bench] 8 | fn hashing_bench(b: &mut Bencher) { 9 | let data = r#"fcc55a724745b7efbf9a54908aec300d01b9830dff4ee435a667330a7fc56ca9 10 | 8913e11dc9cd172efc57c13083b24bd4eb44ef06a9c760431c0b45edf5ea76e3 11 | ee53bde1e736f9c11383433351b98314cbada4742b1b46103a838c7d31a79b7e 12 | 20b30357d3b308721d1c5e1159eb7fe0a79e11368e06e7a1d3a52f222516e520 13 | 07354874a2790f82c62aa61a21d6335bb1b683d75e3f41d7c209bf88d4a06a6b 14 | ceb774b75d0242badee796f39d2d6769ace96184bfc3a80081d94fc6eeda497d 15 | b5c10e8667b9f786f20de678ad72d76c3f59cc1f75fa74b7bf4fdbd56b457689 16 | 999484ab64fd5458774610dd9c76dfaa8020d14cf1cf415deceda52db4111ac7 17 | 11fdf7e67ee5f9fe0c4e17f8a5d2a4df32ee32d6efe6dbfd5d2429ce52d56a62 18 | 6d198ffd7dddf4ad741d0c8b8aadf77cd338bf12320d0ffac2c12fd344fb8107 19 | 7431a7baa0c02b4231b16adc3438aa5569c7e86eba6401eabf3c1f1cf37587e6 20 | 86e9d0a7b49cb0f84bc356339f8aed56a281ce30c27a612b5cc0b2440198f1ca 21 | 4f81ca391ffe0f9c80f04090cf30ba167c20adecca24c865a5120604d21e383a 22 | 21c4fe0df1886d1f648e54c773a6084b1332f8ad51edc0233796384c821e078f 23 | c0ff7e11eaca91b772fb68d670c2af6563e7740ad3eb900c18905468fe0b2635 24 | ef82dc8ef62b14f3ccfac603c016f2a4e2231af30fb8c2c188ac9209a836e171 25 | 93e0c375b23d1271ecec237445746169d845ffd84dc40b20b704d23016d37d7d 26 | 25b1b76c3572db64b9caae2772c183ccda0cc0b42986b7f100861c8dab62313b 27 | 5e18dfd466610c8d944534a2644df922de1283e945be46a09b40edf0330601cb 28 | 124e4d572a885d89630070f393e3fff83b82b9cae94cfbf6dec9b5df6b71bede 29 | a2229ee50dc32599c06e81b6a44f472c1dd331e39a3aba8a439c23e618307d85 30 | 98a88ccb694d9ddc82940f9273113b9154029ed0279ddb472ea5bce35790c4a4 31 | 7fde1d2b20119e4cc8eb028767592ada24bf311c535654d9273a6470479b5d51 32 | 62172924f022b3fa464ec5e20b057c49f217507ed538f6c4be4541b2a46cb00c 33 | 7b4c2c0634d2d40594d5874fb4c313d1efac95a0416eea5781c0d1a3474cee54 34 | 61fedb07a07602d62ae5f92ef5e1ed8f13c9ebccfb42f630293596937ceebf70 35 | f943607f87d75afe4ecb1a0cb9879a408e0aef3415dce30e7ff6efdc975cfcfe 36 | 72c6d526b115aea7c2492a7f74b26703b57f2ea9827058edcee0732f37be309a 37 | 9550e7a282097f8ca5e31fc18a0626a35afa4105d2f7a8e3eaa7185fa019b52c 38 | 8534851a79683d1f9abfa0442712ed3bc42762f743c2ec2513d352806902ca18 39 | fe4519b09a054635d86913aed6cdd601c56fee079c8b6fffb68434d46172453a 40 | 729eb2d6195a03c094373243ee4f60c920f1240dfaa6d2e84f79c84132f084c7 41 | fdf4ba40c75cb6f24d3a11edfac45867fa083ac8cf1049f4505f644ace90aec6 42 | d2b6a8c7847c8fde9a0c494717fc164673cdb430a73ace426cdba126fbd1053e 43 | d6db1ceff62a14aa518b88f4d84905bc17271359380bf514decec55a172d2b61 44 | 93e254aacdb5bea463c49bd95a1abe2e9b3905ee6086bdaa6f45ff03615345f7 45 | 701c2b3839ba83afeb6b5126b2a282fc12c9242bef26dc26aa0a60b6c9a53273 46 | f1d2bc3247fafce6b777de977824f1400a4cf70c4c31b6e944b4db7d0e146a53 47 | 8b9a26247f3f071649d4c78559299952b9ba7cf7bfd502dce197c3c16ac8b6e1 48 | 67b98e714de060cfe027cbfe7dc15932622abbe93499cfa0f6c03bffad050f5e 49 | 2b6c24781cf16e4794312c6b18b907164e218fd2db26cd0a8321243244e1f5ac 50 | 8dc80c7d4aab2e4b66cf2f4b52e11b527dd32ad837d353e81ce01969783e56ac 51 | ad27c131afb9d5778ce9170eb58f8576d8d1f9e90d0f00f1a3351b337bc58815 52 | ebef0d4c8f0d924b2a51d05d7c91f367d37b87973cb2de472bd0deb6c79d87d9 53 | 9a1c55d5af4d70b966fbc09ee531c3de6272af925958ec66dd385be208f5c270 54 | 5d1a62e55f9e9ec782d57e3635d4cc3b0a1c6b52e365d1eb485ccab246a3338a 55 | daa530699a8a4c7dc072e9716373bfc248995c9bb467159d269e289d62aa7404 56 | 64300155a799ffd9ee362b4124aa1331c08648000b201528ebea173adf979634 57 | 69eb4e216dfb546d0f6bf62bc1195e0783369c09305237558b5c3913f6f25fd6 58 | 3b8b07019cdbe8ee366d82dea26903ff1cbd28dd8a301bd6367681e26f81f1ca 59 | 13351a1e922feb0794ac634eede9a29384287d909db2d2095bb9d250a022d10e 60 | cdebaddc6d2904e9fa4e0148009c40fbda4f09c02f9d3f640bfa871b8231559a 61 | 74bb3ce0455f7b497343b8b336ab22af61bf42ece31c1e776aa813b11c667de2 62 | bf003b7031ba665aa6a6e9513cbf12fa2f12133811f18ab35525c1d678621dfa 63 | 9bd6bb3ecdf3891b27f997f6eb6914903a5e068f774545aeff034a562d0ed310 64 | 392d7e64e37c8bf7cb2c9282aedfc19318c5223a28c1e1014005302fac671e03 65 | cc4fd33987c76cf97c38ca710a7bfdf3bf4f74ab51f7dcd42a05dd1b26970ecd 66 | cec48c10375432ff24e96b89cfe4b7e5bd7ae7603a66f4a039a9d55165a8b413 67 | a80dc414f2bda7055abfc085eab6e0fa031d2d64821a00724dae9121957b8e80 68 | 7c4f6c1f8dcc6bc46665a7fee886ac9ad3bfa5d21d11580d75d3c01eadad073b 69 | a7e8d819321b981496ddc2a10142394750c1e3eb0ea33cf86b9ac4be7bf8cc82 70 | 3285c7baab410c41d48b7f1ecd479316cf0f08abd87e2caf6ff70a95e173fad0 71 | 387c58b21c0aba8a8d2ab3a9c641b94d6dc84e4714f51c07c9fce745c05202a5 72 | cebdc80fa6ddfea8b6b3e50cbfd73e00c0413820b007b243f2f4b1d12adefcb0 73 | 75170da30a9ead02a97c92f9c59bc9185c02a52f38671fa362e3025289ffa427 74 | b55510b5ce5b54ebb5834b23dc0548738e9beb7495f7fb596a68a7f27d69f879 75 | 847123e04aeaec29c18040c7ed3e77a7ab88c9f3c03020035726b9b861ab14d0 76 | 0c8641b0fa6069c07688f00370b9b5793225301da6de7daf1fb85ddb660059a9 77 | f65665489f8f0e9c551ff94aea8d19193ac4f8d654dcb9fb75497e42640a37e2 78 | a5500b26b3fd3f77c433c0d85978c667898832f12709d5d79b1d90f62510e109 79 | "#; 80 | 81 | b.iter(|| { 82 | let mut i = 0; 83 | while i < 1000 { 84 | let bytes = data.as_bytes().to_vec(); 85 | FuzzyHash::new(bytes); 86 | i += 1; 87 | } 88 | }); 89 | } 90 | 91 | #[bench] 92 | fn benchmark_hash_file(b: &mut test::Bencher) { 93 | b.iter(|| FuzzyHash::file("./tests/test_data.bin")) 94 | } 95 | 96 | #[bench] 97 | fn read_file_then_hash(b: &mut test::Bencher) { 98 | b.iter(|| { 99 | let data = std::fs::read("./tests/test_data.bin").unwrap(); 100 | FuzzyHash::new(data) 101 | }) 102 | } 103 | -------------------------------------------------------------------------------- /examples/example1.rs: -------------------------------------------------------------------------------- 1 | use fuzzyhash::FuzzyHash; 2 | use std::env; 3 | 4 | fn main() { 5 | if env::args().len() < 2 { 6 | println!("Please provide a file to hash!"); 7 | return; 8 | } 9 | 10 | for i in 1..env::args().len() { 11 | let path = env::args().nth(i).unwrap(); 12 | let data = std::fs::read(path).expect("Could not read file"); 13 | let fuzzy_hash = FuzzyHash::new(data); 14 | println!("{}", fuzzy_hash.to_string()); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/example2.rs: -------------------------------------------------------------------------------- 1 | use fuzzyhash::FuzzyHash; 2 | use std::env; 3 | 4 | pub fn main() { 5 | if env::args().len() != 3 { 6 | println!("Must provide two hashes!"); 7 | return; 8 | } 9 | println!("first: {}", env::args().nth(1).unwrap()); 10 | println!("second: {}", env::args().nth(2).unwrap()); 11 | 12 | let first = FuzzyHash::from(env::args().nth(1).unwrap()); 13 | let second = FuzzyHash::from(env::args().nth(2).unwrap()); 14 | 15 | println!( 16 | "Strings are {}% similar!", 17 | first.compare_to(&second).unwrap_or(0) 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/blockhash.rs: -------------------------------------------------------------------------------- 1 | use super::constants; 2 | use std::num::Wrapping; 3 | 4 | pub const HASH_PRIME: u32 = 0x0100_0193; 5 | pub const HASH_INIT: u32 = 0x2802_1967; 6 | 7 | #[derive(Clone)] 8 | pub struct Context { 9 | pub h: u32, 10 | pub half_h: u32, 11 | pub digest: Vec, 12 | pub half_digest: u8, 13 | pub d_len: u32, 14 | } 15 | 16 | impl Context { 17 | pub fn new() -> Context { 18 | Context { 19 | h: 0, 20 | half_h: 0, 21 | digest: vec![0; constants::SPAM_SUM_LENGTH as usize], 22 | half_digest: 0, 23 | d_len: 0, 24 | } 25 | } 26 | 27 | pub(crate) fn hash(&mut self, c: u8) { 28 | let h1 = self.h; 29 | self.h = self.hash_full(c, h1); 30 | let h2 = self.half_h; 31 | self.half_h = self.hash_full(c, h2); 32 | } 33 | 34 | pub(crate) fn hash_full(&mut self, c: u8, h: u32) -> u32 { 35 | let h_wrapped = Wrapping(h); 36 | let hp_wrapped = Wrapping(HASH_PRIME); 37 | let c_wrapped = Wrapping(c as u32); 38 | 39 | ((h_wrapped * hp_wrapped) ^ (c_wrapped)).0 40 | } 41 | 42 | pub(crate) fn reset(&mut self, init: bool) { 43 | if !init { 44 | self.d_len += 1; 45 | } 46 | 47 | self.digest[self.d_len as usize] = 0; 48 | self.h = HASH_INIT; 49 | if self.d_len < constants::SPAM_SUM_LENGTH / 2 { 50 | self.half_h = HASH_INIT; 51 | self.half_digest = 0; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/compare.rs: -------------------------------------------------------------------------------- 1 | use super::{constants, error::Error, roll::Roll, Result}; 2 | use std::cmp::{max, min}; 3 | 4 | const MAX_LENGTH: usize = 64; 5 | const INSERT_COST: u32 = 1; 6 | const REMOVE_COST: u32 = 1; 7 | const REPLACE_COST: u32 = 2; 8 | 9 | fn compute_distance(s1: &[u8], s2: &[u8]) -> u32 { 10 | let mut t1: Vec = vec![0; MAX_LENGTH + 1]; 11 | let mut t2: Vec = vec![0; MAX_LENGTH + 1]; 12 | let mut t3; 13 | 14 | for (i2, item) in t1.iter_mut().enumerate().take(s2.len() + 1) { 15 | *item = i2 as u32 * REMOVE_COST; 16 | } 17 | 18 | for (i1, _item) in s1.iter().enumerate() { 19 | t2[0] = (i1 as u32 + 1) * INSERT_COST; 20 | for i2 in 0..s2.len() { 21 | let cost_a = t1[i2 + 1] + INSERT_COST; 22 | let cost_d = t2[i2] + REMOVE_COST; 23 | let cost_r = t1[i2] + if s1[i1] == s2[i2] { 0 } else { REPLACE_COST }; 24 | t2[i2 + 1] = min(min(cost_a, cost_d), cost_r); 25 | } 26 | t3 = t1; 27 | t1 = t2; 28 | t2 = t3; 29 | } 30 | t1[s2.len()] 31 | } 32 | 33 | fn has_common_substring(first: &[u8], second: &[u8]) -> bool { 34 | let first_length = first.len(); 35 | let second_length = second.len(); 36 | let mut i: usize = 0; 37 | let mut hashes: Vec = vec![0; constants::SPAM_SUM_LENGTH as usize]; 38 | let mut state = Roll::new(); 39 | 40 | while i < first_length && first[i] != 0 { 41 | state.hash(first[i]); 42 | hashes[i] = state.sum(); 43 | i += 1; 44 | } 45 | 46 | let num_hashes = i; 47 | state = Roll::new(); 48 | 49 | i = 0; 50 | while i < second_length && second[i] != 0 { 51 | state.hash(second[i]); 52 | let h = state.sum(); 53 | 54 | if i < constants::ROLLING_WINDOW - 1 { 55 | i += 1; 56 | continue; 57 | } 58 | 59 | for (j, item) in hashes 60 | .iter() 61 | .enumerate() 62 | .take(num_hashes) 63 | .skip(constants::ROLLING_WINDOW - 1) 64 | { 65 | if *item != 0 && *item == h { 66 | let second_start_pos = i.wrapping_sub(constants::ROLLING_WINDOW).wrapping_add(1); 67 | let mut len = 0; 68 | while len + second_start_pos < second_length && second[len + second_start_pos] != 0 69 | { 70 | len += 1; 71 | } 72 | 73 | if len < constants::ROLLING_WINDOW { 74 | continue; 75 | } 76 | 77 | let mut matched = true; 78 | let first_start_pos = j.wrapping_sub(constants::ROLLING_WINDOW).wrapping_add(1); 79 | for pos in 0..constants::ROLLING_WINDOW { 80 | let first_char = first[first_start_pos + pos]; 81 | let second_char = second[second_start_pos + pos]; 82 | 83 | if first_char != second_char { 84 | matched = false; 85 | break; 86 | } 87 | 88 | if first_char == 0 { 89 | break; 90 | } 91 | } 92 | if matched { 93 | return true; 94 | } 95 | } 96 | } 97 | 98 | i += 1; 99 | } 100 | false 101 | } 102 | 103 | fn eliminate_sequences(input: Vec) -> Vec { 104 | let mut result: Vec = vec![0; input.len()]; 105 | let mut i = 0; 106 | 107 | while i < 3 && i < input.len() { 108 | result[i] = input[i]; 109 | i += 1; 110 | } 111 | 112 | if input.len() < 3 { 113 | return result; 114 | } 115 | 116 | i = 3; 117 | let mut j = 3; 118 | 119 | while i < input.len() { 120 | let current = input[j]; 121 | if current != input[i - 1] || current != input[i - 2] || current != input[i - 3] { 122 | result[j] = input[i]; 123 | j += 1; 124 | } 125 | i += 1; 126 | } 127 | 128 | unsafe { 129 | result.set_len(j); 130 | } 131 | result 132 | } 133 | 134 | fn score_strings(first: Vec, second: Vec, block_size: u32) -> Result { 135 | if first.len() > constants::SPAM_SUM_LENGTH as usize 136 | || second.len() > constants::SPAM_SUM_LENGTH as usize 137 | { 138 | return Ok(0); 139 | } 140 | 141 | if !has_common_substring(&first, &second) { 142 | return Err(Error::NoCommonSubstrings); 143 | } 144 | 145 | let mut score = compute_distance(&first, &second); 146 | score = (score * constants::SPAM_SUM_LENGTH) / ((first.len() + second.len()) as u32); 147 | score = (100 * score) / 64; 148 | if score >= 100 { 149 | return Ok(0); 150 | } 151 | 152 | score = 100 - score; 153 | 154 | let match_size = 155 | block_size / constants::MIN_BLOCK_SIZE * (min(first.len(), second.len()) as u32); 156 | 157 | Ok(if score > match_size { 158 | match_size 159 | } else { 160 | score 161 | }) 162 | } 163 | 164 | pub(crate) fn compare, T: AsRef>(first: S, second: T) -> Result { 165 | let first_parts: Vec<&str> = first.as_ref().split(':').collect(); 166 | let second_parts: Vec<&str> = second.as_ref().split(':').collect(); 167 | 168 | if first_parts.len() != 3 && second_parts.len() != 3 { 169 | return Err(Error::MalformedInput); 170 | } 171 | 172 | let first_block_size = match first_parts[0].parse::() { 173 | Ok(s) => s, 174 | Err(_) => { 175 | return Err(Error::BlockSizeParse); 176 | } 177 | }; 178 | let second_block_size = match second_parts[0].parse::() { 179 | Ok(s) => s, 180 | Err(_) => { 181 | return Err(Error::BlockSizeParse); 182 | } 183 | }; 184 | 185 | if first_block_size != second_block_size 186 | && first_block_size != second_block_size * 2 187 | && second_block_size != first_block_size * 2 188 | { 189 | return Err(Error::IncompatibleBlockSizes); 190 | } 191 | 192 | let first_block1 = eliminate_sequences(first_parts[1].as_bytes().to_vec()); 193 | let first_block2 = eliminate_sequences(first_parts[2].as_bytes().to_vec()); 194 | 195 | let second_block1 = eliminate_sequences(second_parts[1].as_bytes().to_vec()); 196 | let second_block2 = eliminate_sequences(second_parts[2].as_bytes().to_vec()); 197 | 198 | if first_block_size == second_block_size && first_block1.len() == second_block1.len() { 199 | let mut matched = true; 200 | for i in 0..first_block1.len() { 201 | if first_block1[i] != second_block1[i] { 202 | matched = false; 203 | break; 204 | } 205 | } 206 | if matched { 207 | return Ok(100); 208 | } 209 | } 210 | 211 | Ok(if first_block_size == second_block_size { 212 | let score1 = score_strings(first_block1, second_block1, first_block_size).unwrap_or(0); 213 | let score2 = score_strings(first_block2, second_block2, first_block_size * 2).unwrap_or(0); 214 | max(score1, score2) 215 | } else if first_block_size == second_block_size * 2 { 216 | score_strings(first_block1, second_block2, first_block_size)? 217 | } else { 218 | score_strings(first_block2, second_block1, second_block_size)? 219 | }) 220 | } 221 | -------------------------------------------------------------------------------- /src/constants.rs: -------------------------------------------------------------------------------- 1 | pub(crate) const ROLLING_WINDOW: usize = 7; 2 | pub(crate) const MIN_BLOCK_SIZE: u32 = 3; 3 | pub(crate) const NUM_BLOCKHASHES: u32 = 31; 4 | pub(crate) const SPAM_SUM_LENGTH: u32 = 64; 5 | pub(crate) const MAX_RESULT_LENGTH: u32 = 2 * SPAM_SUM_LENGTH + 20; 6 | pub(crate) const BASE64_CHARS: &str = 7 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; 8 | 9 | /// Hashing modes 10 | pub enum Modes { 11 | /// No special behavior 12 | None = 0, 13 | /// Eliminate sequences of more than three identical characters 14 | EliminateSequences = 1, 15 | /// Do not to truncate the second part to SPAMSUM_LENGTH/2 characters 16 | DoNotTruncate = 2, 17 | } 18 | 19 | pub(crate) fn get_base64_char(pos: usize) -> u8 { 20 | BASE64_CHARS.bytes().nth(pos).unwrap_or(0) 21 | } 22 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | #![allow(missing_docs)] 2 | 3 | use std::fmt; 4 | 5 | /// Errors pertaining to processing fuzzy hashes 6 | #[derive(Debug)] 7 | pub enum Error { 8 | /// Fuzzy hashes must contain at least one common substring for comparison 9 | NoCommonSubstrings, 10 | 11 | /// At least one input string is in the wrong format 12 | MalformedInput, 13 | 14 | /// Cannot parse the block size of the string 15 | BlockSizeParse, 16 | 17 | /// Two strings have incompatible block sizes. Sizes must be equal, a multiple or a multiple of 18 | /// 2 from each other. 19 | IncompatibleBlockSizes, 20 | 21 | /// String contains too many blocks for comparison 22 | TooManyBlocks, 23 | 24 | /// Unable to produce a valid hash string 25 | InvalidHashString(std::string::FromUtf8Error), 26 | } 27 | 28 | impl std::error::Error for Error { 29 | fn description(&self) -> &str { 30 | match self { 31 | Error::NoCommonSubstrings => "No common substrings were found between two fuzzy hashes", 32 | Error::MalformedInput => "Strings are not in proper fuzzy hash format", 33 | Error::BlockSizeParse => "Could not parse block sizes in string(s)", 34 | Error::IncompatibleBlockSizes => "Fuzzy hashes have incompatible block sizes", 35 | Error::TooManyBlocks => "Total number of blocks exceeds limit", 36 | Error::InvalidHashString(_) => "Unable to produce a valid hash string", 37 | } 38 | } 39 | 40 | fn cause(&self) -> Option<&dyn std::error::Error> { 41 | match self { 42 | Error::InvalidHashString(e) => Some(e), 43 | _ => None, 44 | } 45 | } 46 | } 47 | 48 | impl fmt::Display for Error { 49 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 50 | write!(f, "Error processing fuzzy hash(es)") 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/hasher.rs: -------------------------------------------------------------------------------- 1 | use super::{blockhash, constants, error::Error, roll, Result}; 2 | 3 | /// The fuzzy hasher 4 | pub struct Hasher { 5 | bh_start: u32, 6 | bh_end: u32, 7 | bh: Vec, 8 | total_size: u32, 9 | roll: roll::Roll, 10 | } 11 | 12 | impl Default for Hasher { 13 | fn default() -> Self { 14 | Self::new() 15 | } 16 | } 17 | 18 | impl Hasher { 19 | /// Build a new fuzzy hasher 20 | pub fn new() -> Hasher { 21 | let mut h = Hasher { 22 | bh_start: 0, 23 | bh_end: 1, 24 | bh: vec![blockhash::Context::new(); constants::NUM_BLOCKHASHES as usize], 25 | total_size: 0, 26 | roll: roll::Roll::new(), 27 | }; 28 | h.bh[0].reset(true); 29 | h 30 | } 31 | 32 | fn memcpy_eliminate_sequences() -> usize { 33 | // TODO 34 | 0 35 | } 36 | 37 | fn try_fork_blockhash(&mut self) { 38 | if self.bh_end < constants::NUM_BLOCKHASHES { 39 | self.bh[self.bh_end as usize].h = self.bh[(self.bh_end - 1) as usize].h; 40 | self.bh[self.bh_end as usize].half_h = self.bh[(self.bh_end - 1) as usize].half_h; 41 | 42 | self.bh[self.bh_end as usize].digest[0] = 0; 43 | self.bh[self.bh_end as usize].half_digest = 0; 44 | self.bh[self.bh_end as usize].d_len = 0; 45 | self.bh_end += 1; 46 | } else if self.bh_end == constants::NUM_BLOCKHASHES - 1 { 47 | self.bh[self.bh_end as usize].h = self.bh[(self.bh_end - 1) as usize].h; 48 | } 49 | } 50 | 51 | fn try_reduce_blockhash(&mut self) { 52 | if self.bh_end - self.bh_start < 2 { 53 | return; 54 | } 55 | 56 | if (constants::MIN_BLOCK_SIZE << self.bh_start) * constants::SPAM_SUM_LENGTH 57 | >= self.total_size 58 | { 59 | return; 60 | } 61 | 62 | if self.bh[(self.bh_start + 1) as usize].d_len < constants::SPAM_SUM_LENGTH / 2 { 63 | return; 64 | } 65 | 66 | self.bh_start += 1; 67 | } 68 | 69 | fn engine_step(&mut self, c: u8) { 70 | self.roll.hash(c); 71 | let h = self.roll.sum(); 72 | for i in self.bh_start..self.bh_end { 73 | self.bh[i as usize].hash(c); 74 | } 75 | 76 | let mut j = self.bh_start; 77 | while j < self.bh_end { 78 | if h % (constants::MIN_BLOCK_SIZE << j) != (constants::MIN_BLOCK_SIZE << j) - 1 { 79 | break; 80 | } 81 | 82 | if self.bh[j as usize].d_len == 0 { 83 | self.try_fork_blockhash(); 84 | } 85 | let pos = self.bh[j as usize].d_len as usize; 86 | self.bh[j as usize].digest[pos] = 87 | constants::get_base64_char((self.bh[j as usize].h % 64) as usize); 88 | self.bh[j as usize].half_digest = 89 | constants::get_base64_char((self.bh[j as usize].half_h % 64) as usize); 90 | 91 | if self.bh[j as usize].d_len < constants::SPAM_SUM_LENGTH - 1 { 92 | self.bh[j as usize].reset(false); 93 | } else { 94 | self.try_reduce_blockhash(); 95 | } 96 | j += 1; 97 | } 98 | } 99 | 100 | /// Add data to the `Hasher`. 101 | pub fn update(&mut self, buffer: &[u8], len: usize) { 102 | self.total_size += len as u32; 103 | for item in buffer.iter().take(len) { 104 | self.engine_step(*item); 105 | } 106 | } 107 | 108 | /// Compute the hash of the data and return a `String` representation 109 | pub fn digest(&mut self, flags: constants::Modes) -> Result { 110 | let mut result = vec![0; constants::MAX_RESULT_LENGTH as usize]; 111 | let mut pos = 0; 112 | let mut bi = self.bh_start; 113 | let mut h = self.roll.sum(); 114 | 115 | while (constants::MIN_BLOCK_SIZE << bi) * constants::SPAM_SUM_LENGTH < self.total_size { 116 | bi += 1; 117 | if bi >= constants::NUM_BLOCKHASHES { 118 | return Err(Error::TooManyBlocks); 119 | } 120 | } 121 | 122 | while bi >= self.bh_end { 123 | bi -= 1; 124 | } 125 | 126 | while bi > self.bh_start && self.bh[bi as usize].d_len < constants::SPAM_SUM_LENGTH / 2 { 127 | bi -= 1; 128 | } 129 | 130 | let actual_blocksize = constants::MIN_BLOCK_SIZE << bi; 131 | let blocksize_string = actual_blocksize.to_string(); 132 | let blocksize_chars = blocksize_string.into_bytes(); 133 | let mut i = blocksize_chars.len(); 134 | 135 | result[pos..(i + pos)].clone_from_slice(&blocksize_chars[..i]); 136 | result[i] = b':'; 137 | i += 1; 138 | 139 | pos += i; 140 | i = self.bh[bi as usize].d_len as usize; 141 | 142 | match flags { 143 | constants::Modes::EliminateSequences => { 144 | i = Hasher::memcpy_eliminate_sequences(); 145 | } 146 | _ => { 147 | result[pos..(i + pos)].clone_from_slice(&self.bh[bi as usize].digest[..i]); 148 | } 149 | } 150 | 151 | pos += i; 152 | if h != 0 { 153 | let base64val = constants::get_base64_char((self.bh[bi as usize].h % 64) as usize); 154 | result[pos] = base64val; 155 | if match flags { 156 | constants::Modes::EliminateSequences => false, 157 | _ => true, 158 | } || i < 3 159 | || base64val != result[pos - 1] 160 | || base64val != result[pos - 2] 161 | || base64val != result[pos - 3] 162 | { 163 | pos += 1; 164 | } 165 | } else if self.bh[bi as usize].digest[i as usize] != 0 { 166 | let base64val = self.bh[bi as usize].digest[i as usize]; 167 | result[pos as usize] = base64val; 168 | if match flags { 169 | constants::Modes::EliminateSequences => false, 170 | _ => true, 171 | } || i < 3 172 | || base64val != result[pos - 1] 173 | || base64val != result[pos - 2] 174 | || base64val != result[pos - 3] 175 | { 176 | pos += 1; 177 | } 178 | } 179 | result[pos] = b':'; 180 | pos += 1; 181 | 182 | if bi < self.bh_end - 1 { 183 | bi += 1; 184 | i = self.bh[bi as usize].d_len as usize; 185 | 186 | if match flags { 187 | constants::Modes::DoNotTruncate => false, 188 | _ => true, 189 | } && i > ((constants::SPAM_SUM_LENGTH / 2) - 1) as usize 190 | { 191 | i = ((constants::SPAM_SUM_LENGTH / 2) - 1) as usize; 192 | } 193 | 194 | match flags { 195 | constants::Modes::EliminateSequences => { 196 | i = Hasher::memcpy_eliminate_sequences(); 197 | } 198 | _ => { 199 | result[pos..(i + pos)].clone_from_slice(&self.bh[bi as usize].digest[..i]); 200 | } 201 | } 202 | pos += i; 203 | 204 | if h != 0 { 205 | h = match flags { 206 | constants::Modes::DoNotTruncate => self.bh[bi as usize].h, 207 | _ => self.bh[bi as usize].half_h, 208 | }; 209 | let base64val = constants::get_base64_char((h % 64) as usize); 210 | result[pos] = base64val; 211 | if match flags { 212 | constants::Modes::EliminateSequences => false, 213 | _ => true, 214 | } || i < 3 215 | || base64val != result[pos - 1] 216 | || base64val != result[pos - 2] 217 | || base64val != result[pos - 3] 218 | { 219 | pos += 1; 220 | } 221 | } else { 222 | i = match flags { 223 | constants::Modes::DoNotTruncate => { 224 | self.bh[bi as usize].digest[self.bh[bi as usize].d_len as usize] 225 | } 226 | _ => self.bh[bi as usize].half_digest, 227 | } as usize; 228 | 229 | if i != 0 { 230 | result[pos] = i as u8; 231 | if match flags { 232 | constants::Modes::EliminateSequences => false, 233 | _ => true, 234 | } || i < 3 235 | || i != result[pos - 1] as usize 236 | || i != result[pos - 2] as usize 237 | || i != result[pos - 3] as usize 238 | { 239 | pos += 1; 240 | } 241 | } 242 | } 243 | } else if h != 0 { 244 | result[pos] = constants::get_base64_char((self.bh[bi as usize].h % 64) as usize); 245 | } 246 | unsafe { 247 | result.set_len(pos); 248 | } 249 | 250 | String::from_utf8(result).map_err(Error::InvalidHashString) 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! An implementation of fuzzyhash/ssdeep hash algorithm. The 2 | //! original [CTPH](https://www.sciencedirect.com/science/article/pii/S1742287606000764?via%3Dihub) 3 | //! paper describes how this fuzzy hash is computed. 4 | //! 5 | //! # Examples 6 | //! 7 | //! **Build a fuzzy hash from blocks of data, like a stream**: 8 | //! 9 | //! ```no_run 10 | //! use fuzzyhash::FuzzyHash; 11 | //! use std::io::Read; 12 | //! 13 | //! let mut file = std::fs::File::open("/path/to/my/file").unwrap(); 14 | //! let mut fuzzy_hash = FuzzyHash::default(); 15 | //! 16 | //! loop { 17 | //! let mut buffer = vec![0; 1024]; 18 | //! let count = file.read(&mut buffer).unwrap(); 19 | //! 20 | //! fuzzy_hash.update(buffer); 21 | //! 22 | //! if count < 1024 { 23 | //! break; 24 | //! } 25 | //! } 26 | //! 27 | //! fuzzy_hash.finalize(); 28 | //! 29 | //! println!("Fuzzy hash of data: {}", fuzzy_hash); 30 | //! ``` 31 | //! 32 | //! **Hash some data**: 33 | //! ```no_run 34 | //! use fuzzyhash::FuzzyHash; 35 | //! 36 | //! let mut buffer = Vec::new(); 37 | //! 38 | //! buffer.push(0xde); 39 | //! buffer.push(0xad); 40 | //! buffer.push(0xbe); 41 | //! buffer.push(0xef); 42 | //! // ... 43 | //! 44 | //! println!("Fuzzy hash of data: {}", FuzzyHash::new(buffer)); 45 | //! ``` 46 | //! 47 | 48 | #![warn(missing_docs)] 49 | 50 | mod blockhash; 51 | mod compare; 52 | mod constants; 53 | pub mod error; 54 | mod hasher; 55 | mod roll; 56 | 57 | pub use constants::Modes; 58 | use hasher::Hasher; 59 | use std::{ 60 | ffi::{CStr, CString}, 61 | fmt, 62 | os::raw::c_char, 63 | path::Path, 64 | }; 65 | 66 | /// Result of fuzzy hash operations 67 | pub type Result = std::result::Result; 68 | 69 | /// Hasher for fuzzy algorithm 70 | pub struct FuzzyHash { 71 | hasher: Hasher, 72 | hash: Option, 73 | } 74 | 75 | impl Default for FuzzyHash { 76 | fn default() -> Self { 77 | Self { 78 | hasher: Hasher::new(), 79 | hash: None, 80 | } 81 | } 82 | } 83 | 84 | impl FuzzyHash { 85 | /// Construct a new FuzzyHash from source data 86 | /// 87 | /// # Example 88 | /// 89 | /// ```no_run 90 | /// use std::fs::read; 91 | /// use std::io::Read; 92 | /// use fuzzyhash::FuzzyHash; 93 | /// 94 | /// let mut data = read("/usr/bin/bash").unwrap(); 95 | /// let mut fuzzy_hash = FuzzyHash::new(data); 96 | /// ``` 97 | /// 98 | pub fn new>(input: S) -> Self { 99 | let input = input.as_ref(); 100 | let mut this = Self::default(); 101 | this.hasher.update(input, input.len()); 102 | this.finalize(); 103 | this 104 | } 105 | 106 | /// Hash a file pointed to by `path`. 107 | /// 108 | /// # Example 109 | /// ```no_run 110 | /// use fuzzyhash::{FuzzyHash}; 111 | /// let hash = FuzzyHash::file("/home/me/a_large_file.bin").unwrap(); 112 | /// ``` 113 | /// 114 | pub fn file>(path: P) -> std::result::Result { 115 | let mut file = std::fs::File::open(path.as_ref())?; 116 | FuzzyHash::read(&mut file) 117 | } 118 | 119 | /// Hash target implementing `std::io::Read` 120 | /// 121 | /// # Example 122 | /// ``` 123 | /// use fuzzyhash::FuzzyHash; 124 | /// use std::io::{Cursor, Read}; 125 | /// 126 | /// let mut cursor = Cursor::new(vec![1,2,3,4,5,6,7,8,9,10]); 127 | /// let fuzzy = FuzzyHash::read(&mut cursor); 128 | /// ``` 129 | pub fn read(reader: &mut R) -> std::result::Result { 130 | let mut hasher = Hasher::new(); 131 | loop { 132 | let mut buffer = [0; 1024]; 133 | let len = reader.read(&mut buffer)?; 134 | hasher.update(&buffer, len); 135 | 136 | if len < 1024 { 137 | break; 138 | } 139 | } 140 | 141 | let mut this = Self { hasher, hash: None }; 142 | this.finalize(); 143 | Ok(this) 144 | } 145 | 146 | /// Add chunk to the data source 147 | pub fn update>(&mut self, input: S) { 148 | let input = input.as_ref(); 149 | self.hasher.update(input, input.len()); 150 | } 151 | 152 | /// Called to finalize the hashing and generate a string value 153 | pub fn finalize(&mut self) { 154 | if self.hash.is_none() { 155 | self.hash = self.hasher.digest(constants::Modes::None).ok(); 156 | } 157 | } 158 | 159 | /// Compare two fuzzy hashes 160 | /// 161 | /// # Arguments 162 | /// * `first` - first fuzzy hash to compare 163 | /// * `second` - second fuzzy hash to compare 164 | /// 165 | /// # Example 166 | /// ``` 167 | /// use fuzzyhash::FuzzyHash; 168 | /// assert_eq!(FuzzyHash::compare( 169 | /// "96:U57GjXnLt9co6pZwvLhJluvrszNgMFwO6MFG8SvkpjTWf:Hj3BeoEcNJ0TspgIG8SvkpjTg", 170 | /// "96:U57GjXnLt9co6pZwvLhJluvrs1eRTxYARdEallia:Hj3BeoEcNJ0TsI9xYeia3R").unwrap(), 171 | /// 63); 172 | /// ``` 173 | pub fn compare, T: AsRef>(first: S, second: T) -> Result { 174 | compare::compare(first, second) 175 | } 176 | 177 | /// Compare this fuzzy hash against another 178 | /// 179 | /// # Arguments 180 | /// * `other` - compare this fuzzy hash to `other` 181 | /// 182 | /// # Example 183 | /// ``` 184 | /// use fuzzyhash::FuzzyHash; 185 | /// let mut fuzzy_hash = FuzzyHash::new("some data to hash for the purposes of running a test"); 186 | /// assert_eq!(fuzzy_hash.compare_to( 187 | /// &"3:HEREar5MFUul0U0KMP:knl8lkKMP".into()), 188 | /// Some(18)); 189 | /// ``` 190 | pub fn compare_to(&self, other: &FuzzyHash) -> Option { 191 | self.hash 192 | .as_ref() 193 | .and_then(|ref hash| FuzzyHash::compare(hash, &other.to_string()).ok()) 194 | } 195 | } 196 | 197 | impl fmt::Display for FuzzyHash { 198 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 199 | write!(f, "{}", self.hash.as_ref().unwrap_or(&String::new())) 200 | } 201 | } 202 | 203 | impl From<&str> for FuzzyHash { 204 | fn from(s: &str) -> Self { 205 | Self { 206 | hasher: Hasher::new(), 207 | hash: Some(s.to_string()), 208 | } 209 | } 210 | } 211 | 212 | impl From for FuzzyHash { 213 | fn from(s: String) -> Self { 214 | Self { 215 | hasher: Hasher::new(), 216 | hash: Some(s), 217 | } 218 | } 219 | } 220 | 221 | /// Returns the fuzzy hash of arbitrary data. This method provides better FFI compatibility. 222 | /// 223 | /// # Arguments 224 | /// * `buf` - a pointer to the array containing the data to hash 225 | /// * `length` - length of buf 226 | /// 227 | /// # Safety 228 | /// 229 | /// This is function is `unsafe` as it is intended to read a string from FFI 230 | /// 231 | /// # Example 232 | /// ``` 233 | /// use fuzzyhash::{fuzzyhash}; 234 | /// use std::ffi::CString; 235 | /// 236 | /// let data = "this is our test data!".to_string(); 237 | /// let hash = unsafe { CString::from_raw(fuzzyhash(data.as_bytes().as_ptr(), data.len())) }; 238 | /// let hash = hash.into_string().unwrap(); 239 | /// println!("Fuzzy Hash: {}", hash); 240 | /// assert_eq!(hash, "3:YKKGhR0tn:YRGRmn"); 241 | /// 242 | /// ``` 243 | #[no_mangle] 244 | pub unsafe extern "C" fn fuzzyhash(buf: *const u8, length: usize) -> *mut c_char { 245 | let data = std::slice::from_raw_parts(buf, length); 246 | let mut fuzzy_hash = FuzzyHash::new(data); 247 | fuzzy_hash.finalize(); 248 | 249 | let s = CString::new(fuzzy_hash.to_string()).unwrap(); 250 | 251 | s.into_raw() 252 | } 253 | 254 | /// FFI Compatible fuzzy hash comparisons. 255 | /// 256 | /// # Arguments 257 | /// * `first` - a C style fuzzy hash string 258 | /// * `second` - a C style fuzzy hash string 259 | /// 260 | /// # Safety 261 | /// 262 | /// This is function is `unsafe` as it is intended to read strings from FFI 263 | /// 264 | /// # Example 265 | /// ``` 266 | /// use fuzzyhash::{fuzzyhash_compare}; 267 | /// use std::ffi::CString; 268 | /// 269 | /// let first = CString::new("this is our test data for a fuzzy hash comparison!").unwrap(); 270 | /// let second = CString::new("this is my test data for a fuzzy hash comparison!").unwrap(); 271 | /// let compared = unsafe { fuzzyhash_compare(first.as_ptr(), second.as_ptr()) }; 272 | /// println!("Fuzzy Hash: {}", compared); 273 | /// assert_eq!(compared, 17); 274 | /// ``` 275 | #[no_mangle] 276 | pub unsafe extern "C" fn fuzzyhash_compare(first: *const c_char, second: *const c_char) -> u32 { 277 | let f = FuzzyHash::new(CStr::from_ptr(first).to_string_lossy().into_owned()); 278 | let s = FuzzyHash::new(CStr::from_ptr(second).to_string_lossy().into_owned()); 279 | 280 | f.compare_to(&s).unwrap_or(0) 281 | } 282 | -------------------------------------------------------------------------------- /src/roll.rs: -------------------------------------------------------------------------------- 1 | use super::constants; 2 | 3 | pub struct Roll { 4 | pub h1: u32, 5 | pub h2: u32, 6 | pub h3: u32, 7 | pub n: u32, 8 | pub window: Vec, 9 | } 10 | 11 | impl Roll { 12 | pub fn sum(&mut self) -> u32 { 13 | self.h3.wrapping_add(self.h1.wrapping_add(self.h2)) 14 | } 15 | 16 | pub fn hash(&mut self, c: u8) { 17 | self.h2 -= self.h1; 18 | self.h2 += constants::ROLLING_WINDOW as u32 * c as u32; 19 | 20 | self.h1 += c as u32; 21 | self.h1 -= self.window[(self.n as usize % constants::ROLLING_WINDOW)] as u32; 22 | 23 | self.window[(self.n as usize % constants::ROLLING_WINDOW)] = c; 24 | self.n += 1; 25 | 26 | self.h3 <<= 5; 27 | self.h3 ^= c as u32; 28 | } 29 | 30 | pub fn new() -> Roll { 31 | Roll { 32 | h1: 0, 33 | h2: 0, 34 | h3: 0, 35 | n: 0, 36 | window: vec![0; constants::ROLLING_WINDOW], 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/compare_empty.rs: -------------------------------------------------------------------------------- 1 | extern crate fuzzyhash; 2 | 3 | use fuzzyhash::FuzzyHash; 4 | 5 | #[test] 6 | fn compare_empty() { 7 | assert!(FuzzyHash::compare("", "").is_err()); 8 | } 9 | -------------------------------------------------------------------------------- /tests/hash_file.rs: -------------------------------------------------------------------------------- 1 | use fuzzyhash::FuzzyHash; 2 | 3 | #[test] 4 | fn hash_test_data() { 5 | let fuzzy_hash = FuzzyHash::file("./tests/test_data.bin").unwrap(); 6 | 7 | assert_eq!( 8 | fuzzy_hash.to_string(), 9 | "192:tEIFoBn+SbDjIZ6MUpH6rDjHPanaVGLGOvkdGep:tEIeBrbDjRAvDVEGOMGQ".to_owned() 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /tests/integration1.rs: -------------------------------------------------------------------------------- 1 | extern crate fuzzyhash; 2 | 3 | use fuzzyhash::FuzzyHash; 4 | 5 | #[test] 6 | fn integration1() { 7 | let first = r#"12d9b8707784bdc6460577d2d79802a937bc7c5e 8 | 653d4332a6146ded98db2644672fc7da10ad35c6 9 | b286e5a9367b972a5f40b4722d01388dbe1c235d 10 | 6311a2f15737d57cf59df1f67d0636ff6ea8143f 11 | 5f255e9bfad4df27f6283e5ff70f640c9e8f0ce1 12 | 1e84a19babc3ee3471a6f14ba7ab45db00966703 13 | 2707430b814386e8a50e27be70a8e38b1ce93e8c 14 | c41b693a20a7a5dfaf2e068306de74b3feb11f1d 15 | 624f2a7cea08daf255c57bb0a698330c4f06b711 16 | db75dcdf1905616b219f8120ffb088aebd20ad07 17 | 7c84871b1f6c355203f18e03dbf8e22cd7d87830 18 | 01198bb7e184632895b03501872ee4c257b0cb1f 19 | 743393e944a677000db0a988562fd98b0f71e60c 20 | 7287b1483fd51ef51fb3747d954ae213ac685898 21 | 14c26c67c91243587564d1a7e71d2c5b7853250f 22 | 47a200b54965019a18ba93e88980c0857dd34ca3 23 | 9e6fb1434f2ac52c78860daa1d13809e987fd1bd 24 | d01ef35c73c9aa52a84dd59a0e31672aeb5a47df 25 | e56d3ea7520aa66df593a2f6b394251641e6bef6 26 | 4ef268b789a1eb042dfa28683394b976cbf5d113 27 | 161dc6b59dd6f0f4350d9288105995037745de25 28 | ab95996d25040f367785b23e11cb8cc423b5d992 29 | 08c0393c74052127e8baafb6f3bb9a0a1137bf50 30 | b2db357d8629e6408b3bc1ace7ac1337044cb146 31 | 53c4bd8d0d0efa225c47f0af505607e5a36d726b 32 | d99e51eeb0061741e82228c99e1c2f426525f700 33 | 65976cf155ce659de2f775060e17624fd0154bb8 34 | e02fdc87856f8de04caddd8e6931d7c80cb64874 35 | 8b7f4e996462136b91b172a592cfef05d4063380 36 | 1a30feb7b9458b909a58fb10b80592fb1126d04a 37 | 6690195b4fe4643505c369146abce70fb1628059 38 | 9cfc9de3128ac90b70f4e967b67c8086038541cd 39 | 340c8ffc32e65debd4cfa73c1cf29664dafaf393 40 | 1b8c6303881548be754b0b9103bd66f5df46b054 41 | e1060c8d7bebb9d40941ebdef925be8557fa22cf 42 | 52446278daf9a694a6d1b68dd72ca8317c8bc1fb 43 | c7a225fd627ab33eae36d5bd10f4e7447e101fb6 44 | e0e71ed2b75ecbb909b676c2e3c0b3ee07213a2f 45 | a2855680648ae62f468d6a43adc77ca2e39c9ff0 46 | 97459b7e325bb55341390654006a866d803c4349 47 | dc948e035531deb9af297a4fbd9fe5502da792c2 48 | 5ce4699dcd96ba470c897c4b71b40f3de8844252 49 | 7d361543706748be7676bdf431c8bb7bfc9477e4 50 | 6d571b28fb09fcf413b8b0d770f3891a27b56bf9 51 | b139ccd5955d3fe379a065d61991573398ad414e 52 | 562a8d41fecd9467efce2cc664c9a0c939ad795f 53 | f7f5fc26356d0ba1198d7c1ee54df9e94ae7d0bf 54 | 3ca1beb87119e20c2e7ce77a33d5eb27b01634cf 55 | 075f2a632dbfb91077e65c764fe28ca1965d5962 56 | 1b4c49f11c2bebca7f154affdaddb4e7ed035e37 57 | 4d521c192f3ed531b82b905591fad810294bfe4c 58 | 93bc88cb7e4d6e2d0cfb049a0c07f8a56d003f31 59 | 443f2c790fac63671ff3697cf3f9575a81e705be 60 | 9826e809bad977835f32c0b7a3c5766424037888 61 | c08db213bf8c4f865ccd5dd9b0c646e8a41a29f3 62 | 400c41f09232d3f9f9f0c588ca69074f8cb345fa 63 | ff8a56ac611c0112dfd478eff75f11fc2c5ac210 64 | 5771f7cb6d0628dd4bdf4f7289b360b1f55c7e9b 65 | 7622801ddca99d4ca75b7f1b4a87fc05c212da2e 66 | 2d5aeaeff2099d2855c96fdd6c3192712e6e4726 67 | 225857505789ab6e3e32c4e194fe0c6a174534a3 68 | bab06ae06731da53d770a602bf06e199c5602b79 69 | 47877ad7d19d48023dbc15536c25d477a8989c27 70 | 987501c0f3834ad665b8745374c07cce419a9572 71 | 2d68227feff63059eab24766e2f35038563a655c 72 | 3f25eb29062f81d6eff030aae23cd0f677a7b63a 73 | eec86609ba9f9cd2f1497425f2e3a3fcd8116518 74 | d894bddc254bdc275118fd132385a2c7b8cf5845 75 | 522fb05fb3dc3255da032031a1057061e487150e 76 | b5edd4cd43970e95c4cedcb3a4387a1012e4fd28 77 | a46c2c677f004695f061bc2a16298e195ebe60d0 78 | 4ab9e6d029fafb29f56963cf54fd9bf35489be1a 79 | 8cc2bee32dce378da888f3246fe02e1889bc81a6 80 | 999684cfdd87b6997f6ad7b37e4969053c598791 81 | 4ab48f8ea1907955f154cf9a833eba8d73348170 82 | 4aaa2b3cc7adc66467233700175382713da15b6a 83 | cba2893e0b522a8199177c4c3f783c0d01fc92ba 84 | 15a066c3f564445e24a434e97208ab7a20c4da95 85 | 7a974fa70883e41bd6c515ab2981429816e2abc8 86 | 72b4c379af49e23d048a6f40c99111bf61162856 87 | 4dc026c73d7ea9ae56341893859ef9597d0c5b33 88 | 2acb831feb42a5c41577748d521bdc6512bd75b0 89 | 27c8460c9fdaa9b8feb9b3c4b706a4524f59ef72 90 | 242d09c83a004a61c06b7d44c3c02569ac40d2b1 91 | f8cc37e7ae7135ae977f7719ec66374cc97a74b1 92 | 0efbf9b428cb261611cb0ed547077e46d1d23aeb 93 | 1cba46119ca43e8d1fdb89c0197eaeed09950d64 94 | fc02667a6cb9f9ac28233f78da12b7e38cc42189 95 | 3f94f79b533089710a461d8839d3214d91ef9c99 96 | 0a3c679818804a2407b80284be1dc6ab84bf4cf4 97 | 913ef11c6e2e7414e5cf143ad392d7e860ec4aa2 98 | 0edb6ae9fc7b56b05b5332dfc9c829192ef9c39a 99 | 0501b043fceb4ad02c03c00e57b4f1177e906d47 100 | dca6ee23942a9800e81c88a00f118ce257db3503 101 | e0dd6caaa22e776ea3451c9aa050357e33e342e2 102 | b834eb3cee294435d314031e1c8e8d1234b3c982 103 | 9755dbf076132bca8c8953e84f21330cd9a39bb9 104 | ee698dd7a9471f02561bf9db90fccb5c17df611e 105 | 3fc8074aa3a7265c371e23006f950f5bf4f00668 106 | 7565521154ef14db183581f8fcac267ca26252f2 107 | 369143e7e58998ed05525f6dfdef693f2e6eab69 108 | 7541c18628d6b5b1fc4746969fa5796adb7a4de8 109 | "#; 110 | 111 | let second = r#"12d9b8707784bdc6460577d2d79802a937bc7c5e 112 | 653d4332a6146ded98db2644672fc7da10ad35c6 113 | b286e5a9367b972a5f40b4722d01388dbe1c235d 114 | 6311a2f15737d57cf59df1f67d0636ff6ea8143f 115 | 5f255e9bfad4df27f6283e5ff70f640c9e8f0ce1 116 | 1e84a19babc3ee3471a6f14ba7ab45db00966703 117 | 2707430b814386e8a50e27be70a8e38b1ce93e8c 118 | c41b693a20a7a5dfaf2e068306de74b3feb11f1d 119 | 624f2a7cea08daf255c57bb0a698330c4f06b711 120 | db75dcdf1905616b219f8120ffb088aebd20ad07 121 | 7c84871b1f6c355203f18e03dbf8e22cd7d87830 122 | 01198bb7e184632895b03501872ee4c257b0cb1f 123 | 743393e944a677000db0a988562fd98b0f71e60c 124 | 7287b1483fd51ef51fb3747d954ae213ac685898 125 | 14c26c67c91243587564d1a7e71d2c5b7853250f 126 | 47a200b54965019a18ba93e88980c0857dd34ca3 127 | 9e6fb1434f2ac52c78860daa1d13809e987fd1bd 128 | d01ef35c73c9aa52a84dd59a0e31672aeb5a47df 129 | e56d3ea7520aa66df593a2f6b394251641e6bef6 130 | 4ef268b789a1eb042dfa28683394b976cbf5d113 131 | 161dc6b59dd6f0f4350d9288105995037745de25 132 | ab95996d25040f367785b23e11cb8cc423b5d992 133 | 08c0393c74052127e8baafb6f3bb9a0a1137bf50 134 | b2db357d8629e6408b3bc1ace7ac1337044cb146 135 | 53c4bd8d0d0efa225c47f0af505607e5a36d726b 136 | d99e51eeb0061741e82228c99e1c2f426525f700 137 | 65976cf155ce659de2f775060e17624fd0154bb8 138 | e02fdc87856f8de04caddd8e6931d7c80cb64874 139 | 8b7f4e996462136b91b172a592cfef05d4063380 140 | 1a30feb7b9458b909a58fb10b80592fb1126d04a 141 | 6690195b4fe4643505c369146abce70fb1628059 142 | 9cfc9de3128ac90b70f4e967b67c8086038541cd 143 | 340c8ffc32e65debd4cfa73c1cf29664dafaf393 144 | 1b8c6303881548be754b0b9103bd66f5df46b054 145 | e1060c8d7bebb9d40941ebdef925be8557fa22cf 146 | 52446278daf9a694a6d1b68dd72ca8317c8bc1fb 147 | c7a225fd627ab33eae36d5bd10f4e7447e101fb6 148 | e0e71ed2b75ecbb909b676c2e3c0b3ee07213a2f 149 | a2855680648ae62f468d6a43adc77ca2e39c9ff0 150 | 97459b7e325bb55341390654006a866d803c4349 151 | dc948e035531deb9af297a4fbd9fe5502da792c2 152 | 5ce4699dcd96ba470c897c4b71b40f3de8844252 153 | 7d361543706748be7676bdf431c8bb7bfc9477e4 154 | 6d571b28fb09fcf413b8b0d770f3891a27b56bf9 155 | b139ccd5955d3fe379a065d61991573398ad414e 156 | 562a8d41fecd9467efce2cc664c9a0c939ad795f 157 | f7f5fc26356d0ba1198d7c1ee54df9e94ae7d0bf 158 | 3ca1beb87119e20c2e7ce77a33d5eb27b01634cf 159 | 075f2a632dbfb91077e65c764fe28ca1965d5962 160 | 1b4c49f11c2bebca7f154affdaddb4e7ed035e37 161 | 4d521c192f3ed531b82b905591fad810294bfe4c 162 | 12a59dcb14026c979722eaab748a905548982f24 163 | 531d50cadcfb125d9a13754ccb35c58f6fcb7547 164 | b0d8a478cbc68d1102b2df8df534439997ba2927 165 | fd85465973598d57f2c35b46bb6ca3dacf75bae7 166 | 3ba7d72e3d66de4edefec5daa770150baec7db82 167 | aab11afb47eb58756bb2d96997e2c6a145922151 168 | 835a8ba53dfdd8b407cb1089cdddb8c0fe2c6059 169 | d3c2d2ef2f2f4090ccd5d4c8c303f0edd6c15dcd 170 | f9cf5b8fd110cf679200054d2e68ff1060cb9feb 171 | 049c5eef3f6abdd0c6b83596dcf6827d87e9baba 172 | 3160849c04b1ec0166bcc9f7e70ce4fff2049c6c 173 | 64ec6ce6ea2a7fd0eb11610fa3759e578ae3ca66 174 | 4f3bcc72c6f413f207aaeba1a5dd63bbe91ab205 175 | f3c1423654179fabd4b32ad5ef1982bc36c9a895 176 | 62e9ce5cb651fee33e14fb7a99a56a5c4cca0700 177 | 8f9e3831c7c14b6c03fa5f0a949e40ad3c0f9b61 178 | ed8b282c7a7dc190426ac57853b2d927dd866e96 179 | 41f5b06422b602f0072c2682e8be08869d520e12 180 | c3050a0ae9841c32b20f1e4030cee23daaa89047 181 | aba50c740ec46d1e77334055c866d1b1bd43a644 182 | 3567c86607698d3b8a067330a4fb50126d0f522c 183 | 25ddf3a291378059fb43fca77d6582a55a5731b0 184 | 0864378a42aacc22b9ecbce0f9bbdb86333c2d7e 185 | dafdd92fdc9f6d3e447c5977976fba10264d1e19 186 | b23df930c0f1b9d14398ba3c3bdf3ee20e3911a2 187 | 60b574cc81b659e08a2c44f3a2add5da1675fd61 188 | 351a7fbf301cc9b46f49c9af7f4f1969c3713495 189 | 7c5ba25097eba8b53030bd79bddc0b3979c78952 190 | 51850ba8a0f1f7db76f1df6a0432e4d5559a023f 191 | e377af34935e595e7ba64b1bc3d111c2036a47fa 192 | 90f96122f538c185a22aad7ae63984fcc514bcce 193 | c3b5d72ff1b885c4fd49bdacb51efc653997dd73 194 | fafc17f5c28f61ac6b9a9154a173227be11ee327 195 | 607645aea1dd84c7f23d6a17d38cdeddbbe83d92 196 | 7c6278cab2d47f08c55c86021da7fed81bf1a324 197 | 1eb83584fe1511d3fd2513ee7c63d59893b824b3 198 | 86f2578150b9143c511404c3e3c52425fded407b 199 | 30d3974c9c85cd71cbbbfa47f9273a938f4f5ba9 200 | 312f7bca3077f9e200108d57ffdee9dde9082b0b 201 | 1ed01a2ccd4e5740b28aebceb196dca66fbafc36 202 | fefe64d9991da579790e8e32aa19eadd2858ca1f 203 | b9b9940661819572e9408766b862d1d6535df709 204 | bad517f6afe032b68c3ef34dee432c72422dc775 205 | a5a7f7e947cde369a5c9832ba2e26d71b577180f 206 | 24c5d19d1793122c8777beb886f9d79354454c81 207 | 38a4171a496de93c897fe28451a7d9b89cf09a5a 208 | cf77b1d5c29359bff76cc414ce46045afa8e258b 209 | a5aeec612e2ad5b05c494464c7d881d34a095042 210 | 4bd05aa06abce1dbba67e034d4a9e9d38c45f891 211 | 6650029767d33c416f018eba434935bd51e0e834 212 | 2263fb5c987afd04a456e78f623272615ec6a17d 213 | "#; 214 | 215 | let first_hash = FuzzyHash::new(first.to_string().into_bytes().to_vec()); 216 | let second_hash = FuzzyHash::new(second.to_string().into_bytes().to_vec()); 217 | 218 | assert_eq!(first_hash.compare_to(&second_hash), Some(44)) 219 | } 220 | -------------------------------------------------------------------------------- /tests/more.rs: -------------------------------------------------------------------------------- 1 | use fuzzyhash::FuzzyHash; 2 | 3 | #[test] 4 | pub fn fix_breakage() { 5 | let first = FuzzyHash::from("3072:oQGiMXTMkux9BPSd0n4bmzwuy+WAAux3i8:op1XTsbBBnnU8nAu48"); 6 | let second = FuzzyHash::from( 7 | "3072:zszq392p8xWp9+fbhBpmLOCeTFvm7RAkEmq8RPFc21xgpYn9R:Agse0Yb//hu7RAkc87go9", 8 | ); 9 | assert_eq!(first.compare_to(&second), Some(0)); 10 | } 11 | -------------------------------------------------------------------------------- /tests/random_data1.rs: -------------------------------------------------------------------------------- 1 | use fuzzyhash::FuzzyHash; 2 | 3 | #[test] 4 | fn random_data1() { 5 | let data = r#"fcc55a724745b7efbf9a54908aec300d01b9830dff4ee435a667330a7fc56ca9 6 | 8913e11dc9cd172efc57c13083b24bd4eb44ef06a9c760431c0b45edf5ea76e3 7 | ee53bde1e736f9c11383433351b98314cbada4742b1b46103a838c7d31a79b7e 8 | 20b30357d3b308721d1c5e1159eb7fe0a79e11368e06e7a1d3a52f222516e520 9 | 07354874a2790f82c62aa61a21d6335bb1b683d75e3f41d7c209bf88d4a06a6b 10 | ceb774b75d0242badee796f39d2d6769ace96184bfc3a80081d94fc6eeda497d 11 | b5c10e8667b9f786f20de678ad72d76c3f59cc1f75fa74b7bf4fdbd56b457689 12 | 999484ab64fd5458774610dd9c76dfaa8020d14cf1cf415deceda52db4111ac7 13 | 11fdf7e67ee5f9fe0c4e17f8a5d2a4df32ee32d6efe6dbfd5d2429ce52d56a62 14 | 6d198ffd7dddf4ad741d0c8b8aadf77cd338bf12320d0ffac2c12fd344fb8107 15 | 7431a7baa0c02b4231b16adc3438aa5569c7e86eba6401eabf3c1f1cf37587e6 16 | 86e9d0a7b49cb0f84bc356339f8aed56a281ce30c27a612b5cc0b2440198f1ca 17 | 4f81ca391ffe0f9c80f04090cf30ba167c20adecca24c865a5120604d21e383a 18 | 21c4fe0df1886d1f648e54c773a6084b1332f8ad51edc0233796384c821e078f 19 | c0ff7e11eaca91b772fb68d670c2af6563e7740ad3eb900c18905468fe0b2635 20 | ef82dc8ef62b14f3ccfac603c016f2a4e2231af30fb8c2c188ac9209a836e171 21 | 93e0c375b23d1271ecec237445746169d845ffd84dc40b20b704d23016d37d7d 22 | 25b1b76c3572db64b9caae2772c183ccda0cc0b42986b7f100861c8dab62313b 23 | 5e18dfd466610c8d944534a2644df922de1283e945be46a09b40edf0330601cb 24 | 124e4d572a885d89630070f393e3fff83b82b9cae94cfbf6dec9b5df6b71bede 25 | a2229ee50dc32599c06e81b6a44f472c1dd331e39a3aba8a439c23e618307d85 26 | 98a88ccb694d9ddc82940f9273113b9154029ed0279ddb472ea5bce35790c4a4 27 | 7fde1d2b20119e4cc8eb028767592ada24bf311c535654d9273a6470479b5d51 28 | 62172924f022b3fa464ec5e20b057c49f217507ed538f6c4be4541b2a46cb00c 29 | 7b4c2c0634d2d40594d5874fb4c313d1efac95a0416eea5781c0d1a3474cee54 30 | 61fedb07a07602d62ae5f92ef5e1ed8f13c9ebccfb42f630293596937ceebf70 31 | f943607f87d75afe4ecb1a0cb9879a408e0aef3415dce30e7ff6efdc975cfcfe 32 | 72c6d526b115aea7c2492a7f74b26703b57f2ea9827058edcee0732f37be309a 33 | 9550e7a282097f8ca5e31fc18a0626a35afa4105d2f7a8e3eaa7185fa019b52c 34 | 8534851a79683d1f9abfa0442712ed3bc42762f743c2ec2513d352806902ca18 35 | fe4519b09a054635d86913aed6cdd601c56fee079c8b6fffb68434d46172453a 36 | 729eb2d6195a03c094373243ee4f60c920f1240dfaa6d2e84f79c84132f084c7 37 | fdf4ba40c75cb6f24d3a11edfac45867fa083ac8cf1049f4505f644ace90aec6 38 | d2b6a8c7847c8fde9a0c494717fc164673cdb430a73ace426cdba126fbd1053e 39 | d6db1ceff62a14aa518b88f4d84905bc17271359380bf514decec55a172d2b61 40 | 93e254aacdb5bea463c49bd95a1abe2e9b3905ee6086bdaa6f45ff03615345f7 41 | 701c2b3839ba83afeb6b5126b2a282fc12c9242bef26dc26aa0a60b6c9a53273 42 | f1d2bc3247fafce6b777de977824f1400a4cf70c4c31b6e944b4db7d0e146a53 43 | 8b9a26247f3f071649d4c78559299952b9ba7cf7bfd502dce197c3c16ac8b6e1 44 | 67b98e714de060cfe027cbfe7dc15932622abbe93499cfa0f6c03bffad050f5e 45 | 2b6c24781cf16e4794312c6b18b907164e218fd2db26cd0a8321243244e1f5ac 46 | 8dc80c7d4aab2e4b66cf2f4b52e11b527dd32ad837d353e81ce01969783e56ac 47 | ad27c131afb9d5778ce9170eb58f8576d8d1f9e90d0f00f1a3351b337bc58815 48 | ebef0d4c8f0d924b2a51d05d7c91f367d37b87973cb2de472bd0deb6c79d87d9 49 | 9a1c55d5af4d70b966fbc09ee531c3de6272af925958ec66dd385be208f5c270 50 | 5d1a62e55f9e9ec782d57e3635d4cc3b0a1c6b52e365d1eb485ccab246a3338a 51 | daa530699a8a4c7dc072e9716373bfc248995c9bb467159d269e289d62aa7404 52 | 64300155a799ffd9ee362b4124aa1331c08648000b201528ebea173adf979634 53 | 69eb4e216dfb546d0f6bf62bc1195e0783369c09305237558b5c3913f6f25fd6 54 | 3b8b07019cdbe8ee366d82dea26903ff1cbd28dd8a301bd6367681e26f81f1ca 55 | 13351a1e922feb0794ac634eede9a29384287d909db2d2095bb9d250a022d10e 56 | cdebaddc6d2904e9fa4e0148009c40fbda4f09c02f9d3f640bfa871b8231559a 57 | 74bb3ce0455f7b497343b8b336ab22af61bf42ece31c1e776aa813b11c667de2 58 | bf003b7031ba665aa6a6e9513cbf12fa2f12133811f18ab35525c1d678621dfa 59 | 9bd6bb3ecdf3891b27f997f6eb6914903a5e068f774545aeff034a562d0ed310 60 | 392d7e64e37c8bf7cb2c9282aedfc19318c5223a28c1e1014005302fac671e03 61 | cc4fd33987c76cf97c38ca710a7bfdf3bf4f74ab51f7dcd42a05dd1b26970ecd 62 | cec48c10375432ff24e96b89cfe4b7e5bd7ae7603a66f4a039a9d55165a8b413 63 | a80dc414f2bda7055abfc085eab6e0fa031d2d64821a00724dae9121957b8e80 64 | 7c4f6c1f8dcc6bc46665a7fee886ac9ad3bfa5d21d11580d75d3c01eadad073b 65 | a7e8d819321b981496ddc2a10142394750c1e3eb0ea33cf86b9ac4be7bf8cc82 66 | 3285c7baab410c41d48b7f1ecd479316cf0f08abd87e2caf6ff70a95e173fad0 67 | 387c58b21c0aba8a8d2ab3a9c641b94d6dc84e4714f51c07c9fce745c05202a5 68 | cebdc80fa6ddfea8b6b3e50cbfd73e00c0413820b007b243f2f4b1d12adefcb0 69 | 75170da30a9ead02a97c92f9c59bc9185c02a52f38671fa362e3025289ffa427 70 | b55510b5ce5b54ebb5834b23dc0548738e9beb7495f7fb596a68a7f27d69f879 71 | 847123e04aeaec29c18040c7ed3e77a7ab88c9f3c03020035726b9b861ab14d0 72 | 0c8641b0fa6069c07688f00370b9b5793225301da6de7daf1fb85ddb660059a9 73 | f65665489f8f0e9c551ff94aea8d19193ac4f8d654dcb9fb75497e42640a37e2 74 | a5500b26b3fd3f77c433c0d85978c667898832f12709d5d79b1d90f62510e109 75 | "#; 76 | 77 | let bytes = data.as_bytes().to_vec(); 78 | let mut fuzzy_hash = FuzzyHash::new(bytes); 79 | fuzzy_hash.finalize(); 80 | 81 | assert_eq!( 82 | fuzzy_hash.to_string(), 83 | "96:S+AQXqxdOnBKd+jHwAznNFzxt2HJwDX9oWZiaK0ld7vVmS85mbaN+MmFRz/jiJ:ZXqxdO8YDnN1SHJiqLaK0lbFbbaN1mFs".to_string() 84 | ); 85 | } 86 | 87 | #[test] 88 | fn random_data_block_hash() { 89 | let data = r#"fcc55a724745b7efbf9a54908aec300d01b9830dff4ee435a667330a7fc56ca9 90 | 8913e11dc9cd172efc57c13083b24bd4eb44ef06a9c760431c0b45edf5ea76e3 91 | ee53bde1e736f9c11383433351b98314cbada4742b1b46103a838c7d31a79b7e 92 | 20b30357d3b308721d1c5e1159eb7fe0a79e11368e06e7a1d3a52f222516e520 93 | 07354874a2790f82c62aa61a21d6335bb1b683d75e3f41d7c209bf88d4a06a6b 94 | ceb774b75d0242badee796f39d2d6769ace96184bfc3a80081d94fc6eeda497d 95 | b5c10e8667b9f786f20de678ad72d76c3f59cc1f75fa74b7bf4fdbd56b457689 96 | 999484ab64fd5458774610dd9c76dfaa8020d14cf1cf415deceda52db4111ac7 97 | 11fdf7e67ee5f9fe0c4e17f8a5d2a4df32ee32d6efe6dbfd5d2429ce52d56a62 98 | 6d198ffd7dddf4ad741d0c8b8aadf77cd338bf12320d0ffac2c12fd344fb8107 99 | 7431a7baa0c02b4231b16adc3438aa5569c7e86eba6401eabf3c1f1cf37587e6 100 | 86e9d0a7b49cb0f84bc356339f8aed56a281ce30c27a612b5cc0b2440198f1ca 101 | 4f81ca391ffe0f9c80f04090cf30ba167c20adecca24c865a5120604d21e383a 102 | 21c4fe0df1886d1f648e54c773a6084b1332f8ad51edc0233796384c821e078f 103 | c0ff7e11eaca91b772fb68d670c2af6563e7740ad3eb900c18905468fe0b2635 104 | ef82dc8ef62b14f3ccfac603c016f2a4e2231af30fb8c2c188ac9209a836e171 105 | 93e0c375b23d1271ecec237445746169d845ffd84dc40b20b704d23016d37d7d 106 | 25b1b76c3572db64b9caae2772c183ccda0cc0b42986b7f100861c8dab62313b 107 | 5e18dfd466610c8d944534a2644df922de1283e945be46a09b40edf0330601cb 108 | 124e4d572a885d89630070f393e3fff83b82b9cae94cfbf6dec9b5df6b71bede 109 | a2229ee50dc32599c06e81b6a44f472c1dd331e39a3aba8a439c23e618307d85 110 | 98a88ccb694d9ddc82940f9273113b9154029ed0279ddb472ea5bce35790c4a4 111 | 7fde1d2b20119e4cc8eb028767592ada24bf311c535654d9273a6470479b5d51 112 | 62172924f022b3fa464ec5e20b057c49f217507ed538f6c4be4541b2a46cb00c 113 | 7b4c2c0634d2d40594d5874fb4c313d1efac95a0416eea5781c0d1a3474cee54 114 | 61fedb07a07602d62ae5f92ef5e1ed8f13c9ebccfb42f630293596937ceebf70 115 | f943607f87d75afe4ecb1a0cb9879a408e0aef3415dce30e7ff6efdc975cfcfe 116 | 72c6d526b115aea7c2492a7f74b26703b57f2ea9827058edcee0732f37be309a 117 | 9550e7a282097f8ca5e31fc18a0626a35afa4105d2f7a8e3eaa7185fa019b52c 118 | 8534851a79683d1f9abfa0442712ed3bc42762f743c2ec2513d352806902ca18 119 | fe4519b09a054635d86913aed6cdd601c56fee079c8b6fffb68434d46172453a 120 | 729eb2d6195a03c094373243ee4f60c920f1240dfaa6d2e84f79c84132f084c7 121 | fdf4ba40c75cb6f24d3a11edfac45867fa083ac8cf1049f4505f644ace90aec6 122 | d2b6a8c7847c8fde9a0c494717fc164673cdb430a73ace426cdba126fbd1053e 123 | d6db1ceff62a14aa518b88f4d84905bc17271359380bf514decec55a172d2b61 124 | 93e254aacdb5bea463c49bd95a1abe2e9b3905ee6086bdaa6f45ff03615345f7 125 | 701c2b3839ba83afeb6b5126b2a282fc12c9242bef26dc26aa0a60b6c9a53273 126 | f1d2bc3247fafce6b777de977824f1400a4cf70c4c31b6e944b4db7d0e146a53 127 | 8b9a26247f3f071649d4c78559299952b9ba7cf7bfd502dce197c3c16ac8b6e1 128 | 67b98e714de060cfe027cbfe7dc15932622abbe93499cfa0f6c03bffad050f5e 129 | 2b6c24781cf16e4794312c6b18b907164e218fd2db26cd0a8321243244e1f5ac 130 | 8dc80c7d4aab2e4b66cf2f4b52e11b527dd32ad837d353e81ce01969783e56ac 131 | ad27c131afb9d5778ce9170eb58f8576d8d1f9e90d0f00f1a3351b337bc58815 132 | ebef0d4c8f0d924b2a51d05d7c91f367d37b87973cb2de472bd0deb6c79d87d9 133 | 9a1c55d5af4d70b966fbc09ee531c3de6272af925958ec66dd385be208f5c270 134 | 5d1a62e55f9e9ec782d57e3635d4cc3b0a1c6b52e365d1eb485ccab246a3338a 135 | daa530699a8a4c7dc072e9716373bfc248995c9bb467159d269e289d62aa7404 136 | 64300155a799ffd9ee362b4124aa1331c08648000b201528ebea173adf979634 137 | 69eb4e216dfb546d0f6bf62bc1195e0783369c09305237558b5c3913f6f25fd6 138 | 3b8b07019cdbe8ee366d82dea26903ff1cbd28dd8a301bd6367681e26f81f1ca 139 | 13351a1e922feb0794ac634eede9a29384287d909db2d2095bb9d250a022d10e 140 | cdebaddc6d2904e9fa4e0148009c40fbda4f09c02f9d3f640bfa871b8231559a 141 | 74bb3ce0455f7b497343b8b336ab22af61bf42ece31c1e776aa813b11c667de2 142 | bf003b7031ba665aa6a6e9513cbf12fa2f12133811f18ab35525c1d678621dfa 143 | 9bd6bb3ecdf3891b27f997f6eb6914903a5e068f774545aeff034a562d0ed310 144 | 392d7e64e37c8bf7cb2c9282aedfc19318c5223a28c1e1014005302fac671e03 145 | cc4fd33987c76cf97c38ca710a7bfdf3bf4f74ab51f7dcd42a05dd1b26970ecd 146 | cec48c10375432ff24e96b89cfe4b7e5bd7ae7603a66f4a039a9d55165a8b413 147 | a80dc414f2bda7055abfc085eab6e0fa031d2d64821a00724dae9121957b8e80 148 | 7c4f6c1f8dcc6bc46665a7fee886ac9ad3bfa5d21d11580d75d3c01eadad073b 149 | a7e8d819321b981496ddc2a10142394750c1e3eb0ea33cf86b9ac4be7bf8cc82 150 | 3285c7baab410c41d48b7f1ecd479316cf0f08abd87e2caf6ff70a95e173fad0 151 | 387c58b21c0aba8a8d2ab3a9c641b94d6dc84e4714f51c07c9fce745c05202a5 152 | cebdc80fa6ddfea8b6b3e50cbfd73e00c0413820b007b243f2f4b1d12adefcb0 153 | 75170da30a9ead02a97c92f9c59bc9185c02a52f38671fa362e3025289ffa427 154 | b55510b5ce5b54ebb5834b23dc0548738e9beb7495f7fb596a68a7f27d69f879 155 | 847123e04aeaec29c18040c7ed3e77a7ab88c9f3c03020035726b9b861ab14d0 156 | 0c8641b0fa6069c07688f00370b9b5793225301da6de7daf1fb85ddb660059a9 157 | f65665489f8f0e9c551ff94aea8d19193ac4f8d654dcb9fb75497e42640a37e2 158 | a5500b26b3fd3f77c433c0d85978c667898832f12709d5d79b1d90f62510e109 159 | "#; 160 | 161 | let bytes = data.as_bytes().to_vec(); 162 | let mut hasher = FuzzyHash::default(); 163 | let mut i = 0; 164 | while i < data.len() { 165 | let some = bytes 166 | .iter() 167 | .skip(i) 168 | .take(10) 169 | .map(|u| u.to_owned()) 170 | .collect::>(); 171 | i += some.len(); 172 | hasher.update(some); 173 | } 174 | 175 | hasher.finalize(); 176 | 177 | assert_eq!( 178 | hasher.to_string(), 179 | "96:S+AQXqxdOnBKd+jHwAznNFzxt2HJwDX9oWZiaK0ld7vVmS85mbaN+MmFRz/jiJ:ZXqxdO8YDnN1SHJiqLaK0lbFbbaN1mFs".to_string() 180 | ); 181 | } 182 | -------------------------------------------------------------------------------- /tests/test_data.bin: -------------------------------------------------------------------------------- 1 | 92dd651156de8191164afa5fc9f1d065e3967977feabbc57ab8594fff9c6632c 2 | c045b49f73825dd2881062ca11a5918d527597ac7c0991895e02547610ad7218 3 | f8a56492038b3247f3bd0f1d36c4d92d08f617fd4df1a2618392d8d4ef1e26a4 4 | 55c5ce454e80c64dff7f2d9ed97f6769eb1b16a2d3c4241cbc20423ce549b311 5 | 19c1c421a539cd7000a63d483e2d1151c0c65c37cc09d05a4ac3c41488622479 6 | 465a1464c565eaa0ea71f0c9c8a682f555fa145a51c82f5021d7a30bb8f4b8de 7 | f365be3c2fab01f30cb5791453b9c842f8a086c0d84ed6defbe6730fd2a4d834 8 | 0d5241c23e9a42d721fa53525277de44b0cf88b7f4490ddb499c903e128cbe7b 9 | 3773c2fcdb0bbb52e05e48b6482a798e91e3ec3741093e631796eab0733b8e22 10 | 2917c7da01b8f4e03e91361a1fd7b6870d161fa490a7ca3b50071ccba48d5384 11 | 827361cf224140c5039fcfff9135b5dffe7b697662bcf09a0e0f27572b9a275b 12 | 39296f5bd26adaf0c456399255f923bb656ec3a041b8cd9d41e30d4386c9b520 13 | 9d3e2f83cc619fa72d6f408a524e00d845179b0b83d933511df864f45544e1fd 14 | 5a9f9ba512673b41ce94c69357724dadabfe0f1a29c7fc184dfce789b2950039 15 | ebc050382c93494828a882d78e55754b02480dda2ebc1514aabb20aecb1e68a1 16 | 05a00eb9136cfe1f3d3b18f8207e70540df8fd95942ce91505d4c5a43300c4c7 17 | 4c615807e97f8c7e1ee7dd065a8ac182e707959b077c9857874ff236dd20a892 18 | bd986a189bc203c50f55f3a7b3cb26c71061341c8693e6eba3785b76b13765c4 19 | 909abe35dd10cedaba906730c2cb356b4bd55d7d5a38a95a99ced9039987c385 20 | 4824ff0553a9be7e833274cc1522d9b5e3be068a932116a04c9007c885c6e74e 21 | 1555baf64ec7dfc721604db40675d3ffb561b15af35f07764ec211dd50b4f1d8 22 | d84fc56da9a3e1254324372dadc3c37dac0f3af0efecaf218d57ff548332ccdb 23 | 71e553afaec85792f36bc0efacc9e7eb26636fb71900680137821dc81d5b3ed0 24 | 13defd8d1afd0cd8987deb993a49f86928691befa78889fdbaca2ff4b18f5f82 25 | 5b0843e427b2321172f03bf88c0aaeb377965a90df98cbff262b001d77c213fc 26 | b0faa181b458b4c62fe90f02a7c4ff60e99a88a8ef94534140feae0ed2827621 27 | d7305b446a9eb44e91839bfc177894023483f0f0884f59265f69e7a154da2716 28 | bd1eb0b057b92b4948219f097bc3ab6ad78d9a216ef8bb2d90ab3ec7dc552fe4 29 | 3487c8570257fcde430b35fe75a532b8cc69c33a5e2f6d4eb7952b4ed8fe18a7 30 | 98bc803c868390bb3adff36b75251ce5e5592a5140bc8940ebf283cefa402a46 31 | 816534a7e58cba3ef11896c650f8ec294ab9ae5260038fe664ca7013ff34a919 32 | 49e173357a05e54b292b3ec80a684e568cc1e5673867d03575ed8cd59333e93f 33 | 3bff626a004f6147ae844a7dbfc01eae5afc56efa0e69258e3a4690ce43a62ad 34 | 2153d67b0d257c1e2966585422f95a92fe77fc19aa1a3ff266308051b7835aa6 35 | fc1d187094506579796b35795769e1eb260b5641b8c342ac2d3fb90c2e50518e 36 | 03133984fc4da77f0e3396213f51ba9e716d3f8baeae60e035a7263f9c8a4d19 37 | 6a86da8130597cd469b00c4f6f8cedb7599e8276be6218bd57c35da6cda912a8 38 | 2e6133d4037aa7f4bdb8e7c51b308a78e3d1c31c027f4305838e34a3fcdea7e7 39 | e078e5a4abe6c202cff7388075848112b777a271211a2205f32d6aecd85f4d79 40 | 6fb5b3ed5c38a4f3a96ffe82a19f3d6973c502af49255de1830548e92dd52ca5 41 | fd4a18eea1d9313f11d8aed787a599fb5d45e77e4210e2695efa3b89d1a006d2 42 | c5f067806776e3de5efa1468030fe043f2a1f8f4814bf77cb7773c9ef5b80c29 43 | a7c62111100eda4c6fe8b0ad26446b8853bf820c773093bf7f7a555157ca24b4 44 | c1801cc22a544b85a1070ae6eba6e276554d7d48d9b2c05996e07631ce0881f6 45 | af68a0a1f3834cc44229e0a10f0aa6408de99c7e9cb8002aaa847c38ea8ebacc 46 | c8abd01c289edd08c45fcb3d595431d359cf16a40243bc10624fcb968087aaca 47 | 7ebf743e42864e27551e65c7b931506b4fb31a53f5eb564be86fe2e4097f5751 48 | 3ffc968f212039adfff5c1573e8494b1a442a4c700d68502a188598bf0f82a7f 49 | 1dce6006dc887da8fbe215e1f555c073305a5ea30fbfa92b97176917c3c30976 50 | e1dc6f46123907f82ca8103d74f973ca0b524e9eebcc2f703b936347ab134871 51 | 01cb31377e4b18a5e769a7956445927f4a1219c4ca9bb33b4c745737be38ef64 52 | 0a4292077e5b7b7a28236c279be833793ff07c16bc0976aa032304bf4b5a573d 53 | 447139abaab28785e87b99ff6dd00cd6eb367ea896ecdd438f86ba9a32992397 54 | 26d4999a4539af236b6de03a0b59d8ad9be34db7e62d3dab2fb791e17f915e44 55 | bbc61b799378fcb0d2bd9517a1823ed06344149d47b9b3d35dab5764d0709180 56 | f24c22a3b4d39ab12415babd9e65a1c48d66ee040b9731adf35a3eddfc6361ce 57 | d52014a5c5954527aaf734c9095d6a81460f912214fccb322fcb62e314a19f2e 58 | 9990c01c31b20ea2be787cac00f8a5afe07eb28e8f3ca1b5bf26d180ec274d9b 59 | d1659008d12f5401d4a9e61664f2904d22cf1e0b43b232fb74f01695216f5965 60 | 1505ed0447bceada89175f6e6fe6b01f0bb78434c6ed130ec9deeddaa54502aa 61 | 47cfd7b825c49da0d0bb4b7fd1e953a617acf5804c2c665507b2f6939d0e6675 62 | 9a3e04fb35e5b9e68aed9f6e4d355d2be687cd2ab0165fd1cb9e9c161b44342d 63 | 3917544d5f726cbeff2c7fa72adae30daf7188d236a3840c8aea69cace836123 64 | f5d85e112eec2b227ddba6d42f241e645223535fd700c4b0225a2ddef1800746 65 | 16afef47a3f5559034c5fe8a8a37a78081761c47f6f5c256bc28deda0b1b4537 66 | 8c59b3940889bc9529f27479dea4af86d560a70c3fd7d70f9b4d06094bba51b5 67 | 40c67a7dd30fc4e525328a2793b05eaff3411d2e88fca0c9cb2362b3890b1947 68 | a40ac281c0047419f45cfd9f83144be413aefd0cd54e5ea4a3d31bf3e210565b 69 | 1983db6ff88a97c89a27ff1ff23d8643cf812cf256886d8544f2f5e8393fdac0 70 | e6d2880dd6dca567e812a97b1c93bc8b5cb94333473b828188596b4964823767 71 | 4271d486059f64ae07b0d4528ed9251c4e5189ff504558dd92b87ec958f35c36 72 | 0f26db6dfc698c294cb0fbfc2d0dc5f45c538f969fd641979ac1361640063469 73 | 3ab084ebcf3503deec8c1d94f789c1db3f8047154707f9dc97eb01918da0fa88 74 | 5f7735bddad11120e9b4b69c6ad0e15b056d4f6ab44ac74d9910cdfcc15475a6 75 | 05af4c726c371270bd23716bd5a1f8b202817fa9d1e09f787045d37689006b5d 76 | 9d1d70ed002e969ba65ce71f0e436fb2407128363428d76a8c7b8e9b8524b336 77 | eb4512522f6f51e8443ef85a83ac8813a45d5445234a94a2d830bb425d91d9c8 78 | 2fa7c0a7e938b978c4998b873a1f7597243c08dee158d33575f0c3d51f7f9d07 79 | 4c6532264ab10f3f65fbda4b33be9b4940b9a948b1372bff2fd9e31f990db217 80 | 7482ad92f8ef347ba9dd1dfbddf0b26918db5f1f083f4f902dbebcd04646854a 81 | 65b8e21f038db504f38c79b92005a2eecc288cbc433634c4e4c98560c1f420fe 82 | b42aa03262331d18af6217c94647ca2946c4500ce86603cc71a1ebd88d6ae70e 83 | 95cb9bbd99461d0e7c2597394bb9645deb9becb58b2429511fa9934f8c78a0cf 84 | fb868eae1fdad05e1b04e901b8a44d07daf33543a39906730c19ec092c9d9d3e 85 | 3d190b6258607345c18dc57e000b02c8e9613ac875b4180d3b0652e49a60f60a 86 | 73d3fce83fe5fbdbc3c81e5c2662a90eb3eb8ecf4e2eeac72942ea3359444bd3 87 | 1f30ca694a6edb0bdaa0f8738a840791b42b6bac59e814b664fe42d1e83ff8a3 88 | 8d0f8b2fa53070607c0b6ed1730c5538b30d2b5d2754fc48a6bea42b36ef76fb 89 | b7bab9499b0302c52a42ac97a68ee2e6429cedd38b59444b8383e642b422cb6a 90 | 346a145fa6b2e54d4f8a286f0cfbdc0d7ce7376244c5589bea42f5bfcfd252d5 91 | 3348e6c24871c5547556f81293073ca12556e7ee1ea97a798646c3a9f692a49e 92 | 9383201b9798a0e95d2c539b4d8637b20f1a74bebbc973561796daf66fae82fc 93 | 2a2e109283672b4000f42afd72cba0a00e1310eb14589f8e1357a1ff460ea41a 94 | e81b73ecf6c3a9ac3afa63458f295319162855907a1677dc6aef5541f56cf32e 95 | e2722f6c5dbdf753a14362ef0876611125c0ebabc07219070934f6510fe343ad 96 | 55bc6873258757fda117a8f1e990e9f5ae872df810e12786e3f0512c833332ab 97 | 6ea5ce83e4072b77e03707f19d4f4f75cbb696d897a1d5c97468dc3b58251d2c 98 | 5a24dbab9ed5f1e24c4e4dd371970cfcb24e53126fbf010199bf3c4092cd2c5a 99 | 57ea9c4a576a8f1acffbcc96b18ab7336d1a9c909580d6315ddd962dec5e2a0f 100 | 90133b65b506ffbb0b9e1282ed9d3e3070485a77da9edfec755b3582ac61dbde 101 | --------------------------------------------------------------------------------