Skip to content

Commit 3649036

Browse files
Fixing bugs
1 parent 6a70a60 commit 3649036

File tree

4 files changed

+131
-87
lines changed

4 files changed

+131
-87
lines changed

src/primitives/Hash.ts

Lines changed: 31 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11

22
// @ts-nocheck
33
/* eslint-disable @typescript-eslint/naming-convention */
4+
import { assertValidHex, normalizeHex } from './hex.js'
5+
46
const assert = (
57
expression: unknown,
68
message: string = 'Hash assertion failed'
@@ -168,42 +170,48 @@ abstract class BaseHash {
168170
* @returns Returns an array denoting the padding.
169171
*/
170172
private _pad (): number[] {
171-
const len = this.pendingTotal
173+
//
174+
let len = this.pendingTotal
172175
const bytes = this._delta8
173176
const k = bytes - ((len + this.padLength) % bytes)
174177
const res = new Array(k + this.padLength)
175178
res[0] = 0x80
176-
let i: number
179+
let i
177180
for (i = 1; i < k; i++) {
178181
res[i] = 0
179182
}
180183

181184
// Append length
182-
const lengthBytes = this.padLength
183-
const maxBits = 1n << BigInt(lengthBytes * 8)
184-
let totalBits = BigInt(len) * 8n
185-
186-
if (totalBits >= maxBits) {
187-
throw new Error('Message too long for this hash function')
188-
}
189-
185+
len <<= 3
186+
let t
190187
if (this.endian === 'big') {
191-
const lenArray = new Array<number>(lengthBytes)
192-
193-
for (let b = lengthBytes - 1; b >= 0; b--) {
194-
lenArray[b] = Number(totalBits & 0xffn)
195-
totalBits >>= 8n
188+
for (t = 8; t < this.padLength; t++) {
189+
res[i++] = 0
196190
}
197191

198-
for (let b = 0; b < lengthBytes; b++) {
199-
res[i++] = lenArray[b]
200-
}
192+
res[i++] = 0
193+
res[i++] = 0
194+
res[i++] = 0
195+
res[i++] = 0
196+
res[i++] = (len >>> 24) & 0xff
197+
res[i++] = (len >>> 16) & 0xff
198+
res[i++] = (len >>> 8) & 0xff
199+
res[i++] = len & 0xff
201200
} else {
202-
for (let b = 0; b < lengthBytes; b++) {
203-
res[i++] = Number(totalBits & 0xffn)
204-
totalBits >>= 8n
201+
res[i++] = len & 0xff
202+
res[i++] = (len >>> 8) & 0xff
203+
res[i++] = (len >>> 16) & 0xff
204+
res[i++] = (len >>> 24) & 0xff
205+
res[i++] = 0
206+
res[i++] = 0
207+
res[i++] = 0
208+
res[i++] = 0
209+
210+
for (t = 8; t < this.padLength; t++) {
211+
res[i++] = 0
205212
}
206213
}
214+
207215
return res
208216
}
209217
}
@@ -262,10 +270,8 @@ export function toArray (
262270
}
263271
}
264272
} else {
265-
msg = msg.replace(/[^a-z0-9]+/gi, '')
266-
if (msg.length % 2 !== 0) {
267-
msg = '0' + msg
268-
}
273+
assertValidHex(msg)
274+
msg = normalizeHex(msg)
269275
for (let i = 0; i < msg.length; i += 2) {
270276
res.push(parseInt(msg[i] + msg[i + 1], 16))
271277
}

src/primitives/__tests/hex.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/* eslint-env jest */
2+
3+
import { assertValidHex, normalizeHex } from '../../primitives/hex'
4+
5+
describe('hex utils', () => {
6+
describe('assertValidHex', () => {
7+
it('should not throw on valid hex strings', () => {
8+
expect(() => assertValidHex('00')).not.toThrow()
9+
expect(() => assertValidHex('abcdef')).not.toThrow()
10+
expect(() => assertValidHex('ABCDEF')).not.toThrow()
11+
expect(() => assertValidHex('1234567890')).not.toThrow()
12+
})
13+
14+
it('should throw on non-hex characters', () => {
15+
expect(() => assertValidHex('zz')).toThrow('Invalid hex string')
16+
expect(() => assertValidHex('0x1234')).toThrow('Invalid hex string')
17+
expect(() => assertValidHex('12 34')).toThrow('Invalid hex string')
18+
expect(() => assertValidHex('g1')).toThrow('Invalid hex string')
19+
})
20+
21+
it('should throw on empty string', () => {
22+
expect(() => assertValidHex('')).toThrow('Invalid hex string')
23+
})
24+
25+
it('should throw on undefined or null', () => {
26+
expect(() => assertValidHex(undefined as any)).toThrow()
27+
expect(() => assertValidHex(null as any)).toThrow()
28+
})
29+
})
30+
31+
describe('normalizeHex', () => {
32+
it('should return lowercase hex', () => {
33+
expect(normalizeHex('ABCD')).toBe('abcd')
34+
})
35+
36+
it('should prepend 0 to odd-length hex strings', () => {
37+
expect(normalizeHex('abc')).toBe('0abc')
38+
expect(normalizeHex('f')).toBe('0f')
39+
})
40+
41+
it('should leave even-length hex strings untouched (except lowercase)', () => {
42+
expect(normalizeHex('AABB')).toBe('aabb')
43+
expect(normalizeHex('001122')).toBe('001122')
44+
})
45+
46+
it('should throw on invalid hex', () => {
47+
expect(() => normalizeHex('xyz')).toThrow('Invalid hex string')
48+
expect(() => normalizeHex('12 34')).toThrow('Invalid hex string')
49+
})
50+
})
51+
})

src/primitives/hex.ts

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,33 @@
11
// src/primitives/hex.ts
22

3-
const PURE_HEX_REGEX = /^[0-9a-fA-F]+$/
3+
// Accepts empty string because empty byte arrays are valid in Bitcoin.
4+
const PURE_HEX_REGEX = /^[0-9a-fA-F]*$/;
45

5-
export function assertValidHex (msg: string): void {
6-
if (typeof msg !== 'string' || msg.length === 0 || !PURE_HEX_REGEX.test(msg)) {
7-
throw new Error('Invalid hex string')
6+
export function assertValidHex(msg: string): void {
7+
if (typeof msg !== 'string') {
8+
throw new Error('Invalid hex string');
9+
}
10+
11+
// Allow empty strings (valid empty byte arrays)
12+
if (msg.length === 0) return;
13+
14+
if (!PURE_HEX_REGEX.test(msg)) {
15+
throw new Error('Invalid hex string');
816
}
917
}
1018

11-
export function normalizeHex (msg: string): string {
12-
assertValidHex(msg)
19+
export function normalizeHex(msg: string): string {
20+
assertValidHex(msg);
21+
22+
// If empty, return empty — never force to "00"
23+
if (msg.length === 0) return '';
1324

14-
// Lowercase first
15-
let normalized = msg.toLowerCase()
25+
let normalized = msg.toLowerCase();
1626

17-
// Prepend "0" if odd-length
18-
if (normalized.length % 2 === 1) {
19-
normalized = '0' + normalized
27+
// Pad odd-length hex
28+
if (normalized.length % 2 !== 0) {
29+
normalized = '0' + normalized;
2030
}
2131

22-
return normalized
32+
return normalized;
2333
}

src/primitives/utils.ts

Lines changed: 27 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import BigNumber from './BigNumber.js'
22
import { hash256 } from './Hash.js'
3+
import { assertValidHex, normalizeHex } from './hex.js'
34

45
const BufferCtor =
56
typeof globalThis !== 'undefined' ? (globalThis as any).Buffer : undefined
@@ -80,28 +81,19 @@ for (let i = 0; i < 6; i++) {
8081
}
8182

8283
const hexToArray = (msg: string): number[] => {
83-
if (CAN_USE_BUFFER && PURE_HEX_REGEX.test(msg)) {
84-
const normalized = msg.length % 2 === 0 ? msg : '0' + msg
84+
assertValidHex(msg)
85+
const normalized = msg.length % 2 === 0 ? msg : '0' + msg
86+
if (CAN_USE_BUFFER) {
8587
return Array.from(BufferCtor.from(normalized, 'hex'))
8688
}
87-
const res: number[] = new Array(Math.ceil(msg.length / 2))
88-
let nibble = -1
89-
let size = 0
90-
for (let i = 0; i < msg.length; i++) {
91-
const value = HEX_CHAR_TO_VALUE[msg.charCodeAt(i)]
92-
if (value === -1) continue
93-
if (nibble === -1) {
94-
nibble = value
95-
} else {
96-
res[size++] = (nibble << 4) | value
97-
nibble = -1
98-
}
99-
}
100-
if (nibble !== -1) {
101-
res[size++] = nibble
89+
const out = new Array(normalized.length / 2)
90+
let o = 0
91+
for (let i = 0; i < normalized.length; i += 2) {
92+
const hi = HEX_CHAR_TO_VALUE[normalized.charCodeAt(i)]
93+
const lo = HEX_CHAR_TO_VALUE[normalized.charCodeAt(i + 1)]
94+
out[o++] = (hi << 4) | lo
10295
}
103-
if (size !== res.length) res.length = size
104-
return res
96+
return out
10597
}
10698

10799
export function base64ToArray (msg: string): number[] {
@@ -237,6 +229,7 @@ export const toUTF8 = (arr: number[]): string => {
237229

238230
for (let i = 0; i < arr.length; i++) {
239231
const byte = arr[i]
232+
240233
// this byte is part of a multi-byte sequence, skip it
241234
// added to avoid modifying i within the loop which is considered unsafe.
242235
if (skip > 0) {
@@ -247,41 +240,26 @@ export const toUTF8 = (arr: number[]): string => {
247240
// 1-byte sequence (0xxxxxxx)
248241
if (byte <= 0x7f) {
249242
result += String.fromCharCode(byte)
250-
continue
251-
}
252-
253-
// 2-byte sequence (110xxxxx 10xxxxxx)
254-
if (byte >= 0xc0 && byte <= 0xdf) {
255-
const avail = arr.length - (i + 1)
256-
const byte2 = avail >= 1 ? arr[i + 1] : 0
257-
skip = Math.min(1, avail)
258-
243+
} else if (byte >= 0xc0 && byte <= 0xdf) {
244+
// 2-byte sequence (110xxxxx 10xxxxxx)
245+
const byte2 = arr[i + 1]
246+
skip = 1
259247
const codePoint = ((byte & 0x1f) << 6) | (byte2 & 0x3f)
260248
result += String.fromCharCode(codePoint)
261-
continue
262-
}
263-
264-
// 3-byte sequence (1110xxxx 10xxxxxx 10xxxxxx)
265-
if (byte >= 0xe0 && byte <= 0xef) {
266-
const avail = arr.length - (i + 1)
267-
const byte2 = avail >= 1 ? arr[i + 1] : 0
268-
const byte3 = avail >= 2 ? arr[i + 2] : 0
269-
skip = Math.min(2, avail)
270-
249+
} else if (byte >= 0xe0 && byte <= 0xef) {
250+
// 3-byte sequence (1110xxxx 10xxxxxx 10xxxxxx)
251+
const byte2 = arr[i + 1]
252+
const byte3 = arr[i + 2]
253+
skip = 2
271254
const codePoint =
272255
((byte & 0x0f) << 12) | ((byte2 & 0x3f) << 6) | (byte3 & 0x3f)
273256
result += String.fromCharCode(codePoint)
274-
continue
275-
}
276-
277-
// 4-byte sequence (11110xxx 10xxxxxx 10xxxxxx 10xxxxxx)
278-
if (byte >= 0xf0 && byte <= 0xf7) {
279-
const avail = arr.length - (i + 1)
280-
const byte2 = avail >= 1 ? arr[i + 1] : 0
281-
const byte3 = avail >= 2 ? arr[i + 2] : 0
282-
const byte4 = avail >= 3 ? arr[i + 3] : 0
283-
skip = Math.min(3, avail)
284-
257+
} else if (byte >= 0xf0 && byte <= 0xf7) {
258+
// 4-byte sequence (11110xxx 10xxxxxx 10xxxxxx 10xxxxxx)
259+
const byte2 = arr[i + 1]
260+
const byte3 = arr[i + 2]
261+
const byte4 = arr[i + 3]
262+
skip = 3
285263
const codePoint =
286264
((byte & 0x07) << 18) |
287265
((byte2 & 0x3f) << 12) |
@@ -292,7 +270,6 @@ export const toUTF8 = (arr: number[]): string => {
292270
const surrogate1 = 0xd800 + ((codePoint - 0x10000) >> 10)
293271
const surrogate2 = 0xdc00 + ((codePoint - 0x10000) & 0x3ff)
294272
result += String.fromCharCode(surrogate1, surrogate2)
295-
continue
296273
}
297274
}
298275

0 commit comments

Comments
 (0)