From 7b38cb403aa33de218f647bf49670d8941ba6181 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Sat, 22 Apr 2023 13:43:11 +0200 Subject: [PATCH 1/8] feat: `asyncHash` with native sha256 digest and fallback --- benchmark/_utils.mjs | 16 ++++++++++++++++ benchmark/hash.mjs | 36 ++++++++++++++++++++++++++++++++++++ benchmark/object-hash.mjs | 35 ++++++++++------------------------- package.json | 6 +++++- pnpm-lock.yaml | 16 ++++++++++++++++ src/hash.ts | 15 +++++++++++++++ src/index.ts | 2 +- test/index.test.ts | 14 +++++++++++++- 8 files changed, 112 insertions(+), 28 deletions(-) create mode 100644 benchmark/_utils.mjs create mode 100644 benchmark/hash.mjs diff --git a/benchmark/_utils.mjs b/benchmark/_utils.mjs new file mode 100644 index 0000000..4caf13f --- /dev/null +++ b/benchmark/_utils.mjs @@ -0,0 +1,16 @@ +export function generateItems(num) { + return new Array(num).fill(0).map(() => { + return { + propNum: Math.random(), + propBool: Math.random() > 0.5, + propString: Math.random().toString(16), + propDate: new Date(), + propObj: { + propNum: Math.random(), + propBool: Math.random() > 0.5, + propString: Math.random().toString(16), + propDate: new Date(), + }, + }; + }); +} diff --git a/benchmark/hash.mjs b/benchmark/hash.mjs new file mode 100644 index 0000000..f13591f --- /dev/null +++ b/benchmark/hash.mjs @@ -0,0 +1,36 @@ +import Benchmark from "benchmark"; +import { hash, asyncHash } from "ohash"; +import largeJson from "./fixture/large.mjs"; +import { generateItems } from "./_utils.mjs"; + +const dataSets = { + emptyObject: {}, + singleObject: generateItems(1)[0], + tinyArray: generateItems(10), + mediumArray: generateItems(100), + largeArray: generateItems(1000), + largeJson, +}; + +const suite = new Benchmark.Suite(); + +for (const [name, data] of Object.entries(dataSets)) { + suite.add(`hash(${name})`, () => { + hash(data); + }); + suite.add(`asyncHash(${name})`, async () => { + await asyncHash(data); + }); +} + +suite + // add listeners + .on("cycle", function (event) { + console.log(event.target.toString()); + }) + .on("complete", function () { + console.log("Fastest is " + this.filter("fastest").map("name")); + }) + .run({ + async: false, + }); diff --git a/benchmark/object-hash.mjs b/benchmark/object-hash.mjs index 6c0a770..c179ecf 100644 --- a/benchmark/object-hash.mjs +++ b/benchmark/object-hash.mjs @@ -1,51 +1,36 @@ import Benchmark from "benchmark"; import { objectHash } from "ohash"; -import largeJson from './fixture/large.mjs' +import largeJson from "./fixture/large.mjs"; +import { generateItems } from "./_utils.mjs"; -function generateItems(num) { - return new Array(num).fill(0).map(() => { - return { - propNum: Math.random(), - propBool: Math.random() > 0.5, - propString: Math.random().toString(16), - propDate: new Date(), - propObj: { - propNum: Math.random(), - propBool: Math.random() > 0.5, - propString: Math.random().toString(16), - propDate: new Date(), - }, - }; - }); -} - -const suite = new Benchmark.Suite(); const singleObject = generateItems(1)[0]; const tinyArray = generateItems(10); const mediumArray = generateItems(100); const largeArray = generateItems(1000); -suite.add("hash({})", function () { +const suite = new Benchmark.Suite(); + +suite.add("objectHash({})", function () { const v = objectHash({}); }); -suite.add("hash(singleObject)", function () { +suite.add("objectHash(singleObject)", function () { const v = objectHash(singleObject); }); -suite.add("hash(tinyArray)", function () { +suite.add("objectHash(tinyArray)", function () { const v = objectHash(tinyArray); }); -suite.add("hash(mediumArray)", function () { +suite.add("objectHash(mediumArray)", function () { const v = objectHash(mediumArray); }); -suite.add("hash(largeArray)", function () { +suite.add("objectHash(largeArray)", function () { const v = objectHash(largeArray); }); -suite.add("hash(largeJson)", function () { +suite.add("objectHash(largeJson)", function () { const v = objectHash(largeJson); }); diff --git a/package.json b/package.json index ea7a9ba..b03d3bf 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "test": "pnpm lint && vitest run" }, "devDependencies": { + "@types/benchmark": "^2.1.2", "@types/node": "^18.15.13", "@vitest/coverage-c8": "^0.30.1", "benchmark": "^2.1.4", @@ -42,5 +43,8 @@ "unbuild": "^1.2.1", "vitest": "^0.30.1" }, - "packageManager": "pnpm@8.3.1" + "packageManager": "pnpm@8.3.1", + "dependencies": { + "uncrypto": "^0.1.2" + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1b6d0e8..c49710c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,6 +1,14 @@ lockfileVersion: '6.0' +dependencies: + uncrypto: + specifier: ^0.1.2 + version: 0.1.2 + devDependencies: + '@types/benchmark': + specifier: ^2.1.2 + version: 2.1.2 '@types/node': specifier: ^18.15.13 version: 18.15.13 @@ -667,6 +675,10 @@ packages: rollup: 3.20.6 dev: true + /@types/benchmark@2.1.2: + resolution: {integrity: sha512-EDKtLYNMKrig22jEvhXq8TBFyFgVNSPmDF2b9UzJ7+eylPqdZVo17PCUMkn1jP6/1A/0u78VqYC6VrX6b8pDWA==} + dev: true + /@types/chai-subset@1.3.3: resolution: {integrity: sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==} dependencies: @@ -3646,6 +3658,10 @@ packages: - supports-color dev: true + /uncrypto@0.1.2: + resolution: {integrity: sha512-kuZwRKV615lEw/Xx3Iz56FKk3nOeOVGaVmw0eg+x4Mne28lCotNFbBhDW7dEBCBKyKbRQiCadEZeNAFPVC5cgw==} + dev: false + /universalify@2.0.0: resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} engines: {node: '>= 10.0.0'} diff --git a/src/hash.ts b/src/hash.ts index 2890b3f..2a7e750 100644 --- a/src/hash.ts +++ b/src/hash.ts @@ -13,3 +13,18 @@ export function hash(object: any, options: HashOptions = {}): string { typeof object === "string" ? object : objectHash(object, options); return sha256base64(hashed).slice(0, 10); } + +export async function asyncHash( + object: any, + options: HashOptions = {} +): Promise { + if (!globalThis.crypto?.subtle?.digest) { + return hash(object, options); + } + const hashed = + typeof object === "string" ? object : objectHash(object, options); + const encoded = new TextEncoder().encode(hashed); + const digest = await globalThis.crypto?.subtle?.digest("SHA-256", encoded); + const b64Digest = btoa(String.fromCharCode(...new Uint8Array(digest))); + return b64Digest.toString().slice(0, 10); +} diff --git a/src/index.ts b/src/index.ts index bde8db8..aa1c47b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ export { objectHash } from "./object-hash"; -export { hash } from "./hash"; +export { hash, asyncHash } from "./hash"; export { murmurHash } from "./crypto/murmur"; export { sha256, sha256base64 } from "./crypto/sha256"; export { isEqual } from "./utils"; diff --git a/test/index.test.ts b/test/index.test.ts index 20d3fdf..6ae03e5 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -1,5 +1,13 @@ import { describe, expect, it } from "vitest"; -import { murmurHash, objectHash, hash, sha256, isEqual, diff } from "../src"; +import { + murmurHash, + objectHash, + hash, + sha256, + isEqual, + diff, + asyncHash, +} from "../src"; import { sha256base64 } from "../src/crypto/sha256"; describe("objectHash", () => { @@ -60,6 +68,10 @@ it("hash", () => { expect(hash({ foo: "bar" })).toMatchInlineSnapshot('"dZbtA7f0lK"'); }); +it("asyncHash", async () => { + expect(await asyncHash({ foo: "bar" })).toMatchInlineSnapshot('"dZbtA7f0lK"'); +}); + describe("isEqual", () => { const cases = [ [{ foo: "bar" }, { foo: "bar" }, true], From 57485e3b59b1d0f97266f629c1b50bad4ab45157 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Sat, 22 Apr 2023 13:46:40 +0200 Subject: [PATCH 2/8] revert package.json --- package.json | 5 +---- pnpm-lock.yaml | 9 --------- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/package.json b/package.json index b03d3bf..5a5f827 100644 --- a/package.json +++ b/package.json @@ -43,8 +43,5 @@ "unbuild": "^1.2.1", "vitest": "^0.30.1" }, - "packageManager": "pnpm@8.3.1", - "dependencies": { - "uncrypto": "^0.1.2" - } + "packageManager": "pnpm@8.3.1" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c49710c..98f9dd5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,10 +1,5 @@ lockfileVersion: '6.0' -dependencies: - uncrypto: - specifier: ^0.1.2 - version: 0.1.2 - devDependencies: '@types/benchmark': specifier: ^2.1.2 @@ -3658,10 +3653,6 @@ packages: - supports-color dev: true - /uncrypto@0.1.2: - resolution: {integrity: sha512-kuZwRKV615lEw/Xx3Iz56FKk3nOeOVGaVmw0eg+x4Mne28lCotNFbBhDW7dEBCBKyKbRQiCadEZeNAFPVC5cgw==} - dev: false - /universalify@2.0.0: resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} engines: {node: '>= 10.0.0'} From 30eb57e0ed61aa9b843caae623b23a973fe51f43 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Sat, 22 Apr 2023 14:02:42 +0200 Subject: [PATCH 3/8] update --- benchmark/hash.mjs | 20 +++++++++----------- package.json | 5 ++++- pnpm-lock.yaml | 9 +++++++++ src/hash.ts | 18 ++++++++++++++---- 4 files changed, 36 insertions(+), 16 deletions(-) diff --git a/benchmark/hash.mjs b/benchmark/hash.mjs index f13591f..9f4979e 100644 --- a/benchmark/hash.mjs +++ b/benchmark/hash.mjs @@ -18,19 +18,17 @@ for (const [name, data] of Object.entries(dataSets)) { suite.add(`hash(${name})`, () => { hash(data); }); - suite.add(`asyncHash(${name})`, async () => { - await asyncHash(data); - }); + suite.add( + `asyncHash(${name})`, + (ctx) => { + asyncHash(data).then(() => ctx.resolve()); + }, + { defer: true } + ); } suite - // add listeners - .on("cycle", function (event) { + .on("cycle", (event) => { console.log(event.target.toString()); }) - .on("complete", function () { - console.log("Fastest is " + this.filter("fastest").map("name")); - }) - .run({ - async: false, - }); + .run(); diff --git a/package.json b/package.json index 5a5f827..b03d3bf 100644 --- a/package.json +++ b/package.json @@ -43,5 +43,8 @@ "unbuild": "^1.2.1", "vitest": "^0.30.1" }, - "packageManager": "pnpm@8.3.1" + "packageManager": "pnpm@8.3.1", + "dependencies": { + "uncrypto": "^0.1.2" + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 98f9dd5..c49710c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,5 +1,10 @@ lockfileVersion: '6.0' +dependencies: + uncrypto: + specifier: ^0.1.2 + version: 0.1.2 + devDependencies: '@types/benchmark': specifier: ^2.1.2 @@ -3653,6 +3658,10 @@ packages: - supports-color dev: true + /uncrypto@0.1.2: + resolution: {integrity: sha512-kuZwRKV615lEw/Xx3Iz56FKk3nOeOVGaVmw0eg+x4Mne28lCotNFbBhDW7dEBCBKyKbRQiCadEZeNAFPVC5cgw==} + dev: false + /universalify@2.0.0: resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} engines: {node: '>= 10.0.0'} diff --git a/src/hash.ts b/src/hash.ts index 2a7e750..8c204f9 100644 --- a/src/hash.ts +++ b/src/hash.ts @@ -1,3 +1,4 @@ +import { subtle } from "uncrypto"; import { objectHash, HashOptions } from "./object-hash"; import { sha256base64 } from "./crypto/sha256"; @@ -18,13 +19,22 @@ export async function asyncHash( object: any, options: HashOptions = {} ): Promise { - if (!globalThis.crypto?.subtle?.digest) { + if (!subtle.digest) { return hash(object, options); } const hashed = typeof object === "string" ? object : objectHash(object, options); const encoded = new TextEncoder().encode(hashed); - const digest = await globalThis.crypto?.subtle?.digest("SHA-256", encoded); - const b64Digest = btoa(String.fromCharCode(...new Uint8Array(digest))); - return b64Digest.toString().slice(0, 10); + const digest = await subtle.digest("SHA-256", encoded); + return _arrayBufferToBase64(digest).slice(0, 10); +} + +function _arrayBufferToBase64(buffer: ArrayBuffer) { + let binary = ""; + const bytes = new Uint8Array(buffer); + const len = bytes.byteLength; + for (let i = 0; i < len; i++) { + binary += String.fromCharCode(bytes[i]); + } + return btoa(binary); } From 930c4d3362e7e8a88872d1a675521203879d0387 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Sat, 22 Apr 2023 14:04:49 +0200 Subject: [PATCH 4/8] bench: disable async --- benchmark/hash.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmark/hash.mjs b/benchmark/hash.mjs index 9f4979e..445c1cb 100644 --- a/benchmark/hash.mjs +++ b/benchmark/hash.mjs @@ -31,4 +31,4 @@ suite .on("cycle", (event) => { console.log(event.target.toString()); }) - .run(); + .run({ async: false }); From 845d0be92931a4510928ae11566dacf4c45a73c8 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 15 Sep 2024 10:06:26 +0000 Subject: [PATCH 5/8] chore: apply automated updates --- src/hash.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hash.ts b/src/hash.ts index 8c204f9..a8f2ec0 100644 --- a/src/hash.ts +++ b/src/hash.ts @@ -17,7 +17,7 @@ export function hash(object: any, options: HashOptions = {}): string { export async function asyncHash( object: any, - options: HashOptions = {} + options: HashOptions = {}, ): Promise { if (!subtle.digest) { return hash(object, options); From bc5b848bfff02320260acecfa0d35c627104f9cf Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Sun, 15 Sep 2024 14:50:29 +0200 Subject: [PATCH 6/8] avoid uncrypto dep --- src/hash.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/hash.ts b/src/hash.ts index a8f2ec0..81edb5d 100644 --- a/src/hash.ts +++ b/src/hash.ts @@ -1,4 +1,3 @@ -import { subtle } from "uncrypto"; import { objectHash, HashOptions } from "./object-hash"; import { sha256base64 } from "./crypto/sha256"; @@ -19,13 +18,13 @@ export async function asyncHash( object: any, options: HashOptions = {}, ): Promise { - if (!subtle.digest) { + if (!globalThis.crypto?.subtle.digest) { return hash(object, options); } const hashed = typeof object === "string" ? object : objectHash(object, options); const encoded = new TextEncoder().encode(hashed); - const digest = await subtle.digest("SHA-256", encoded); + const digest = await globalThis.crypto?.subtle.digest("SHA-256", encoded); return _arrayBufferToBase64(digest).slice(0, 10); } From 42c1e251f5f91fddd27cdb0bb8752f0d46cef0a5 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Sun, 15 Sep 2024 15:01:23 +0200 Subject: [PATCH 7/8] update --- README.md | 11 +++++++---- benchmark/hash.mjs | 8 ++++---- src/hash.ts | 2 +- src/index.ts | 2 +- test/index.test.ts | 6 +++--- 5 files changed, 16 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 4412ef5..091f67e 100644 --- a/README.md +++ b/README.md @@ -27,23 +27,26 @@ Import: ```js // ESM -import { hash, objectHash, murmurHash, sha256 } from "ohash"; +import { hash, hashAsync, objectHash, murmurHash, sha256 } from "ohash"; // CommonJS -const { hash, objectHash, murmurHash, sha256 } = require("ohash"); +const { hash, hashAsync, objectHash, murmurHash, sha256 } = require("ohash"); ``` -### `hash(object, options?)` +### `hash(object, options?)` / `hashAsync(object, options?)` Converts object value into a string hash using `objectHash` and then applies `sha256` with Base64 encoding (trimmed by length of 10). +`hashAsync` is slightly faster as will leverage [`SubtleCrypto.digest`](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest) when available. + Usage: ```js -import { hash } from "ohash"; +import { hash, hashAsync } from "ohash"; // "dZbtA7f0lK" console.log(hash({ foo: "bar" })); +console.log(await hashAsync({ foo: "bar" })); ``` ### `objectHash(object, options?)` diff --git a/benchmark/hash.mjs b/benchmark/hash.mjs index 445c1cb..0c32c44 100644 --- a/benchmark/hash.mjs +++ b/benchmark/hash.mjs @@ -1,5 +1,5 @@ import Benchmark from "benchmark"; -import { hash, asyncHash } from "ohash"; +import { hash, hashAsync } from "ohash"; import largeJson from "./fixture/large.mjs"; import { generateItems } from "./_utils.mjs"; @@ -19,11 +19,11 @@ for (const [name, data] of Object.entries(dataSets)) { hash(data); }); suite.add( - `asyncHash(${name})`, + `hashAsync(${name})`, (ctx) => { - asyncHash(data).then(() => ctx.resolve()); + hashAsync(data).then(() => ctx.resolve()); }, - { defer: true } + { defer: true }, ); } diff --git a/src/hash.ts b/src/hash.ts index 81edb5d..745d059 100644 --- a/src/hash.ts +++ b/src/hash.ts @@ -14,7 +14,7 @@ export function hash(object: any, options: HashOptions = {}): string { return sha256base64(hashed).slice(0, 10); } -export async function asyncHash( +export async function hashAsync( object: any, options: HashOptions = {}, ): Promise { diff --git a/src/index.ts b/src/index.ts index aa1c47b..d093199 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ export { objectHash } from "./object-hash"; -export { hash, asyncHash } from "./hash"; +export { hash, hashAsync } from "./hash"; export { murmurHash } from "./crypto/murmur"; export { sha256, sha256base64 } from "./crypto/sha256"; export { isEqual } from "./utils"; diff --git a/test/index.test.ts b/test/index.test.ts index de1e42f..2696f7d 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -6,7 +6,7 @@ import { sha256, isEqual, diff, - asyncHash, + hashAsync, } from "../src"; import { sha256base64 } from "../src/crypto/sha256"; @@ -100,8 +100,8 @@ it("hash", () => { expect(hash({ foo: "bar" })).toMatchInlineSnapshot('"dZbtA7f0lK"'); }); -it("asyncHash", async () => { - expect(await asyncHash({ foo: "bar" })).toMatchInlineSnapshot('"dZbtA7f0lK"'); +it("hashAsync", async () => { + expect(await hashAsync({ foo: "bar" })).toMatchInlineSnapshot('"dZbtA7f0lK"'); }); describe("isEqual", () => { From fe1a61c624b391334ce5b133818cc62b6b843196 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Sun, 15 Sep 2024 21:06:17 +0200 Subject: [PATCH 8/8] add sha256 async utils --- README.md | 10 ++++++---- src/crypto/sha256.ts | 47 ++++++++++++++++++++++++++++++++++++++++++++ src/hash.ts | 28 ++++++++++---------------- src/index.ts | 7 ++++++- test/index.test.ts | 22 ++++++++++++++++++++- 5 files changed, 90 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 091f67e..eabf3f4 100644 --- a/README.md +++ b/README.md @@ -124,26 +124,28 @@ import { murmurHash } from "ohash"; console.log(murmurHash("Hello World")); ``` -### `sha256` +### `sha256` / `sha256Async` Create a secure [SHA 256](https://en.wikipedia.org/wiki/SHA-2) digest from input string. ```js -import { sha256 } from "ohash"; +import { sha256, sha256Async } from "ohash"; // "a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e" console.log(sha256("Hello World")); +console.log(await sha256Async("Hello World")); ``` -### `sha256base64` +### `sha256base64` / `sha256base64Async` Create a secure [SHA 256](https://en.wikipedia.org/wiki/SHA-2) digest in Base64 encoding from input string. ```js -import { sha256base64 } from "ohash"; +import { sha256base64, sha256base64Async } from "ohash"; // "pZGm1Av0IEBKARczz7exkNYsZb8LzaMrV7J32a2fFG4" console.log(sha256base64("Hello World")); +console.log(await sha256base64Async("Hello World")); ``` ## 💻 Development diff --git a/src/crypto/sha256.ts b/src/crypto/sha256.ts index d56f306..76f54a5 100644 --- a/src/crypto/sha256.ts +++ b/src/crypto/sha256.ts @@ -133,6 +133,53 @@ export function sha256(message: string) { return new SHA256().finalize(message).toString(); } +export function sha256Async(message: string) { + if (!globalThis.crypto?.subtle?.digest) { + return new SHA256().finalize(message).toString(); + } + return globalThis.crypto.subtle + .digest("SHA-256", new TextEncoder().encode(message)) + .then((digest) => arrayBufferToHex(digest)); +} + export function sha256base64(message: string) { return new SHA256().finalize(message).toString(Base64); } + +export function sha256base64Async(message: string) { + if (!globalThis.crypto?.subtle?.digest) { + return Promise.resolve(sha256base64(message)); + } + return globalThis.crypto.subtle + .digest("SHA-256", new TextEncoder().encode(message)) + .then((digest) => arrayBufferToBase64(digest)); +} + +// --- internal --- + +function arrayBufferToBase64(buffer: ArrayBuffer) { + if (globalThis.Buffer) { + return globalThis.Buffer.from(buffer) + .toString("base64") + .replace(/[+/=]/g, ""); + } + let binary = ""; + const bytes = new Uint8Array(buffer); + const len = bytes.byteLength; + for (let i = 0; i < len; i++) { + binary += String.fromCharCode(bytes[i]); + } + return btoa(binary).replace(/[+/=]/g, ""); +} + +function arrayBufferToHex(buffer: ArrayBuffer) { + if (globalThis.Buffer) { + return globalThis.Buffer.from(buffer).toString("hex"); + } + const bytes = new Uint8Array(buffer); + const hex = []; + for (const byte of bytes) { + hex.push(byte.toString(16).padStart(2, "0")); + } + return hex.join(""); +} diff --git a/src/hash.ts b/src/hash.ts index 745d059..476095d 100644 --- a/src/hash.ts +++ b/src/hash.ts @@ -1,5 +1,5 @@ import { objectHash, HashOptions } from "./object-hash"; -import { sha256base64 } from "./crypto/sha256"; +import { sha256base64, sha256base64Async } from "./crypto/sha256"; /** * Hash any JS value into a string @@ -14,26 +14,18 @@ export function hash(object: any, options: HashOptions = {}): string { return sha256base64(hashed).slice(0, 10); } -export async function hashAsync( +/** + * Hash any JS value into a string + * @param {object} object value to hash + * @param {HashOptions} options hashing options + * @return {Promise} hash value + * @api public + */ +export function hashAsync( object: any, options: HashOptions = {}, ): Promise { - if (!globalThis.crypto?.subtle.digest) { - return hash(object, options); - } const hashed = typeof object === "string" ? object : objectHash(object, options); - const encoded = new TextEncoder().encode(hashed); - const digest = await globalThis.crypto?.subtle.digest("SHA-256", encoded); - return _arrayBufferToBase64(digest).slice(0, 10); -} - -function _arrayBufferToBase64(buffer: ArrayBuffer) { - let binary = ""; - const bytes = new Uint8Array(buffer); - const len = bytes.byteLength; - for (let i = 0; i < len; i++) { - binary += String.fromCharCode(bytes[i]); - } - return btoa(binary); + return sha256base64Async(hashed).then((digest) => digest.slice(0, 10)); } diff --git a/src/index.ts b/src/index.ts index d093199..fdd6066 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,11 @@ export { objectHash } from "./object-hash"; export { hash, hashAsync } from "./hash"; export { murmurHash } from "./crypto/murmur"; -export { sha256, sha256base64 } from "./crypto/sha256"; +export { + sha256, + sha256Async, + sha256base64, + sha256base64Async, +} from "./crypto/sha256"; export { isEqual } from "./utils"; export { diff } from "./diff"; diff --git a/test/index.test.ts b/test/index.test.ts index 2696f7d..5418796 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -4,11 +4,13 @@ import { objectHash, hash, sha256, + sha256Async, + sha256base64, + sha256base64Async, isEqual, diff, hashAsync, } from "../src"; -import { sha256base64 } from "../src/crypto/sha256"; describe("objectHash", () => { it("basic object", () => { @@ -87,6 +89,15 @@ it("sha256", () => { ); }); +it("sha256Async", async () => { + expect(await sha256Async("Hello World")).toMatchInlineSnapshot( + '"a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e"', + ); + expect(await sha256Async("")).toMatchInlineSnapshot( + '"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"', + ); +}); + it("sha256base64", () => { expect(sha256base64("Hello World")).toMatchInlineSnapshot( '"pZGm1Av0IEBKARczz7exkNYsZb8LzaMrV7J32a2fFG4"', @@ -96,6 +107,15 @@ it("sha256base64", () => { ); }); +it("sha256base64Async", async () => { + expect(await sha256base64Async("Hello World")).toMatchInlineSnapshot( + '"pZGm1Av0IEBKARczz7exkNYsZb8LzaMrV7J32a2fFG4"', + ); + expect(await sha256base64Async("")).toMatchInlineSnapshot( + '"47DEQpj8HBSaTImW5JCeuQeRkm5NMpJWZG3hSuFU"', + ); +}); + it("hash", () => { expect(hash({ foo: "bar" })).toMatchInlineSnapshot('"dZbtA7f0lK"'); });