aboutsummaryrefslogtreecommitdiffstats
path: root/vanilla/node_modules/@exodus/bytes/fallback/hex.js
blob: b3d92d0fc0740112b7f128914242c9a0b8b9b102 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
import { E_STRING } from './_utils.js'
import { nativeDecoder, nativeEncoder, decode2string } from './platform.js'
import { encodeAscii, decodeAscii } from './latin1.js'

let hexArray // array of 256 bytes converted to two-char hex strings
let hexCodes // hexArray converted to u16 code pairs
let dehexArray
const _00 = 0x30_30 // '00' string in hex, the only allowed char pair to generate 0 byte
const _ff = 0x66_66 // 'ff' string in hex, max allowed char pair (larger than 'FF' string)
const allowed = '0123456789ABCDEFabcdef'

export const E_HEX = 'Input is not a hex string'

// Expects a checked Uint8Array
export function toHex(arr) {
  if (!hexArray) hexArray = Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, '0'))
  const length = arr.length // this helps Hermes

  // Only old browsers use this, barebone engines don't have TextDecoder
  // But Hermes can use this when it (hopefully) implements TextDecoder
  if (nativeDecoder) {
    if (!hexCodes) {
      hexCodes = new Uint16Array(256)
      const u8 = new Uint8Array(hexCodes.buffer, hexCodes.byteOffset, hexCodes.byteLength)
      for (let i = 0; i < 256; i++) {
        const pair = hexArray[i]
        u8[2 * i] = pair.charCodeAt(0)
        u8[2 * i + 1] = pair.charCodeAt(1)
      }
    }

    const oa = new Uint16Array(length)
    let i = 0
    for (const last3 = arr.length - 3; ; i += 4) {
      if (i >= last3) break // loop is fast enough for moving this here to be useful on JSC
      const x0 = arr[i]
      const x1 = arr[i + 1]
      const x2 = arr[i + 2]
      const x3 = arr[i + 3]
      oa[i] = hexCodes[x0]
      oa[i + 1] = hexCodes[x1]
      oa[i + 2] = hexCodes[x2]
      oa[i + 3] = hexCodes[x3]
    }

    for (; i < length; i++) oa[i] = hexCodes[arr[i]]
    return decodeAscii(oa)
  }

  return decode2string(arr, 0, length, hexArray)
}

export function fromHex(str) {
  if (typeof str !== 'string') throw new TypeError(E_STRING)
  if (str.length % 2 !== 0) throw new SyntaxError(E_HEX)

  const length = str.length / 2 // this helps Hermes in loops
  const arr = new Uint8Array(length)

  // Native encoder path is beneficial even for small arrays in Hermes
  if (nativeEncoder) {
    if (!dehexArray) {
      dehexArray = new Uint8Array(_ff + 1) // 26 KiB cache, >2x perf improvement on Hermes
      const u8 = new Uint8Array(2)
      const u16 = new Uint16Array(u8.buffer, u8.byteOffset, 1) // for endianess-agnostic transform
      const map = [...allowed].map((c) => [c.charCodeAt(0), parseInt(c, 16)])
      for (const [ch, vh] of map) {
        u8[0] = ch // first we read high hex char
        for (const [cl, vl] of map) {
          u8[1] = cl // then we read low hex char
          dehexArray[u16[0]] = (vh << 4) | vl
        }
      }
    }

    const codes = encodeAscii(str, E_HEX)
    const codes16 = new Uint16Array(codes.buffer, codes.byteOffset, codes.byteLength / 2)
    let i = 0
    for (const last3 = length - 3; i < last3; i += 4) {
      const ai = codes16[i]
      const bi = codes16[i + 1]
      const ci = codes16[i + 2]
      const di = codes16[i + 3]
      const a = dehexArray[ai]
      const b = dehexArray[bi]
      const c = dehexArray[ci]
      const d = dehexArray[di]
      if ((!a && ai !== _00) || (!b && bi !== _00) || (!c && ci !== _00) || (!d && di !== _00)) {
        throw new SyntaxError(E_HEX)
      }

      arr[i] = a
      arr[i + 1] = b
      arr[i + 2] = c
      arr[i + 3] = d
    }

    while (i < length) {
      const ai = codes16[i]
      const a = dehexArray[ai]
      if (!a && ai !== _00) throw new SyntaxError(E_HEX)
      arr[i++] = a
    }
  } else {
    if (!dehexArray) {
      // no regex input validation here, so we map all other bytes to -1 and recheck sign
      // non-ASCII chars throw already though, so we should process only 0-127
      dehexArray = new Int8Array(128).fill(-1)
      for (let i = 0; i < 16; i++) {
        const s = i.toString(16)
        dehexArray[s.charCodeAt(0)] = dehexArray[s.toUpperCase().charCodeAt(0)] = i
      }
    }

    let j = 0
    for (let i = 0; i < length; i++) {
      const a = str.charCodeAt(j++)
      const b = str.charCodeAt(j++)
      const res = (dehexArray[a] << 4) | dehexArray[b]
      if (res < 0 || (0x7f | a | b) !== 0x7f) throw new SyntaxError(E_HEX) // 0-127
      arr[i] = res
    }
  }

  return arr
}