Skip to content

Commit a3944fe

Browse files
authored
Merge pull request #1694 from AliceGrey/master
Add MurmurHash3 Operation
2 parents 56a8e02 + cfc8a50 commit a3944fe

File tree

4 files changed

+222
-0
lines changed

4 files changed

+222
-0
lines changed

src/core/config/Categories.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,7 @@
369369
"SHA2",
370370
"SHA3",
371371
"SM3",
372+
"MurmurHash3",
372373
"Keccak",
373374
"Shake",
374375
"RIPEMD",
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/**
2+
* Based on murmurhash-js (https://github.com/garycourt/murmurhash-js)
3+
* @author Gary Court
4+
* @license MIT
5+
*
6+
* @author AliceGrey [[email protected]]
7+
* @copyright Crown Copyright 2024
8+
* @license Apache-2.0
9+
*/
10+
11+
import Operation from "../Operation.mjs";
12+
13+
/**
14+
* MurmurHash3 operation
15+
*/
16+
class MurmurHash3 extends Operation {
17+
18+
/**
19+
* MurmurHash3 constructor
20+
*/
21+
constructor() {
22+
super();
23+
24+
this.name = "MurmurHash3";
25+
this.module = "Default";
26+
this.description = "Generates a MurmurHash v3 for a string input and an optional seed input";
27+
this.infoURL = "https://wikipedia.org/wiki/MurmurHash";
28+
this.inputType = "string";
29+
this.outputType = "number";
30+
this.args = [
31+
{
32+
name: "Seed",
33+
type: "number",
34+
value: 0
35+
},
36+
{
37+
name: "Convert to Signed",
38+
type: "boolean",
39+
value: false
40+
}
41+
];
42+
}
43+
44+
/**
45+
* Calculates the MurmurHash3 hash of the input.
46+
* Based on Gary Court's JS MurmurHash implementation
47+
* @see http://github.com/garycourt/murmurhash-js
48+
* @author AliceGrey [[email protected]]
49+
* @param {string} input ASCII only
50+
* @param {number} seed Positive integer only
51+
* @return {number} 32-bit positive integer hash
52+
*/
53+
mmh3(input, seed) {
54+
let h1b;
55+
let k1;
56+
const remainder = input.length & 3; // input.length % 4
57+
const bytes = input.length - remainder;
58+
let h1 = seed;
59+
const c1 = 0xcc9e2d51;
60+
const c2 = 0x1b873593;
61+
let i = 0;
62+
63+
while (i < bytes) {
64+
k1 =
65+
((input.charCodeAt(i) & 0xff)) |
66+
((input.charCodeAt(++i) & 0xff) << 8) |
67+
((input.charCodeAt(++i) & 0xff) << 16) |
68+
((input.charCodeAt(++i) & 0xff) << 24);
69+
++i;
70+
71+
k1 = ((((k1 & 0xffff) * c1) + ((((k1 >>> 16) * c1) & 0xffff) << 16))) & 0xffffffff;
72+
k1 = (k1 << 15) | (k1 >>> 17);
73+
k1 = ((((k1 & 0xffff) * c2) + ((((k1 >>> 16) * c2) & 0xffff) << 16))) & 0xffffffff;
74+
75+
h1 ^= k1;
76+
h1 = (h1 << 13) | (h1 >>> 19);
77+
h1b = ((((h1 & 0xffff) * 5) + ((((h1 >>> 16) * 5) & 0xffff) << 16))) & 0xffffffff;
78+
h1 = (((h1b & 0xffff) + 0x6b64) + ((((h1b >>> 16) + 0xe654) & 0xffff) << 16));
79+
}
80+
81+
k1 = 0;
82+
83+
if (remainder === 3) {
84+
k1 ^= (input.charCodeAt(i + 2) & 0xff) << 16;
85+
}
86+
87+
if (remainder === 3 || remainder === 2) {
88+
k1 ^= (input.charCodeAt(i + 1) & 0xff) << 8;
89+
}
90+
91+
if (remainder === 3 || remainder === 2 || remainder === 1) {
92+
k1 ^= (input.charCodeAt(i) & 0xff);
93+
94+
k1 = (((k1 & 0xffff) * c1) + ((((k1 >>> 16) * c1) & 0xffff) << 16)) & 0xffffffff;
95+
k1 = (k1 << 15) | (k1 >>> 17);
96+
k1 = (((k1 & 0xffff) * c2) + ((((k1 >>> 16) * c2) & 0xffff) << 16)) & 0xffffffff;
97+
h1 ^= k1;
98+
}
99+
100+
h1 ^= input.length;
101+
102+
h1 ^= h1 >>> 16;
103+
h1 = (((h1 & 0xffff) * 0x85ebca6b) + ((((h1 >>> 16) * 0x85ebca6b) & 0xffff) << 16)) & 0xffffffff;
104+
h1 ^= h1 >>> 13;
105+
h1 = ((((h1 & 0xffff) * 0xc2b2ae35) + ((((h1 >>> 16) * 0xc2b2ae35) & 0xffff) << 16))) & 0xffffffff;
106+
h1 ^= h1 >>> 16;
107+
108+
return h1 >>> 0;
109+
}
110+
111+
/**
112+
* Converts an unsigned 32-bit integer to a signed 32-bit integer
113+
* @author AliceGrey [[email protected]]
114+
* @param {value} 32-bit unsigned integer
115+
* @return {number} 32-bit signed integer
116+
*/
117+
unsignedToSigned(value) {
118+
if (value & 0x80000000) {
119+
return -0x100000000 + value;
120+
} else {
121+
return value;
122+
}
123+
}
124+
125+
/**
126+
* @param {string} input
127+
* @param {Object[]} args
128+
* @returns {number}
129+
*/
130+
run(input, args) {
131+
if (args && args.length >= 1) {
132+
const seed = args[0];
133+
const hash = this.mmh3(input, seed);
134+
if (args.length > 1 && args[1]) {
135+
return this.unsignedToSigned(hash);
136+
}
137+
return hash;
138+
}
139+
return this.mmh3(input);
140+
}
141+
}
142+
143+
export default MurmurHash3;

tests/operations/index.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ import "./tests/LZNT1Decompress.mjs";
6666
import "./tests/MS.mjs";
6767
import "./tests/Magic.mjs";
6868
import "./tests/MorseCode.mjs";
69+
import "./tests/MurmurHash3.mjs";
6970
import "./tests/NetBIOS.mjs";
7071
import "./tests/NormaliseUnicode.mjs";
7172
import "./tests/OTP.mjs";
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/**
2+
* MurmurHash3 tests
3+
* @author AliceGrey [[email protected]]
4+
* @copyright Crown Copyright 2024
5+
* @license Apache-2.0
6+
*/
7+
8+
import TestRegister from "../../lib/TestRegister.mjs";
9+
10+
TestRegister.addTests([
11+
{
12+
name: "To MurmurHash3: nothing",
13+
input: "",
14+
expectedOutput: "0",
15+
recipeConfig: [
16+
{
17+
op: "MurmurHash3",
18+
args: [0],
19+
},
20+
],
21+
},
22+
{
23+
name: "To MurmurHash3: 1",
24+
input: "1",
25+
expectedOutput: "2484513939",
26+
recipeConfig: [
27+
{
28+
op: "MurmurHash3",
29+
args: [0],
30+
},
31+
],
32+
},
33+
{
34+
name: "To MurmurHash3: Hello World!",
35+
input: "Hello World!",
36+
expectedOutput: "3691591037",
37+
recipeConfig: [
38+
{
39+
op: "MurmurHash3",
40+
args: [0],
41+
},
42+
],
43+
},
44+
{
45+
name: "To MurmurHash3: Hello World! with seed",
46+
input: "Hello World!",
47+
expectedOutput: "1148600031",
48+
recipeConfig: [
49+
{
50+
op: "MurmurHash3",
51+
args: [1337],
52+
},
53+
],
54+
},
55+
{
56+
name: "To MurmurHash3: foo",
57+
input: "foo",
58+
expectedOutput: "4138058784",
59+
recipeConfig: [
60+
{
61+
op: "MurmurHash3",
62+
args: [0],
63+
},
64+
],
65+
},
66+
{
67+
name: "To MurmurHash3: foo signed",
68+
input: "foo",
69+
expectedOutput: "-156908512",
70+
recipeConfig: [
71+
{
72+
op: "MurmurHash3",
73+
args: [0, true],
74+
},
75+
],
76+
}
77+
]);

0 commit comments

Comments
 (0)