diff --git a/.changeset/fix-hex-signed-odd-length.md b/.changeset/fix-hex-signed-odd-length.md new file mode 100644 index 00000000..6d11efa7 --- /dev/null +++ b/.changeset/fix-hex-signed-odd-length.md @@ -0,0 +1,5 @@ +--- +"ox": patch +--- + +Fixed `Hex.toBigInt` and `Hex.toNumber` throwing on odd-length hex with `signed: true`, and `Hex.fromNumber` returning `0x0` for a negative number with `signed: true` and no `size`. diff --git a/src/core/Hex.ts b/src/core/Hex.ts index 0253f0e0..a8683edf 100644 --- a/src/core/Hex.ts +++ b/src/core/Hex.ts @@ -286,7 +286,9 @@ export function fromNumber( maxValue = BigInt(Number.MAX_SAFE_INTEGER) } - const minValue = typeof maxValue === 'bigint' && signed ? -maxValue - 1n : 0 + // Negative values need a `size` to encode as two's complement. + const minValue = + size && typeof maxValue === 'bigint' && signed ? -maxValue - 1n : 0 if ((maxValue && value_ > maxValue) || value_ < minValue) { const suffix = typeof value === 'bigint' ? 'n' : '' @@ -611,9 +613,7 @@ export function toBigInt(hex: Hex, options: toBigInt.Options = {}): bigint { const value = BigInt(hex) if (!signed) return value - const size = (hex.length - 2) / 2 - - const max_unsigned = (1n << (BigInt(size) * 8n)) - 1n + const max_unsigned = (1n << (BigInt(size(hex)) * 8n)) - 1n const max_signed = max_unsigned >> 1n if (value <= max_signed) return value diff --git a/src/core/_test/Hex.test.ts b/src/core/_test/Hex.test.ts index 184e89c7..9ccc32c8 100644 --- a/src/core/_test/Hex.test.ts +++ b/src/core/_test/Hex.test.ts @@ -221,6 +221,12 @@ describe('fromNumber', () => { ).toThrowErrorMatchingInlineSnapshot( '[Hex.IntegerOutOfRangeError: Number `-32769` is not in safe 16-bit signed integer range (`-32768` to `32767`)]', ) + expect(() => + // @ts-expect-error `signed` requires `size`. + Hex.fromNumber(-1, { signed: true }), + ).toThrowErrorMatchingInlineSnapshot( + '[Hex.IntegerOutOfRangeError: Number `-1` is not in safe signed integer range (`0` to `9007199254740991`)]', + ) }) }) @@ -404,6 +410,10 @@ describe('toBigInt', () => { expect(Hex.toBigInt('0x7fff', { signed: true })).toBe(32767n) expect(Hex.toBigInt('0x8000', { signed: true })).toBe(-32768n) + expect(Hex.toBigInt('0xf', { signed: true })).toBe(15n) + expect(Hex.toBigInt('0xfff', { signed: true })).toBe(4095n) + expect(Hex.toBigInt(Hex.fromNumber(420), { signed: true })).toBe(420n) + expect( Hex.toBigInt( '0x000000000000000000000000000000000000000000000000aade1ed08b0b325c',