Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,19 @@ const diff = diff(obj1, obj2);
console.log(diff(obj1, obj2));
```

## `murmurHash(str)`

Converts input string (of any length) into a 32-bit positive integer using [MurmurHash3](https://en.wikipedia.org/wiki/MurmurHash).

Usage:

```js
import { murmurHash } from "ohash/murmur";

// "427197390"
console.log(murmurHash("Hello World"));
```

## Contribute

- Clone this repository
Expand All @@ -148,3 +161,6 @@ Made with 💛 Published under [MIT License](./LICENSE).
Object serialization originally based on [puleos/object-hash](https://github.com/puleos/object-hash) by [Scott Puleo](https://github.com/puleos/).

sha256 implementation originally based on [brix/crypto-js](https://github.com/brix/crypto-js).

MurmurHash implementation based on [perezd/node-murmurhash](https://github.com/perezd/node-murmurhash) and
[garycourt/murmurhash-js](https://github.com/garycourt/murmurhash-js) by [Gary Court](mailto:[email protected]) and [Austin Appleby](mailto:[email protected]).
3 changes: 2 additions & 1 deletion build.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { defineBuildConfig } from "unbuild";
import { transform } from "esbuild";
import { rm } from "node:fs/promises";
import { defineBuildConfig } from "unbuild";

export default defineBuildConfig({
hooks: {
Expand All @@ -21,6 +21,7 @@ export default defineBuildConfig({
async "build:done"() {
await rm("dist/index.d.ts");
await rm("dist/crypto/js/index.d.ts");
await rm("dist/crypto/js/murmur.d.ts");
await rm("dist/crypto/node/index.d.ts");
},
},
Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@
"types": "./dist/crypto/js/index.d.mts",
"default": "./dist/crypto/js/index.mjs"
}
},
"./murmur": {
"types": "./dist/crypto/js/murmur.d.mts",
"default": "./dist/crypto/js/murmur.mjs"
}
},
"types": "./dist/index.d.mts",
Expand Down
91 changes: 91 additions & 0 deletions src/crypto/js/murmur.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/**
* JS Implementation of MurmurHash3 (r136) (as of May 20, 2011)
*
* @param {Uint8Array | string} key
* @param {number} seed Positive integer only
* @return {number} 32-bit positive integer hash
*/
export function murmurHash(key: Uint8Array | string, seed: number = 0): number {
if (typeof key === "string") {
key = createBuffer(key);
}

let i = 0;
let h1 = seed;
let k1;
let h1b;

const remainder = key.length & 3; // key.length % 4
const bytes = key.length - remainder;
const c1 = 0xcc_9e_2d_51;
const c2 = 0x1b_87_35_93;

while (i < bytes) {
k1 =
(key[i] & 0xff) |
((key[++i] & 0xff) << 8) |
((key[++i] & 0xff) << 16) |
((key[++i] & 0xff) << 24);
++i;

k1 =
((k1 & 0xff_ff) * c1 + ((((k1 >>> 16) * c1) & 0xff_ff) << 16)) &
0xff_ff_ff_ff;
k1 = (k1 << 15) | (k1 >>> 17);
k1 =
((k1 & 0xff_ff) * c2 + ((((k1 >>> 16) * c2) & 0xff_ff) << 16)) &
0xff_ff_ff_ff;

h1 ^= k1;
h1 = (h1 << 13) | (h1 >>> 19);
h1b =
((h1 & 0xff_ff) * 5 + ((((h1 >>> 16) * 5) & 0xff_ff) << 16)) &
0xff_ff_ff_ff;
h1 =
(h1b & 0xff_ff) + 0x6b_64 + ((((h1b >>> 16) + 0xe6_54) & 0xff_ff) << 16);
}

k1 = 0;

switch (remainder) {
case 3: {
k1 ^= (key[i + 2] & 0xff) << 16;
/* falls through */
}
case 2: {
k1 ^= (key[i + 1] & 0xff) << 8;
/* falls through */
}
case 1: {
k1 ^= key[i] & 0xff;
k1 =
((k1 & 0xff_ff) * c1 + ((((k1 >>> 16) * c1) & 0xff_ff) << 16)) &
0xff_ff_ff_ff;
k1 = (k1 << 15) | (k1 >>> 17);
k1 =
((k1 & 0xff_ff) * c2 + ((((k1 >>> 16) * c2) & 0xff_ff) << 16)) &
0xff_ff_ff_ff;
h1 ^= k1;
}
}

h1 ^= key.length;

h1 ^= h1 >>> 16;
h1 =
((h1 & 0xff_ff) * 0x85_eb_ca_6b +
((((h1 >>> 16) * 0x85_eb_ca_6b) & 0xff_ff) << 16)) &
0xff_ff_ff_ff;
h1 ^= h1 >>> 13;
h1 =
((h1 & 0xff_ff) * 0xc2_b2_ae_35 +
((((h1 >>> 16) * 0xc2_b2_ae_35) & 0xff_ff) << 16)) &
0xff_ff_ff_ff;
h1 ^= h1 >>> 16;

return h1 >>> 0;
}

function createBuffer(val: any) {
return new TextEncoder().encode(val);
}
38 changes: 38 additions & 0 deletions test/crypto.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { describe, expect, it } from "vitest";
import * as cryptoJS from "../src/crypto/js";
import * as cryptoNode from "../src/crypto/node";

import { murmurHash } from "../src/crypto/js/murmur";

// import * as cryptoDistJS from "../dist/crypto/js/index.mjs";

const impls = {
Expand All @@ -23,3 +25,39 @@ describe("crypto:digest", () => {
});
}
});

describe("crypto:murmurHash", () => {
it("Generates correct hash for 0 bytes without seed", () => {
expect(murmurHash("")).toMatchInlineSnapshot("0");
});
it("Generates correct hash for 0 bytes with seed", () => {
expect(murmurHash("", 1)).toMatchInlineSnapshot("1364076727"); // 0x514E28B7
});
it("Generates correct hash for 'Hello World'", () => {
expect(murmurHash("Hello World")).toMatchInlineSnapshot("427197390");
});
it("Generates the correct hash for varios string lengths", () => {
expect(murmurHash("a")).toMatchInlineSnapshot("1009084850");
expect(murmurHash("aa")).toMatchInlineSnapshot("923832745");
expect(murmurHash("aaa")).toMatchInlineSnapshot("3033554871");
expect(murmurHash("aaaa")).toMatchInlineSnapshot("2129582471");
expect(murmurHash("aaaaa")).toMatchInlineSnapshot("3922341931");
expect(murmurHash("aaaaaa")).toMatchInlineSnapshot("1736445713");
expect(murmurHash("aaaaaaa")).toMatchInlineSnapshot("1497565372");
expect(murmurHash("aaaaaaaa")).toMatchInlineSnapshot("3662943087");
expect(murmurHash("aaaaaaaaa")).toMatchInlineSnapshot("2724714153");
});
it("Works with Uint8Arrays", () => {
expect(
murmurHash(new Uint8Array([0x21, 0x43, 0x65, 0x87])),
).toMatchInlineSnapshot("4116402539"); // 0xF55B516B
});
it("Handles UTF-8 high characters correctly", () => {
expect(murmurHash("ππππππππ", 0x97_47_b2_8c)).toMatchInlineSnapshot(
"3581961153",
);
});
it("Gives correct hash with uint32 maximum value as seed", () => {
expect(murmurHash("a", 2_147_483_647)).toMatchInlineSnapshot("3574244913");
});
});