update createHash implementation and add tests
This commit is contained in:
parent
c225f73859
commit
c1a169a3ed
5 changed files with 167 additions and 52 deletions
14
package-lock.json
generated
14
package-lock.json
generated
|
|
@ -17,6 +17,7 @@
|
||||||
"ws": "^8.16.0"
|
"ws": "^8.16.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@noble/hashes": "^2.2.0",
|
||||||
"esbuild": "^0.20.0",
|
"esbuild": "^0.20.0",
|
||||||
"esbuild-svelte": "^0.9.4",
|
"esbuild-svelte": "^0.9.4",
|
||||||
"lucide-svelte": "^0.577.0",
|
"lucide-svelte": "^0.577.0",
|
||||||
|
|
@ -537,6 +538,19 @@
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@noble/hashes": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20.19.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@pkgjs/parseargs": {
|
"node_modules/@pkgjs/parseargs": {
|
||||||
"version": "0.11.0",
|
"version": "0.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@
|
||||||
"ws": "^8.16.0"
|
"ws": "^8.16.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@noble/hashes": "^2.2.0",
|
||||||
"esbuild": "^0.20.0",
|
"esbuild": "^0.20.0",
|
||||||
"esbuild-svelte": "^0.9.4",
|
"esbuild-svelte": "^0.9.4",
|
||||||
"lucide-svelte": "^0.577.0",
|
"lucide-svelte": "^0.577.0",
|
||||||
|
|
|
||||||
|
|
@ -1,74 +1,88 @@
|
||||||
export function createHash(algorithm) {
|
import { sha256, sha512 } from "@noble/hashes/sha2.js";
|
||||||
const alg = algorithm.toUpperCase().replace("-", "");
|
import { sha1, md5 } from "@noble/hashes/legacy.js";
|
||||||
|
|
||||||
const subtleAlg =
|
const HASHERS = {
|
||||||
alg === "SHA256"
|
SHA1: sha1,
|
||||||
? "SHA-256"
|
SHA256: sha256,
|
||||||
: alg === "SHA1"
|
SHA512: sha512,
|
||||||
? "SHA-1"
|
MD5: md5,
|
||||||
: alg === "SHA512"
|
};
|
||||||
? "SHA-512"
|
|
||||||
: alg;
|
const SUBTLE_ALG = {
|
||||||
|
SHA1: "SHA-1",
|
||||||
|
SHA256: "SHA-256",
|
||||||
|
SHA512: "SHA-512",
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeAlgorithm(algorithm) {
|
||||||
|
return algorithm.toUpperCase().replace(/-/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function encode(bytes, encoding) {
|
||||||
|
if (!encoding) {
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (encoding === "hex") {
|
||||||
|
let hex = "";
|
||||||
|
|
||||||
|
for (let i = 0; i < bytes.length; i++) {
|
||||||
|
hex += bytes[i].toString(16).padStart(2, "0");
|
||||||
|
}
|
||||||
|
|
||||||
|
return hex;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (encoding === "base64") {
|
||||||
|
let binary = "";
|
||||||
|
|
||||||
|
for (let i = 0; i < bytes.length; i++) {
|
||||||
|
binary += String.fromCharCode(bytes[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return btoa(binary);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unsupported digest encoding: ${encoding}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createHash(algorithm) {
|
||||||
|
const alg = normalizeAlgorithm(algorithm);
|
||||||
|
const hasher = HASHERS[alg];
|
||||||
|
|
||||||
|
if (!hasher) {
|
||||||
|
throw new Error(`Unsupported hash algorithm: ${algorithm}`);
|
||||||
|
}
|
||||||
|
|
||||||
let inputData = new Uint8Array(0);
|
let inputData = new Uint8Array(0);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
update(data) {
|
update(data) {
|
||||||
if (typeof data === "string") {
|
const bytes =
|
||||||
data = new TextEncoder().encode(data);
|
typeof data === "string" ? new TextEncoder().encode(data) : data;
|
||||||
}
|
const merged = new Uint8Array(inputData.length + bytes.length);
|
||||||
|
|
||||||
const merged = new Uint8Array(inputData.length + data.length);
|
|
||||||
|
|
||||||
merged.set(inputData);
|
merged.set(inputData);
|
||||||
merged.set(data, inputData.length);
|
merged.set(bytes, inputData.length);
|
||||||
|
|
||||||
inputData = merged;
|
inputData = merged;
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
},
|
},
|
||||||
|
|
||||||
digest(encoding) {
|
digest(encoding) {
|
||||||
console.warn("[shim:crypto] createHash.digest - using placeholder");
|
return encode(hasher(inputData), encoding);
|
||||||
|
|
||||||
const hash = simpleHash(inputData);
|
|
||||||
|
|
||||||
if (encoding === "hex") {
|
|
||||||
return hash;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (encoding === "base64") {
|
|
||||||
return btoa(hash);
|
|
||||||
}
|
|
||||||
|
|
||||||
return hash;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async digestAsync(encoding) {
|
async digestAsync(encoding) {
|
||||||
const hashBuffer = await crypto.subtle.digest(subtleAlg, inputData);
|
const subtleAlg = SUBTLE_ALG[alg];
|
||||||
const hashArray = new Uint8Array(hashBuffer);
|
|
||||||
|
|
||||||
if (encoding === "hex") {
|
if (!subtleAlg) {
|
||||||
return Array.from(hashArray)
|
// SubtleCrypto doesn't cover MD5; fall back to the sync hasher.
|
||||||
.map((b) => b.toString(16).padStart(2, "0"))
|
return encode(hasher(inputData), encoding);
|
||||||
.join("");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (encoding === "base64") {
|
const buf = await crypto.subtle.digest(subtleAlg, inputData);
|
||||||
return btoa(String.fromCharCode(...hashArray));
|
return encode(new Uint8Array(buf), encoding);
|
||||||
}
|
|
||||||
|
|
||||||
return hashArray;
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function simpleHash(data) {
|
|
||||||
let hash = 0;
|
|
||||||
|
|
||||||
for (let i = 0; i < data.length; i++) {
|
|
||||||
hash = ((hash << 5) - hash + data[i]) | 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Math.abs(hash).toString(16).padStart(8, "0");
|
|
||||||
}
|
|
||||||
|
|
|
||||||
86
src/shims/crypto/create-hash.test.js
Normal file
86
src/shims/crypto/create-hash.test.js
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { createHash } from "./create-hash.js";
|
||||||
|
|
||||||
|
// "abc" / empty SHA digests: NIST FIPS 180-4 worked examples (SHA_All.pdf).
|
||||||
|
// MD5: RFC 1321 §A.5 test suite.
|
||||||
|
const VECTORS = {
|
||||||
|
SHA1: {
|
||||||
|
empty: "da39a3ee5e6b4b0d3255bfef95601890afd80709",
|
||||||
|
abc: "a9993e364706816aba3e25717850c26c9cd0d89d",
|
||||||
|
},
|
||||||
|
SHA256: {
|
||||||
|
empty: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||||
|
abc: "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
|
||||||
|
},
|
||||||
|
SHA512: {
|
||||||
|
empty:
|
||||||
|
"cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e",
|
||||||
|
abc: "ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f",
|
||||||
|
},
|
||||||
|
MD5: {
|
||||||
|
empty: "d41d8cd98f00b204e9800998ecf8427e",
|
||||||
|
abc: "900150983cd24fb0d6963f7d28e17f72",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("createHash", () => {
|
||||||
|
for (const [alg, vec] of Object.entries(VECTORS)) {
|
||||||
|
it(`${alg} digests "abc" correctly (hex)`, () => {
|
||||||
|
expect(createHash(alg).update("abc").digest("hex")).toBe(vec.abc);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
it("handles empty input (no update calls)", () => {
|
||||||
|
expect(createHash("sha256").digest("hex")).toBe(VECTORS.SHA256.empty);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("normalizes algorithm names (sha-256 -> SHA256)", () => {
|
||||||
|
expect(createHash("sha-256").update("abc").digest("hex")).toBe(
|
||||||
|
VECTORS.SHA256.abc,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("digest() with no encoding returns raw bytes", () => {
|
||||||
|
const result = createHash("sha256").update("abc").digest();
|
||||||
|
expect(result).toBeInstanceOf(Uint8Array);
|
||||||
|
expect(result.length).toBe(32);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("digest('base64') returns the base64 of the raw bytes", () => {
|
||||||
|
const result = createHash("sha256").update("abc").digest("base64");
|
||||||
|
expect(result).toBe("ungWv48Bz+pBQUDeXa4iI7ADYaOWF3qctBD/YfIAFa0=");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("supports multiple update() calls", () => {
|
||||||
|
const result = createHash("sha256")
|
||||||
|
.update("a")
|
||||||
|
.update("b")
|
||||||
|
.update("c")
|
||||||
|
.digest("hex");
|
||||||
|
expect(result).toBe(VECTORS.SHA256.abc);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws on unsupported algorithm", () => {
|
||||||
|
expect(() => createHash("whirlpool")).toThrow(/Unsupported hash algorithm/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws on unsupported encoding", () => {
|
||||||
|
expect(() => createHash("sha256").update("abc").digest("utf8")).toThrow(
|
||||||
|
/Unsupported digest encoding/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("digestAsync", () => {
|
||||||
|
it("SHA-256 async matches the sync result", async () => {
|
||||||
|
const h = createHash("sha256");
|
||||||
|
h.update("abc");
|
||||||
|
expect(await h.digestAsync("hex")).toBe(VECTORS.SHA256.abc);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("MD5 async falls back to the sync hasher (SubtleCrypto doesn't support it)", async () => {
|
||||||
|
const h = createHash("md5");
|
||||||
|
h.update("abc");
|
||||||
|
expect(await h.digestAsync("hex")).toBe(VECTORS.MD5.abc);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -17,7 +17,7 @@ describe("ContentCache size accounting", () => {
|
||||||
expect(cache.currentBytes).toBe(0);
|
expect(cache.currentBytes).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("replacing an entry reflects the new size, not old + new", () => {
|
it("replacing an entry reflects the new size", () => {
|
||||||
const cache = new ContentCache(1024);
|
const cache = new ContentCache(1024);
|
||||||
cache.set("a.md", "short");
|
cache.set("a.md", "short");
|
||||||
cache.set("a.md", "a much longer string");
|
cache.set("a.md", "a much longer string");
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue