mirror of
https://github.com/Nystik-gh/ignis.git
synced 2026-06-17 04:35:53 +00:00
update createHash implementation and add tests
This commit is contained in:
14
package-lock.json
generated
14
package-lock.json
generated
@@ -17,6 +17,7 @@
|
||||
"ws": "^8.16.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@noble/hashes": "^2.2.0",
|
||||
"esbuild": "^0.20.0",
|
||||
"esbuild-svelte": "^0.9.4",
|
||||
"lucide-svelte": "^0.577.0",
|
||||
@@ -537,6 +538,19 @@
|
||||
"@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": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"ws": "^8.16.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@noble/hashes": "^2.2.0",
|
||||
"esbuild": "^0.20.0",
|
||||
"esbuild-svelte": "^0.9.4",
|
||||
"lucide-svelte": "^0.577.0",
|
||||
|
||||
@@ -1,74 +1,88 @@
|
||||
export function createHash(algorithm) {
|
||||
const alg = algorithm.toUpperCase().replace("-", "");
|
||||
import { sha256, sha512 } from "@noble/hashes/sha2.js";
|
||||
import { sha1, md5 } from "@noble/hashes/legacy.js";
|
||||
|
||||
const subtleAlg =
|
||||
alg === "SHA256"
|
||||
? "SHA-256"
|
||||
: alg === "SHA1"
|
||||
? "SHA-1"
|
||||
: alg === "SHA512"
|
||||
? "SHA-512"
|
||||
: alg;
|
||||
const HASHERS = {
|
||||
SHA1: sha1,
|
||||
SHA256: sha256,
|
||||
SHA512: sha512,
|
||||
MD5: md5,
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
return {
|
||||
update(data) {
|
||||
if (typeof data === "string") {
|
||||
data = new TextEncoder().encode(data);
|
||||
}
|
||||
|
||||
const merged = new Uint8Array(inputData.length + data.length);
|
||||
const bytes =
|
||||
typeof data === "string" ? new TextEncoder().encode(data) : data;
|
||||
const merged = new Uint8Array(inputData.length + bytes.length);
|
||||
|
||||
merged.set(inputData);
|
||||
merged.set(data, inputData.length);
|
||||
|
||||
merged.set(bytes, inputData.length);
|
||||
inputData = merged;
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
digest(encoding) {
|
||||
console.warn("[shim:crypto] createHash.digest - using placeholder");
|
||||
|
||||
const hash = simpleHash(inputData);
|
||||
|
||||
if (encoding === "hex") {
|
||||
return hash;
|
||||
}
|
||||
|
||||
if (encoding === "base64") {
|
||||
return btoa(hash);
|
||||
}
|
||||
|
||||
return hash;
|
||||
return encode(hasher(inputData), encoding);
|
||||
},
|
||||
|
||||
async digestAsync(encoding) {
|
||||
const hashBuffer = await crypto.subtle.digest(subtleAlg, inputData);
|
||||
const hashArray = new Uint8Array(hashBuffer);
|
||||
const subtleAlg = SUBTLE_ALG[alg];
|
||||
|
||||
if (encoding === "hex") {
|
||||
return Array.from(hashArray)
|
||||
.map((b) => b.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
if (!subtleAlg) {
|
||||
// SubtleCrypto doesn't cover MD5; fall back to the sync hasher.
|
||||
return encode(hasher(inputData), encoding);
|
||||
}
|
||||
|
||||
if (encoding === "base64") {
|
||||
return btoa(String.fromCharCode(...hashArray));
|
||||
}
|
||||
|
||||
return hashArray;
|
||||
const buf = await crypto.subtle.digest(subtleAlg, inputData);
|
||||
return encode(new Uint8Array(buf), encoding);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
it("replacing an entry reflects the new size, not old + new", () => {
|
||||
it("replacing an entry reflects the new size", () => {
|
||||
const cache = new ContentCache(1024);
|
||||
cache.set("a.md", "short");
|
||||
cache.set("a.md", "a much longer string");
|
||||
|
||||
Reference in New Issue
Block a user