Release Helper Tools 0.1.0

This commit is contained in:
2026-09-01 02:33:45 +02:00
commit 5fffd48642
78 changed files with 11635 additions and 0 deletions
+79
View File
@@ -0,0 +1,79 @@
import { describe, expect, it } from "vitest";
import {
adler32,
cidrContains,
digestHex,
encodeText,
fnv1a32,
formatChecksum,
formatIpv4,
formatIpv6,
parseCidr,
parseIpAddress,
parseIpv4,
parseIpv6,
crc32,
} from "../../src/helpers";
describe("checksums and cryptographic digests", () => {
it("matches published checksum vectors", async () => {
const input = encodeText("123456789");
expect(formatChecksum(crc32(input))).toBe("cbf43926");
expect(formatChecksum(adler32(input))).toBe("091e01de");
expect(formatChecksum(fnv1a32(encodeText("hello")))).toBe("4f9f2cab");
await expect(digestHex(encodeText("abc"), "SHA-256")).resolves.toBe(
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
);
});
it("applies byte ceilings before digest work", async () => {
expect(() => crc32(new Uint8Array(2), 1)).toThrow(/limit/u);
await expect(digestHex(new Uint8Array(2), "SHA-256", 1)).rejects.toThrow(
/limit/u,
);
});
});
describe("IPv4, IPv6, and CIDR", () => {
it("parses strict IPv4 and canonical IPv6", () => {
expect(parseIpv4("192.0.2.1")).toMatchObject({
version: 4,
canonical: "192.0.2.1",
value: 3221225985n,
});
expect(formatIpv4(parseIpv4("203.0.113.4").bytes)).toBe("203.0.113.4");
expect(parseIpv6("2001:0db8:0:0:0:ff00:0042:8329").canonical).toBe(
"2001:db8::ff00:42:8329",
);
expect(parseIpv6("::ffff:192.0.2.128").canonical).toBe("::ffff:c000:280");
expect(formatIpv6(1n)).toBe("::1");
expect(parseIpAddress("::1").version).toBe(6);
});
it("computes exact network ranges and containment", () => {
const ipv4 = parseCidr("192.0.2.129/25");
expect(ipv4).toMatchObject({
canonical: "192.0.2.128/25",
size: 128n,
});
expect(ipv4.first.canonical).toBe("192.0.2.128");
expect(ipv4.last.canonical).toBe("192.0.2.255");
expect(ipv4.broadcast?.canonical).toBe("192.0.2.255");
expect(cidrContains(ipv4, "192.0.2.200")).toBe(true);
expect(cidrContains(ipv4, "192.0.3.1")).toBe(false);
const ipv6 = parseCidr("2001:db8::1/126");
expect(ipv6.canonical).toBe("2001:db8::/126");
expect(ipv6.last.canonical).toBe("2001:db8::3");
expect(cidrContains(ipv6, "2001:db8::2")).toBe(true);
});
it("rejects ambiguous and malformed addresses", () => {
expect(() => parseIpv4("127.00.0.1")).toThrow(/octet/u);
expect(() => parseIpv4("256.0.0.1")).toThrow(/255/u);
expect(() => parseIpv6("1::2::3")).toThrow(/IPv6/u);
expect(() => parseIpv6("fe80::1%eth0")).toThrow(/scoped/u);
expect(() => parseIpv6("::192.0.2.1:192.0.2.2")).toThrow(/final/u);
expect(() => parseCidr("192.0.2.1/33")).toThrow(/32/u);
});
});
+106
View File
@@ -0,0 +1,106 @@
import { describe, expect, it } from "vitest";
import {
base64ToBytes,
base64UrlToBytes,
bytesToBase64,
bytesToBase64Url,
bytesToHex,
convertLineEndings,
decodeText,
decodeUrlComponent,
encodeText,
encodeUrlComponent,
fromCodePoints,
hexToBytes,
inspectCodePoints,
inspectUrl,
normalizeUnicode,
parseFormEncoded,
segmentGraphemes,
stringifyFormEncoded,
transformCase,
} from "../../src/helpers";
describe("binary and text encoding", () => {
it("round-trips canonical Base64, Base64URL, hex, and text encodings", () => {
const bytes = encodeText("Hello, 🌍", "utf-8");
expect(bytesToBase64(bytes)).toBe("SGVsbG8sIPCfjI0=");
expect(decodeText(base64ToBytes("SGVsbG8sIPCfjI0="))).toBe("Hello, 🌍");
expect(bytesToBase64Url(new Uint8Array([251, 255]), false)).toBe("-_8");
expect(base64UrlToBytes("-_8")).toEqual(new Uint8Array([251, 255]));
expect(base64UrlToBytes("Zg==")).toEqual(new Uint8Array([102]));
expect(bytesToHex(bytes).startsWith("48656c6c6f")).toBe(true);
expect(hexToBytes("0x00 ff", { allowWhitespace: true })).toEqual(
new Uint8Array([0, 255]),
);
expect(decodeText(encodeText("Aé", "latin1"), "latin1")).toBe("Aé");
expect(decodeText(encodeText("A🌍", "utf-16le"), "utf-16le")).toBe("A🌍");
expect(decodeText(encodeText("A🌍", "utf-16be"), "utf-16be")).toBe("A🌍");
});
it("rejects ambiguous, malformed, and oversized encodings", () => {
expect(() => base64ToBytes("Zg")).toThrow(/padding/u);
expect(() => base64ToBytes("Zh==")).toThrow(/unused|canonical/u);
expect(() => base64UrlToBytes("+w")).toThrow(/Invalid/u);
expect(() => base64UrlToBytes("Zg=")).toThrow(/padding/u);
expect(() => base64ToBytes("Z g==")).toThrow(/whitespace/u);
expect(() => hexToBytes("abc")).toThrow(/pairs/u);
expect(() => hexToBytes("ffff", { maxOutputBytes: 1 })).toThrow(/limit/u);
expect(() => encodeText("€", "latin1")).toThrow(/Latin-1/u);
});
});
describe("URL, Unicode, case, and line helpers", () => {
it("handles URL components and repeated form fields without object coercion", () => {
expect(decodeUrlComponent(encodeUrlComponent("a b/✓"))).toBe("a b/✓");
const entries = parseFormEncoded("tag=one&tag=two+words&empty=");
expect(entries).toEqual([
["tag", "one"],
["tag", "two words"],
["empty", ""],
]);
expect(stringifyFormEncoded(entries)).toBe("tag=one&tag=two+words&empty=");
expect(
inspectUrl("../a?q=1#x", "https://example.test/base/"),
).toMatchObject({
href: "https://example.test/a?q=1#x",
protocol: "https:",
hostname: "example.test",
passwordPresent: false,
});
});
it("inspects scalar values and user-perceived graphemes", () => {
const points = inspectCodePoints("A🌍");
expect(points).toEqual([
{
character: "A",
codePoint: 65,
hex: "U+0041",
utf16Index: 0,
utf16Length: 1,
},
{
character: "🌍",
codePoint: 0x1f30d,
hex: "U+1F30D",
utf16Index: 1,
utf16Length: 2,
},
]);
expect(fromCodePoints(points.map((point) => point.codePoint))).toBe("A🌍");
expect(segmentGraphemes("👨‍👩‍👧‍👦é")).toHaveLength(2);
expect(normalizeUnicode("e\u0301", "NFC")).toBe("é");
expect(() => fromCodePoints([0xd800])).toThrow(/scalar/u);
expect(fromCodePoints(new Array(100_000).fill(0x61))).toHaveLength(100_000);
});
it("transforms case and line endings deterministically", () => {
expect(transformCase("XML http value", "camel")).toBe("xmlHttpValue");
expect(transformCase("hello-world", "pascal")).toBe("HelloWorld");
expect(transformCase("helloWorld", "snake")).toBe("hello_world");
expect(convertLineEndings("a\r\nb\rc\n", "lf")).toBe("a\nb\nc\n");
expect(convertLineEndings("a\nb", "crlf")).toBe("a\r\nb");
});
});
+53
View File
@@ -0,0 +1,53 @@
import { describe, expect, it } from "vitest";
import {
convertDataUnit,
convertNumberBase,
convertUnit,
formatBigIntRadix,
parseBigIntRadix,
unitDimension,
} from "../../src/helpers";
describe("BigInt number bases", () => {
it("parses and formats exact large integers from base 2 through 36", () => {
expect(parseBigIntRadix("-0xFF_FF", 16)).toBe(-65_535n);
expect(convertNumberBase("11111111", 2, 16, { uppercase: true })).toBe(
"FF",
);
expect(
formatBigIntRadix(0x1234abcdn, 16, { prefix: true, groupSize: 4 }),
).toBe("0x1234_abcd");
expect(parseBigIntRadix("zz", 36)).toBe(1295n);
});
it("rejects invalid radices, digits, separators, and digit bombs", () => {
expect(() => parseBigIntRadix("2", 2)).toThrow(/Digit/u);
expect(() => parseBigIntRadix("10", 1)).toThrow(/Radix/u);
expect(() => parseBigIntRadix("_1", 10)).toThrow(/separator/u);
expect(() => parseBigIntRadix("1234", 10, { maxDigits: 3 })).toThrow(
/exceeds/u,
);
});
});
describe("unit conversion", () => {
it("distinguishes decimal and binary data units", () => {
expect(convertDataUnit(1, "MiB", "B")).toBe(1_048_576);
expect(convertDataUnit(1, "MB", "B")).toBe(1_000_000);
expect(convertDataUnit(8, "b", "B")).toBe(1);
expect(() => convertDataUnit(Number.MAX_VALUE, "TB", "b")).toThrow(
/finite number range/u,
);
});
it("converts compatible physical units and affine temperatures", () => {
expect(convertUnit(1, "mi", "km")).toBeCloseTo(1.609344, 12);
expect(convertUnit(1, "lb", "kg")).toBeCloseTo(0.45359237, 12);
expect(convertUnit(32, "F", "C")).toBeCloseTo(0, 12);
expect(convertUnit(0, "C", "K")).toBeCloseTo(273.15, 12);
expect(convertUnit(2, "week", "day")).toBe(14);
expect(unitDimension("cm")).toBe("length");
expect(() => convertUnit(1, "m", "kg")).toThrow(/Cannot convert/u);
});
});
@@ -0,0 +1,133 @@
import { describe, expect, it, vi } from "vitest";
import {
HelperLimitError,
assertBoundedBytes,
assertBoundedItems,
assertBoundedText,
createObjectUrlLease,
createSeededRandom,
sanitizeDownloadFilename,
secureRandomBytes,
secureRandomInt,
shuffleSeeded,
triggerBlobDownload,
} from "../../src/helpers";
describe("shared ceilings and download safety", () => {
it("reports actual and configured bounds", () => {
expect(() => assertBoundedText("123", 2)).toThrow(HelperLimitError);
expect(() => assertBoundedBytes(new Uint8Array(3), 2)).toThrow(/limit/u);
expect(() => assertBoundedItems(3, 2)).toThrow(/limit/u);
try {
assertBoundedText("123", 2, "Sample");
} catch (error) {
expect(error).toMatchObject({ actual: 3, limit: 2 });
}
});
it("sanitizes filenames and revokes object URLs exactly once", () => {
expect(sanitizeDownloadFilename("../bad:\0name?.txt")).toBe(
"_bad__name_.txt",
);
expect(sanitizeDownloadFilename("...", "../fallback?.txt")).toBe(
"_fallback_.txt",
);
expect(sanitizeDownloadFilename("CON.txt")).toBe("_CON.txt");
expect(sanitizeDownloadFilename("report\u202egnp.exe")).toBe(
"report_gnp.exe",
);
const emojiBoundary = sanitizeDownloadFilename(
`${"a".repeat(179)}😀.txt`,
"fallback.txt",
180,
);
expect(emojiBoundary).not.toMatch(/[\uD800-\uDBFF]$/u);
expect(emojiBoundary.length).toBeLessThanOrEqual(180);
expect(() => sanitizeDownloadFilename("x", "fallback", 0)).toThrow(
/positive/u,
);
const createObjectURL = vi.fn(() => "blob:test");
const revokeObjectURL = vi.fn();
const lease = createObjectUrlLease(new Blob(["x"]), {
createObjectURL,
revokeObjectURL,
});
expect(lease).toMatchObject({ url: "blob:test", revoked: false });
lease.revoke();
lease.revoke();
expect(lease.revoked).toBe(true);
expect(revokeObjectURL).toHaveBeenCalledOnce();
});
it("removes download anchors and revokes their URL on the next task", () => {
const originalUrl = globalThis.URL;
const createObjectURL = vi.fn(() => "blob:download");
const revokeObjectURL = vi.fn();
const click = vi
.spyOn(HTMLAnchorElement.prototype, "click")
.mockImplementation(() => undefined);
vi.stubGlobal("URL", { createObjectURL, revokeObjectURL });
vi.useFakeTimers();
try {
const lease = triggerBlobDownload(new Blob(["local"]), "../safe?.txt");
expect(click).toHaveBeenCalledOnce();
expect(click.mock.instances[0]).toMatchObject({
download: "_safe_.txt",
});
expect(document.querySelector('a[href="blob:download"]')).toBeNull();
expect(lease.revoked).toBe(false);
vi.runAllTimers();
expect(lease.revoked).toBe(true);
expect(revokeObjectURL).toHaveBeenCalledWith("blob:download");
} finally {
vi.useRealTimers();
vi.stubGlobal("URL", originalUrl);
}
});
});
describe("secure and seeded randomness", () => {
it("fills secure byte requests in Web Crypto-sized chunks", () => {
const getRandomValues = vi.fn(<T extends ArrayBufferView>(array: T) => {
new Uint8Array(array.buffer, array.byteOffset, array.byteLength).fill(7);
return array;
});
const cryptoSource = { getRandomValues } as unknown as Pick<
Crypto,
"getRandomValues"
>;
expect(secureRandomBytes(70_000, cryptoSource)).toEqual(
new Uint8Array(70_000).fill(7),
);
expect(getRandomValues).toHaveBeenCalledTimes(2);
expect(() => secureRandomBytes(2, cryptoSource, 1)).toThrow(/exceeds/u);
});
it("uses rejection sampling for bounded secure integers", () => {
const samples = [0xffffffff, 7];
const getRandomValues = <T extends ArrayBufferView>(array: T): T => {
new Uint32Array(array.buffer, array.byteOffset, 1)[0] =
samples.shift() ?? 0;
return array;
};
expect(secureRandomInt(10, 20, { getRandomValues })).toBe(17);
});
it("produces reproducible seeded primitives without mutating inputs", () => {
const first = createSeededRandom("repeatable");
const second = createSeededRandom("repeatable");
expect(Array.from({ length: 8 }, () => first.nextUint32())).toEqual(
Array.from({ length: 8 }, () => second.nextUint32()),
);
const input = [1, 2, 3, 4, 5];
const shuffled = shuffleSeeded(input, 42);
expect(shuffled).toEqual(shuffleSeeded(input, 42));
expect(shuffled).not.toEqual(input);
expect(input).toEqual([1, 2, 3, 4, 5]);
expect(createSeededRandom("bytes").bytes(7)).toHaveLength(7);
const value = createSeededRandom("float").nextFloat();
expect(value).toBeGreaterThanOrEqual(0);
expect(value).toBeLessThan(1);
});
});
+77
View File
@@ -0,0 +1,77 @@
import { describe, expect, it } from "vitest";
import {
formatDuration,
formatTimestamp,
parseCsv,
parseDuration,
parseTimestamp,
safeJsonParse,
stableStringify,
stringifyCsv,
} from "../../src/helpers";
describe("bounded structured data", () => {
it("parses hardened JSON and serializes deterministic key order", () => {
expect(safeJsonParse('{"b":2,"a":[true,null]}')).toEqual({
b: 2,
a: [true, null],
});
expect(stableStringify({ z: 1, a: { d: 4, c: 3 } }, 2)).toBe(
'{\n "a": {\n "c": 3,\n "d": 4\n },\n "z": 1\n}',
);
expect(() => safeJsonParse('{"__proto__":{"polluted":true}}')).toThrow(
/Dangerous/u,
);
expect(() => safeJsonParse("[[[0]]]", { maxDepth: 2 })).toThrow(/depth/u);
expect(() => safeJsonParse("[1,2,3]", { maxNodes: 3 })).toThrow(/node/u);
const cycle: Record<string, unknown> = {};
cycle.self = cycle;
expect(() => stableStringify(cycle)).toThrow(/cyclic/u);
expect(() => stableStringify({ value: 1n })).toThrow(/BigInt/u);
});
it("handles quoted CSV, embedded newlines, and round trips", () => {
const rows = [
["name", "note"],
["Ada", 'comma, quote " and\nnewline'],
["", "last"],
];
const csv = stringifyCsv(rows);
expect(csv).toContain('"comma, quote "" and\nnewline"');
expect(parseCsv(csv)).toEqual(rows);
expect(parseCsv("a;b\r\n1;2", { delimiter: ";" })).toEqual([
["a", "b"],
["1", "2"],
]);
expect(() => parseCsv('"unterminated')).toThrow(/unterminated/u);
expect(() => parseCsv("a,b,c", { maxColumns: 2 })).toThrow(/column/u);
});
});
describe("timestamps and durations", () => {
it("normalizes ISO and Unix timestamps", () => {
expect(parseTimestamp(0, "seconds")).toMatchObject({
epochMilliseconds: 0,
epochSeconds: 0,
iso: "1970-01-01T00:00:00.000Z",
});
expect(parseTimestamp("2000-01-01T00:00:00Z").epochSeconds).toBe(946684800);
expect(formatTimestamp(0, "UTC")).toMatch(/1970/u);
expect(() => parseTimestamp("not a date")).toThrow(/range/u);
});
it("parses ISO, clock, and token durations and formats them", () => {
expect(parseDuration("P1DT2H3M4.5S")).toBe(93_784_500);
expect(parseDuration("01:02:03.004")).toBe(3_723_004);
expect(parseDuration("2h 30m")).toBe(9_000_000);
expect(parseDuration("-2h 30m")).toBe(-9_000_000);
expect(parseDuration(formatDuration(-9_000_000, "human"))).toBe(-9_000_000);
expect(formatDuration(93_784_500, "iso")).toBe("P1DT2H3M4.5S");
expect(formatDuration(3_723_004, "clock")).toBe("01:02:03.004");
expect(formatDuration(9_000_000, "human")).toBe("2h 30m");
expect(formatDuration(86_400_000, "iso")).toBe("P1D");
expect(formatDuration(59_999.6, "clock")).toBe("00:01:00.000");
expect(() => parseDuration("1 month")).toThrow(/Duration/u);
});
});