Skip to content

Commit bf117c9

Browse files
authored
fix: update string hash algorithm (#542)
This fixes an cjs/esm import error with the farmhash-modern dependency. The string hash algorithm is switched to use FNV-1a instead which has a purejs implementation that works well on the server and in the browser. Unit tests added as well to ensure future compatibility.
1 parent 66886f4 commit bf117c9

File tree

4 files changed

+59
-15
lines changed

4 files changed

+59
-15
lines changed

packages/root-cms/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@
7272
"csv-parse": "5.5.2",
7373
"csv-stringify": "6.4.4",
7474
"dts-dom": "3.7.0",
75-
"farmhash-modern": "1.1.0",
75+
"fnv-plus": "1.3.1",
7676
"jsonwebtoken": "9.0.2",
7777
"kleur": "4.1.5",
7878
"sirv": "2.0.3",
@@ -102,6 +102,7 @@
102102
"@preact/compat": "17.1.2",
103103
"@tabler/icons-preact": "2.47.0",
104104
"@types/body-parser": "1.19.3",
105+
"@types/fnv-plus": "1.3.2",
105106
"@types/gapi": "0.0.47",
106107
"@types/gapi.client.drive-v3": "0.0.4",
107108
"@types/gapi.client.sheets-v4": "0.0.4",
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import {describe, it, expect} from 'vitest';
2+
import {hashStr} from './strings.js';
3+
4+
describe('hashStr', () => {
5+
it('generates consistent hash for input', () => {
6+
const input = 'hello world';
7+
// Run the hash twice to verify the output is consistent.
8+
const hash1 = hashStr(input);
9+
const hash2 = hashStr(input);
10+
expect(hash1).toMatchInlineSnapshot('"a65e7023cd59e"');
11+
expect(hash1).toEqual(hash2);
12+
});
13+
14+
it('generates the same hash for strings with leading and trailing whitespace', () => {
15+
const input1 = 'hello test';
16+
const input2 = 'hello test ';
17+
const input3 = ' hello test ';
18+
const input4 = 'hello test ';
19+
const hash1 = hashStr(input1);
20+
const hash2 = hashStr(input2);
21+
const hash3 = hashStr(input3);
22+
const hash4 = hashStr(input4);
23+
expect(hash1).toMatchInlineSnapshot('"5a364db8899ed"');
24+
expect(hash1).toEqual(hash2);
25+
expect(hash1).toEqual(hash3);
26+
expect(hash1).toEqual(hash4);
27+
});
28+
});

packages/root-cms/shared/strings.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* Shared utility functions for handling strings.
33
*/
44

5-
import * as farmhash from 'farmhash-modern';
5+
import fnv from 'fnv-plus';
66

77
/**
88
* Cleans a source string for use in translations. Performs the following:
@@ -24,12 +24,20 @@ function removeTrailingWhitespace(str: string) {
2424
/**
2525
* Returns a hash fingerprint for a string.
2626
*
27-
* Note that this hash function is meant to be fast and for collision avoidance
28-
* for use in a hash map, but is not intended for cryptographic purposes. For
29-
* these reasons farmhash is used here.
27+
* Note that this hash function is meant to be fast and collision-free for use
28+
* in a hash map, but is not intended for cryptographic purposes. For these
29+
* reasons `FNV-1a` is used here.
3030
*
31-
* @see https://www.npmjs.com/package/farmhash-modern
31+
* NOTE: farmhash-modern was previously tested here, but had issues with
32+
* cjs/esm imports. A purejs implementation should be used here that can run on
33+
* the server and in the browser.
34+
*
35+
* @see https://www.npmjs.com/package/fnv-plus
3236
*/
3337
export function hashStr(str: string): string {
34-
return String(farmhash.fingerprint32(normalizeStr(str)));
38+
// Avoid hashing empty strings and invalid types.
39+
if (!str || typeof str !== 'string') {
40+
throw new Error('input string is invalid');
41+
}
42+
return fnv.fast1a52hex(normalizeStr(str));
3543
}

pnpm-lock.yaml

Lines changed: 15 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)