diff --git a/packages/ans/src/bytes.test.ts b/packages/ans/src/bytes.test.ts new file mode 100644 index 0000000..ee1dfb0 --- /dev/null +++ b/packages/ans/src/bytes.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from 'vitest'; +import { fromHex, toHex } from './bytes.js'; + +describe('fromHex', () => { + it('decodes valid hex, with and without 0x prefix', () => { + expect([...fromHex('00ff10')]).toEqual([0x00, 0xff, 0x10]); + expect([...fromHex('0xdeadbeef')]).toEqual([0xde, 0xad, 0xbe, 0xef]); + expect([...fromHex('')]).toEqual([]); + }); + + it('round-trips with toHex', () => { + const bytes = Uint8Array.from([0, 1, 127, 128, 255]); + expect([...fromHex(toHex(bytes))]).toEqual([...bytes]); + }); + + it('throws on odd-length input', () => { + expect(() => fromHex('abc')).toThrow(/odd-length/); + }); + + it('throws on non-hex characters instead of silently decoding to 0', () => { + // Previously parseInt('zz', 16) === NaN, which a Uint8Array stores as 0, + // so fromHex('zzzz') silently returned [0, 0]. + expect(() => fromHex('zzzz')).toThrow(/invalid hex/); + expect(() => fromHex('00gg')).toThrow(/invalid hex/); + expect(() => fromHex('0xnothex')).toThrow(/invalid hex/); + }); +}); diff --git a/packages/ans/src/bytes.ts b/packages/ans/src/bytes.ts index 8cc809f..058df41 100644 --- a/packages/ans/src/bytes.ts +++ b/packages/ans/src/bytes.ts @@ -26,6 +26,9 @@ export function toHex(bytes: Uint8Array): string { export function fromHex(hex: string): Uint8Array { const clean = hex.startsWith('0x') ? hex.slice(2) : hex; if (clean.length % 2 !== 0) throw new Error('odd-length hex string'); + // Reject non-hex input. Without this, parseInt returns NaN for a bad pair + // and the Uint8Array silently stores 0, producing wrong bytes. + if (!/^[0-9a-fA-F]*$/.test(clean)) throw new Error('invalid hex string'); const out = new Uint8Array(clean.length / 2); for (let i = 0; i < out.length; i++) out[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16); return out;