@exodus/bytes 1.0.0-rc.2 → 1.0.0-rc.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -2
- package/array.js +1 -1
- package/assert.js +10 -2
- package/base32.js +33 -0
- package/base64.js +92 -153
- package/fallback/_utils.js +6 -0
- package/fallback/base32.js +198 -0
- package/fallback/base64.js +162 -0
- package/fallback/hex.js +107 -0
- package/fallback/utf8.js +280 -0
- package/hex.js +11 -68
- package/package.json +18 -7
- package/utf8.js +110 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { assertUint8 } from '../assert.js'
|
|
2
|
+
import { nativeEncoder, nativeDecoder } from './_utils.js'
|
|
3
|
+
|
|
4
|
+
// See https://datatracker.ietf.org/doc/html/rfc4648
|
|
5
|
+
|
|
6
|
+
const BASE64 = [...'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/']
|
|
7
|
+
const BASE64URL = [...'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_']
|
|
8
|
+
const BASE64_HELPERS = {}
|
|
9
|
+
const BASE64URL_HELPERS = {}
|
|
10
|
+
|
|
11
|
+
export const E_CHAR = 'Invalid character in base64 input'
|
|
12
|
+
export const E_PADDING = 'Invalid base64 padding'
|
|
13
|
+
export const E_LENGTH = 'Invalid base64 length'
|
|
14
|
+
export const E_LAST = 'Invalid last chunk'
|
|
15
|
+
|
|
16
|
+
// Alternatively, we could have mapped 0-255 bytes to charcodes and just used btoa(ascii),
|
|
17
|
+
// but that approach is _slower_ than our toBase64js function, even on Hermes
|
|
18
|
+
|
|
19
|
+
// We construct output by concatenating chars, this seems to be fine enough on modern JS engines
|
|
20
|
+
export function toBase64(arr, isURL, padding) {
|
|
21
|
+
assertUint8(arr)
|
|
22
|
+
const fullChunks = Math.floor(arr.length / 3)
|
|
23
|
+
const fullChunksBytes = fullChunks * 3
|
|
24
|
+
let o = ''
|
|
25
|
+
let i = 0
|
|
26
|
+
|
|
27
|
+
const alphabet = isURL ? BASE64URL : BASE64
|
|
28
|
+
const helpers = isURL ? BASE64URL_HELPERS : BASE64_HELPERS
|
|
29
|
+
if (!helpers.pairs) {
|
|
30
|
+
helpers.pairs = []
|
|
31
|
+
if (nativeDecoder) {
|
|
32
|
+
// Lazy to save memory in case if this is not needed
|
|
33
|
+
helpers.codepairs = new Uint16Array(64 * 64)
|
|
34
|
+
const u16 = helpers.codepairs
|
|
35
|
+
const u8 = new Uint8Array(u16.buffer, u16.byteOffset, u16.byteLength) // write as 1-byte to ignore BE/LE difference
|
|
36
|
+
for (let i = 0; i < 64; i++) {
|
|
37
|
+
const ic = alphabet[i].charCodeAt(0)
|
|
38
|
+
for (let j = 0; j < 64; j++) u8[(i << 7) | (j << 1)] = u8[(j << 7) | ((i << 1) + 1)] = ic
|
|
39
|
+
}
|
|
40
|
+
} else {
|
|
41
|
+
const p = helpers.pairs
|
|
42
|
+
for (let i = 0; i < 64; i++) {
|
|
43
|
+
for (let j = 0; j < 64; j++) p.push(`${alphabet[i]}${alphabet[j]}`)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const { pairs, codepairs } = helpers
|
|
49
|
+
|
|
50
|
+
// Fast path for complete blocks
|
|
51
|
+
// This whole loop can be commented out, the algorithm won't change, it's just an optimization of the next loop
|
|
52
|
+
if (nativeDecoder) {
|
|
53
|
+
const oa = new Uint16Array(fullChunks * 2)
|
|
54
|
+
for (let j = 0; i < fullChunksBytes; i += 3) {
|
|
55
|
+
const a = arr[i]
|
|
56
|
+
const b = arr[i + 1]
|
|
57
|
+
const c = arr[i + 2]
|
|
58
|
+
oa[j++] = codepairs[(a << 4) | (b >> 4)]
|
|
59
|
+
oa[j++] = codepairs[((b & 0x0f) << 8) | c]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
o = nativeDecoder.decode(oa)
|
|
63
|
+
} else {
|
|
64
|
+
for (; i < fullChunksBytes; i += 3) {
|
|
65
|
+
const a = arr[i]
|
|
66
|
+
const b = arr[i + 1]
|
|
67
|
+
const c = arr[i + 2]
|
|
68
|
+
o += pairs[(a << 4) | (b >> 4)] + pairs[((b & 0x0f) << 8) | c]
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// If we have something left, process it with a full algo
|
|
73
|
+
let carry = 0
|
|
74
|
+
let shift = 2 // First byte needs to be shifted by 2 to get 6 bits
|
|
75
|
+
const length = arr.length
|
|
76
|
+
for (; i < length; i++) {
|
|
77
|
+
const x = arr[i]
|
|
78
|
+
o += alphabet[carry | (x >> shift)] // shift >= 2, so this fits
|
|
79
|
+
if (shift === 6) {
|
|
80
|
+
shift = 0
|
|
81
|
+
o += alphabet[x & 0x3f]
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
carry = (x << (6 - shift)) & 0x3f
|
|
85
|
+
shift += 2 // Each byte prints 6 bits and leaves 2 bits
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (shift !== 2) o += alphabet[carry] // shift 2 means we have no carry left
|
|
89
|
+
if (padding) o += ['', '==', '='][length - fullChunksBytes]
|
|
90
|
+
|
|
91
|
+
return o
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// TODO: can this be optimized? This only affects non-Hermes barebone engines though
|
|
95
|
+
const mapSize = nativeEncoder ? 256 : 65_536 // we have to store 64 KiB map or recheck everything if we can't decode to byte array
|
|
96
|
+
|
|
97
|
+
// Last chunk is rechecked at API
|
|
98
|
+
export function fromBase64(str, isURL) {
|
|
99
|
+
let inputLength = str.length
|
|
100
|
+
while (str[inputLength - 1] === '=') inputLength--
|
|
101
|
+
const paddingLength = str.length - inputLength
|
|
102
|
+
const tailLength = inputLength % 4
|
|
103
|
+
const mainLength = inputLength - tailLength // multiples of 4
|
|
104
|
+
if (tailLength === 1) throw new SyntaxError(E_LENGTH)
|
|
105
|
+
if (paddingLength > 3 || (paddingLength !== 0 && str.length % 4 !== 0)) {
|
|
106
|
+
throw new SyntaxError(E_PADDING)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const alphabet = isURL ? BASE64URL : BASE64
|
|
110
|
+
const helpers = isURL ? BASE64URL_HELPERS : BASE64_HELPERS
|
|
111
|
+
|
|
112
|
+
if (!helpers.fromMap) {
|
|
113
|
+
helpers.fromMap = new Int8Array(mapSize).fill(-1) // no regex input validation here, so we map all other bytes to -1 and recheck sign
|
|
114
|
+
alphabet.forEach((c, i) => (helpers.fromMap[c.charCodeAt(0)] = i))
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const m = helpers.fromMap
|
|
118
|
+
|
|
119
|
+
const arr = new Uint8Array(Math.floor((inputLength * 3) / 4))
|
|
120
|
+
let at = 0
|
|
121
|
+
let i = 0
|
|
122
|
+
|
|
123
|
+
if (nativeEncoder) {
|
|
124
|
+
const codes = nativeEncoder.encode(str)
|
|
125
|
+
if (codes.length !== str.length) throw new SyntaxError(E_CHAR) // non-ascii
|
|
126
|
+
while (i < mainLength) {
|
|
127
|
+
const a = (m[codes[i++]] << 18) | (m[codes[i++]] << 12) | (m[codes[i++]] << 6) | m[codes[i++]]
|
|
128
|
+
if (a < 0) throw new SyntaxError(E_CHAR)
|
|
129
|
+
arr[at++] = a >> 16
|
|
130
|
+
arr[at++] = (a >> 8) & 0xff
|
|
131
|
+
arr[at++] = a & 0xff
|
|
132
|
+
}
|
|
133
|
+
} else {
|
|
134
|
+
while (i < mainLength) {
|
|
135
|
+
const a =
|
|
136
|
+
(m[str.charCodeAt(i++)] << 18) |
|
|
137
|
+
(m[str.charCodeAt(i++)] << 12) |
|
|
138
|
+
(m[str.charCodeAt(i++)] << 6) |
|
|
139
|
+
m[str.charCodeAt(i++)]
|
|
140
|
+
if (a < 0) throw new SyntaxError(E_CHAR)
|
|
141
|
+
arr[at++] = a >> 16
|
|
142
|
+
arr[at++] = (a >> 8) & 0xff
|
|
143
|
+
arr[at++] = a & 0xff
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Can be 0, 2 or 3, verified by padding checks already
|
|
148
|
+
if (tailLength < 2) return arr // 0
|
|
149
|
+
const ab = (m[str.charCodeAt(i++)] << 6) | m[str.charCodeAt(i++)]
|
|
150
|
+
if (ab < 0) throw new SyntaxError(E_CHAR)
|
|
151
|
+
arr[at++] = ab >> 4
|
|
152
|
+
if (tailLength < 3) {
|
|
153
|
+
if (ab & 0xf) throw new SyntaxError(E_LAST)
|
|
154
|
+
return arr // 2
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const c = m[str.charCodeAt(i++)]
|
|
158
|
+
if (c < 0) throw new SyntaxError(E_CHAR)
|
|
159
|
+
arr[at++] = ((ab << 4) & 0xff) | (c >> 2)
|
|
160
|
+
if (c & 0x3) throw new SyntaxError(E_LAST)
|
|
161
|
+
return arr // 3
|
|
162
|
+
}
|
package/fallback/hex.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { assertUint8 } from '../assert.js'
|
|
2
|
+
import { nativeEncoder } from './_utils.js'
|
|
3
|
+
|
|
4
|
+
let hexArray
|
|
5
|
+
let dehexArray
|
|
6
|
+
|
|
7
|
+
export const E_HEX = 'Input is not a hex string'
|
|
8
|
+
|
|
9
|
+
function toHexPart(arr, start, end) {
|
|
10
|
+
let o = ''
|
|
11
|
+
let i = start
|
|
12
|
+
const last3 = end - 3
|
|
13
|
+
// Unrolled loop is faster
|
|
14
|
+
while (i < last3) {
|
|
15
|
+
const a = arr[i++]
|
|
16
|
+
const b = arr[i++]
|
|
17
|
+
const c = arr[i++]
|
|
18
|
+
const d = arr[i++]
|
|
19
|
+
o += hexArray[a]
|
|
20
|
+
o += hexArray[b]
|
|
21
|
+
o += hexArray[c]
|
|
22
|
+
o += hexArray[d]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
while (i < end) o += hexArray[arr[i++]]
|
|
26
|
+
return o
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function toHex(arr) {
|
|
30
|
+
assertUint8(arr)
|
|
31
|
+
|
|
32
|
+
if (!hexArray) hexArray = Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, '0'))
|
|
33
|
+
const length = arr.length // this helps Hermes
|
|
34
|
+
|
|
35
|
+
if (length > 30_000) {
|
|
36
|
+
// Limit concatenation to avoid excessive GC
|
|
37
|
+
// Thresholds checked on Hermes
|
|
38
|
+
const concat = []
|
|
39
|
+
for (let i = 0; i < length; ) {
|
|
40
|
+
const step = i + 500
|
|
41
|
+
const end = step > length ? length : step
|
|
42
|
+
concat.push(toHexPart(arr, i, end))
|
|
43
|
+
i = end
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const res = concat.join('')
|
|
47
|
+
concat.length = 0
|
|
48
|
+
return res
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return toHexPart(arr, 0, length)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// TODO: can this be optimized? This only affects non-Hermes barebone engines though
|
|
55
|
+
const mapSize = nativeEncoder ? 256 : 65_536 // we have to store 64 KiB map or recheck everything if we can't decode to byte array
|
|
56
|
+
|
|
57
|
+
export function fromHex(str) {
|
|
58
|
+
if (typeof str !== 'string') throw new TypeError('Input is not a string')
|
|
59
|
+
if (str.length % 2 !== 0) throw new SyntaxError(E_HEX)
|
|
60
|
+
|
|
61
|
+
// We don't use native Buffer impl, as rechecking input make it slower than pure js
|
|
62
|
+
// This path is used only on older engines though
|
|
63
|
+
|
|
64
|
+
if (!dehexArray) {
|
|
65
|
+
dehexArray = new Int8Array(mapSize).fill(-1) // no regex input validation here, so we map all other bytes to -1 and recheck sign
|
|
66
|
+
for (let i = 0; i < 16; i++) {
|
|
67
|
+
const s = i.toString(16)
|
|
68
|
+
dehexArray[s.charCodeAt(0)] = dehexArray[s.toUpperCase().charCodeAt(0)] = i
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const length = str.length / 2 // this helps Hermes in loops
|
|
73
|
+
const arr = new Uint8Array(length)
|
|
74
|
+
let j = 0
|
|
75
|
+
if (nativeEncoder) {
|
|
76
|
+
// Native encoder path is beneficial even for small arrays in Hermes
|
|
77
|
+
const codes = nativeEncoder.encode(str)
|
|
78
|
+
if (codes.length !== str.length) throw new SyntaxError(E_HEX) // non-ascii
|
|
79
|
+
const last3 = length - 3 // Unroll nativeEncoder path as this is what modern Hermes takes and a small perf improvement is nice there
|
|
80
|
+
let i = 0
|
|
81
|
+
while (i < last3) {
|
|
82
|
+
const a = (dehexArray[codes[j++]] << 4) | dehexArray[codes[j++]]
|
|
83
|
+
const b = (dehexArray[codes[j++]] << 4) | dehexArray[codes[j++]]
|
|
84
|
+
const c = (dehexArray[codes[j++]] << 4) | dehexArray[codes[j++]]
|
|
85
|
+
const d = (dehexArray[codes[j++]] << 4) | dehexArray[codes[j++]]
|
|
86
|
+
if (a < 0 || b < 0 || c < 0 || d < 0) throw new SyntaxError(E_HEX)
|
|
87
|
+
arr[i++] = a
|
|
88
|
+
arr[i++] = b
|
|
89
|
+
arr[i++] = c
|
|
90
|
+
arr[i++] = d
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
while (i < length) {
|
|
94
|
+
const res = (dehexArray[codes[j++]] << 4) | dehexArray[codes[j++]]
|
|
95
|
+
if (res < 0) throw new SyntaxError(E_HEX)
|
|
96
|
+
arr[i++] = res
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
for (let i = 0; i < length; i++) {
|
|
100
|
+
const res = (dehexArray[str.charCodeAt(j++)] << 4) | dehexArray[str.charCodeAt(j++)]
|
|
101
|
+
if (res < 0) throw new SyntaxError(E_HEX)
|
|
102
|
+
arr[i] = res
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return arr
|
|
107
|
+
}
|
package/fallback/utf8.js
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
export const E_STRICT = 'Input is not well-formed utf8'
|
|
2
|
+
export const E_STRICT_UNICODE = 'Input is not well-formed Unicode'
|
|
3
|
+
|
|
4
|
+
const replacementPoint = 0xff_fd
|
|
5
|
+
|
|
6
|
+
// https://encoding.spec.whatwg.org/#utf-8-decoder
|
|
7
|
+
// We are most likely in loose mode, for non-loose escape & decodeURIComponent solved everything
|
|
8
|
+
export function decode(arr, loose) {
|
|
9
|
+
const start = 0
|
|
10
|
+
const end = arr.length
|
|
11
|
+
let out = ''
|
|
12
|
+
const tmp = []
|
|
13
|
+
|
|
14
|
+
for (let i = start; i < end; i++) {
|
|
15
|
+
if (tmp.length > 0x2_00) {
|
|
16
|
+
// far below MAX_ARGUMENTS_LENGTH in npmjs.com/buffer, we use smaller chunks
|
|
17
|
+
// length can be off by a few as large code points produce two utf-16 char codes, also we overshoot in unrolled loop
|
|
18
|
+
out += String.fromCharCode.apply(String, tmp)
|
|
19
|
+
tmp.length = 0
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const byte = arr[i]
|
|
23
|
+
if (byte < 0x80) {
|
|
24
|
+
// Fast path ascii
|
|
25
|
+
tmp.push(byte)
|
|
26
|
+
// Unroll the loop a bit for faster ops, overshoot by 20 chars
|
|
27
|
+
for (let j = 0; j < 5; j++) {
|
|
28
|
+
if (i + 1 >= end) break
|
|
29
|
+
const byte1 = arr[i + 1]
|
|
30
|
+
if (byte1 >= 0x80) break
|
|
31
|
+
tmp.push(byte1)
|
|
32
|
+
i++
|
|
33
|
+
if (i + 1 >= end) break
|
|
34
|
+
const byte2 = arr[i + 1]
|
|
35
|
+
if (byte2 >= 0x80) break
|
|
36
|
+
tmp.push(byte2)
|
|
37
|
+
i++
|
|
38
|
+
if (i + 1 >= end) break
|
|
39
|
+
const byte3 = arr[i + 1]
|
|
40
|
+
if (byte3 >= 0x80) break
|
|
41
|
+
tmp.push(byte3)
|
|
42
|
+
i++
|
|
43
|
+
if (i + 1 >= end) break
|
|
44
|
+
const byte4 = arr[i + 1]
|
|
45
|
+
if (byte4 >= 0x80) break
|
|
46
|
+
tmp.push(byte4)
|
|
47
|
+
i++
|
|
48
|
+
}
|
|
49
|
+
} else if (byte < 0xc2) {
|
|
50
|
+
if (!loose) throw new TypeError(E_STRICT)
|
|
51
|
+
tmp.push(replacementPoint)
|
|
52
|
+
} else if (byte < 0xe0) {
|
|
53
|
+
// need 1 more
|
|
54
|
+
if (i + 1 >= end) {
|
|
55
|
+
if (!loose) throw new TypeError(E_STRICT)
|
|
56
|
+
tmp.push(replacementPoint)
|
|
57
|
+
break
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const byte1 = arr[i + 1]
|
|
61
|
+
if (byte1 < 0x80 || byte1 > 0xbf) {
|
|
62
|
+
if (!loose) throw new TypeError(E_STRICT)
|
|
63
|
+
tmp.push(replacementPoint)
|
|
64
|
+
continue
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
i++
|
|
68
|
+
tmp.push(((byte & 0x1f) << 6) | (byte1 & 0x3f))
|
|
69
|
+
} else if (byte < 0xf0) {
|
|
70
|
+
// need 2 more
|
|
71
|
+
if (i + 1 >= end) {
|
|
72
|
+
if (!loose) throw new TypeError(E_STRICT)
|
|
73
|
+
tmp.push(replacementPoint)
|
|
74
|
+
break
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const lower = byte === 0xe0 ? 0xa0 : 0x80
|
|
78
|
+
const upper = byte === 0xed ? 0x9f : 0xbf
|
|
79
|
+
const byte1 = arr[i + 1]
|
|
80
|
+
if (byte1 < lower || byte1 > upper) {
|
|
81
|
+
if (!loose) throw new TypeError(E_STRICT)
|
|
82
|
+
tmp.push(replacementPoint)
|
|
83
|
+
continue
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
i++
|
|
87
|
+
if (i + 1 >= end) {
|
|
88
|
+
if (!loose) throw new TypeError(E_STRICT)
|
|
89
|
+
tmp.push(replacementPoint)
|
|
90
|
+
break
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const byte2 = arr[i + 1]
|
|
94
|
+
if (byte2 < 0x80 || byte2 > 0xbf) {
|
|
95
|
+
if (!loose) throw new TypeError(E_STRICT)
|
|
96
|
+
tmp.push(replacementPoint)
|
|
97
|
+
continue
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
i++
|
|
101
|
+
tmp.push(((byte & 0xf) << 12) | ((byte1 & 0x3f) << 6) | (byte2 & 0x3f))
|
|
102
|
+
} else if (byte <= 0xf4) {
|
|
103
|
+
// need 3 more
|
|
104
|
+
if (i + 1 >= end) {
|
|
105
|
+
if (!loose) throw new TypeError(E_STRICT)
|
|
106
|
+
tmp.push(replacementPoint)
|
|
107
|
+
break
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const lower = byte === 0xf0 ? 0x90 : 0x80
|
|
111
|
+
const upper = byte === 0xf4 ? 0x8f : 0xbf
|
|
112
|
+
const byte1 = arr[i + 1]
|
|
113
|
+
if (byte1 < lower || byte1 > upper) {
|
|
114
|
+
if (!loose) throw new TypeError(E_STRICT)
|
|
115
|
+
tmp.push(replacementPoint)
|
|
116
|
+
continue
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
i++
|
|
120
|
+
if (i + 1 >= end) {
|
|
121
|
+
if (!loose) throw new TypeError(E_STRICT)
|
|
122
|
+
tmp.push(replacementPoint)
|
|
123
|
+
break
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const byte2 = arr[i + 1]
|
|
127
|
+
if (byte2 < 0x80 || byte2 > 0xbf) {
|
|
128
|
+
if (!loose) throw new TypeError(E_STRICT)
|
|
129
|
+
tmp.push(replacementPoint)
|
|
130
|
+
continue
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
i++
|
|
134
|
+
if (i + 1 >= end) {
|
|
135
|
+
if (!loose) throw new TypeError(E_STRICT)
|
|
136
|
+
tmp.push(replacementPoint)
|
|
137
|
+
break
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const byte3 = arr[i + 1]
|
|
141
|
+
if (byte3 < 0x80 || byte3 > 0xbf) {
|
|
142
|
+
if (!loose) throw new TypeError(E_STRICT)
|
|
143
|
+
tmp.push(replacementPoint)
|
|
144
|
+
continue
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
i++
|
|
148
|
+
const codePoint =
|
|
149
|
+
((byte & 0xf) << 18) | ((byte1 & 0x3f) << 12) | ((byte2 & 0x3f) << 6) | (byte3 & 0x3f)
|
|
150
|
+
if (codePoint > 0xff_ff) {
|
|
151
|
+
// split into char codes as String.fromCharCode is faster than String.fromCodePoint
|
|
152
|
+
const u = codePoint - 0x1_00_00
|
|
153
|
+
tmp.push(0xd8_00 + ((u >> 10) & 0x3_ff), 0xdc_00 + (u & 0x3_ff))
|
|
154
|
+
} else {
|
|
155
|
+
tmp.push(codePoint)
|
|
156
|
+
}
|
|
157
|
+
// eslint-disable-next-line sonarjs/no-duplicated-branches
|
|
158
|
+
} else {
|
|
159
|
+
if (!loose) throw new TypeError(E_STRICT)
|
|
160
|
+
tmp.push(replacementPoint)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (tmp.length > 0) out += String.fromCharCode.apply(String, tmp)
|
|
165
|
+
return out
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function encode(string, loose) {
|
|
169
|
+
const length = string.length
|
|
170
|
+
let lead = null
|
|
171
|
+
let small = true
|
|
172
|
+
let bytes = new Uint8Array(length) // assume ascii
|
|
173
|
+
let p = 0
|
|
174
|
+
|
|
175
|
+
for (let i = 0; i < length; i++) {
|
|
176
|
+
const code = string.charCodeAt(i)
|
|
177
|
+
if (code < 0x80) {
|
|
178
|
+
// Fast path for ascii
|
|
179
|
+
if (lead) {
|
|
180
|
+
if (!loose) throw new TypeError(E_STRICT_UNICODE)
|
|
181
|
+
bytes[p++] = 0xef
|
|
182
|
+
bytes[p++] = 0xbf
|
|
183
|
+
bytes[p++] = 0xbd
|
|
184
|
+
lead = null
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
bytes[p++] = code
|
|
188
|
+
// Unroll the loop a bit for faster ops
|
|
189
|
+
for (let j = 0; j < 5; j++) {
|
|
190
|
+
if (i + 1 >= length) break
|
|
191
|
+
const c1 = string.charCodeAt(i + 1)
|
|
192
|
+
if (c1 >= 0x80) break
|
|
193
|
+
bytes[p++] = c1
|
|
194
|
+
i++
|
|
195
|
+
if (i + 1 >= length) break
|
|
196
|
+
const c2 = string.charCodeAt(i + 1)
|
|
197
|
+
if (c2 >= 0x80) break
|
|
198
|
+
bytes[p++] = c2
|
|
199
|
+
i++
|
|
200
|
+
if (i + 1 >= length) break
|
|
201
|
+
const c3 = string.charCodeAt(i + 1)
|
|
202
|
+
if (c3 >= 0x80) break
|
|
203
|
+
bytes[p++] = c3
|
|
204
|
+
i++
|
|
205
|
+
if (i + 1 >= length) break
|
|
206
|
+
const c4 = string.charCodeAt(i + 1)
|
|
207
|
+
if (c4 >= 0x80) break
|
|
208
|
+
bytes[p++] = c4
|
|
209
|
+
i++
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
continue
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (small) {
|
|
216
|
+
// TODO: use resizable array buffers? will have to return a non-resizeable one
|
|
217
|
+
const bytesNew = new Uint8Array(length * 3) // maximium can be 3x of the string length in charcodes
|
|
218
|
+
bytesNew.set(bytes)
|
|
219
|
+
bytes = bytesNew
|
|
220
|
+
small = false
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// surrogate, charcodes = [d800 + a & 3ff, dc00 + b & 3ff]; codePoint = 0x1_00_00 | (a << 10) | b
|
|
224
|
+
// lead: d800 - dbff
|
|
225
|
+
// trail: dc00 - dfff
|
|
226
|
+
if (code >= 0xd8_00 && code < 0xe0_00) {
|
|
227
|
+
if (lead && code < 0xdc_00) {
|
|
228
|
+
// a second lead, meaning the previous one was unpaired
|
|
229
|
+
if (!loose) throw new TypeError(E_STRICT_UNICODE)
|
|
230
|
+
bytes[p++] = 0xef
|
|
231
|
+
bytes[p++] = 0xbf
|
|
232
|
+
bytes[p++] = 0xbd
|
|
233
|
+
lead = null
|
|
234
|
+
// code is still processed as a new lead
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (!lead) {
|
|
238
|
+
if (code > 0xdb_ff || i + 1 >= length) {
|
|
239
|
+
// lead out of range || unpaired
|
|
240
|
+
if (!loose) throw new TypeError(E_STRICT_UNICODE)
|
|
241
|
+
bytes[p++] = 0xef
|
|
242
|
+
bytes[p++] = 0xbf
|
|
243
|
+
bytes[p++] = 0xbd
|
|
244
|
+
continue
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
lead = code
|
|
248
|
+
continue
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// here, codePoint is always between 0x1_00_00 and 0x11_00_00, we encode as 4 bytes
|
|
252
|
+
const codePoint = (((lead - 0xd8_00) << 10) | (code - 0xdc_00)) + 0x1_00_00
|
|
253
|
+
bytes[p++] = (codePoint >> 18) | 0xf0
|
|
254
|
+
bytes[p++] = ((codePoint >> 12) & 0x3f) | 0x80
|
|
255
|
+
bytes[p++] = ((codePoint >> 6) & 0x3f) | 0x80
|
|
256
|
+
bytes[p++] = (codePoint & 0x3f) | 0x80
|
|
257
|
+
lead = null
|
|
258
|
+
continue
|
|
259
|
+
} else if (lead) {
|
|
260
|
+
if (!loose) throw new TypeError(E_STRICT_UNICODE)
|
|
261
|
+
bytes[p++] = 0xef
|
|
262
|
+
bytes[p++] = 0xbf
|
|
263
|
+
bytes[p++] = 0xbd
|
|
264
|
+
lead = null
|
|
265
|
+
// code is still processed
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// We are left with a non-pair char code above ascii, it gets encoded to 2 or 3 bytes
|
|
269
|
+
if (code < 0x8_00) {
|
|
270
|
+
bytes[p++] = (code >> 6) | 0xc0
|
|
271
|
+
bytes[p++] = (code & 0x3f) | 0x80
|
|
272
|
+
} else {
|
|
273
|
+
bytes[p++] = (code >> 12) | 0xe0
|
|
274
|
+
bytes[p++] = ((code >> 6) & 0x3f) | 0x80
|
|
275
|
+
bytes[p++] = (code & 0x3f) | 0x80
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return bytes.length === p ? bytes : bytes.slice(0, p)
|
|
280
|
+
}
|
package/hex.js
CHANGED
|
@@ -1,78 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { assertUint8 } from './assert.js'
|
|
2
|
+
import { typedView } from './array.js'
|
|
3
|
+
import * as js from './fallback/hex.js'
|
|
3
4
|
|
|
4
5
|
const { Buffer } = globalThis // Buffer is optional, only used when native
|
|
5
6
|
const haveNativeBuffer = Buffer && !Buffer.TYPED_ARRAY_SUPPORT
|
|
6
7
|
const { toHex: webHex } = Uint8Array.prototype // Modern engines have this
|
|
7
8
|
|
|
8
|
-
let hexArray
|
|
9
|
-
let dehexArray
|
|
10
|
-
|
|
11
9
|
export function toHex(arr) {
|
|
12
|
-
|
|
13
|
-
if (
|
|
10
|
+
assertUint8(arr)
|
|
11
|
+
if (arr.length === 0) return ''
|
|
14
12
|
if (webHex && arr.toHex === webHex) return arr.toHex()
|
|
15
|
-
if (haveNativeBuffer)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
if (!hexArray) hexArray = Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, '0'))
|
|
21
|
-
const length = arr.length // this helps Hermes
|
|
22
|
-
|
|
23
|
-
if (length > 30000) {
|
|
24
|
-
// Limit concatenation to avoid excessive GC
|
|
25
|
-
// Thresholds checked on Hermes
|
|
26
|
-
const concat = []
|
|
27
|
-
for (let i = 0; i < length; ) {
|
|
28
|
-
const step = i + 500
|
|
29
|
-
const end = step > length ? length : step
|
|
30
|
-
let chunk = ''
|
|
31
|
-
for (; i < end; i++) chunk += hexArray[arr[i]]
|
|
32
|
-
concat.push(chunk)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const res = concat.join('')
|
|
36
|
-
concat.length = 0
|
|
37
|
-
return res
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
let out = ''
|
|
41
|
-
for (let i = 0; i < length; i++) out += hexArray[arr[i]]
|
|
42
|
-
return out
|
|
13
|
+
if (!haveNativeBuffer) return js.toHex(arr)
|
|
14
|
+
if (arr.constructor === Buffer && Buffer.isBuffer(arr)) return arr.toString('hex')
|
|
15
|
+
return Buffer.from(arr.buffer, arr.byteOffset, arr.byteLength).toString('hex')
|
|
43
16
|
}
|
|
44
17
|
|
|
45
18
|
// Unlike Buffer.from(), throws on invalid input
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
} else {
|
|
50
|
-
fromHex = (str, format = 'uint8') => {
|
|
51
|
-
if (typeof str !== 'string') throw new TypeError('Input is not a string')
|
|
52
|
-
assert(str.length % 2 === 0, 'Input is not a hex string')
|
|
53
|
-
|
|
54
|
-
// We don't use native Buffer impl, as rechecking input make it slower than pure js
|
|
55
|
-
// This path is used only on older engines though
|
|
56
|
-
|
|
57
|
-
if (!dehexArray) {
|
|
58
|
-
dehexArray = new Array(103) // f is 102
|
|
59
|
-
for (let i = 0; i < 16; i++) {
|
|
60
|
-
const s = i.toString(16)
|
|
61
|
-
dehexArray[s.charCodeAt(0)] = dehexArray[s.toUpperCase().charCodeAt(0)] = i
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const arr = new Uint8Array(str.length / 2)
|
|
66
|
-
let j = 0
|
|
67
|
-
const length = arr.length // this helps Hermes
|
|
68
|
-
for (let i = 0; i < length; i++) {
|
|
69
|
-
const a = dehexArray[str.charCodeAt(j++)] * 16 + dehexArray[str.charCodeAt(j++)]
|
|
70
|
-
if (!a && Number.isNaN(a)) throw new Error('Input is not a hex string')
|
|
71
|
-
arr[i] = a
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return fromTypedArray(arr, format)
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export { fromHex }
|
|
19
|
+
export const fromHex = Uint8Array.fromHex
|
|
20
|
+
? (str, format = 'uint8') => typedView(Uint8Array.fromHex(str), format)
|
|
21
|
+
: (str, format = 'uint8') => typedView(js.fromHex(str), format)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/bytes",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.4",
|
|
4
4
|
"description": "Various operations on Uint8Array data",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"lint": "eslint .",
|
|
@@ -39,17 +39,25 @@
|
|
|
39
39
|
},
|
|
40
40
|
"type": "module",
|
|
41
41
|
"files": [
|
|
42
|
+
"/fallback/_utils.js",
|
|
43
|
+
"/fallback/base32.js",
|
|
44
|
+
"/fallback/base64.js",
|
|
45
|
+
"/fallback/hex.js",
|
|
46
|
+
"/fallback/utf8.js",
|
|
47
|
+
"/array.js",
|
|
42
48
|
"/assert.js",
|
|
49
|
+
"/base32.js",
|
|
43
50
|
"/base64.js",
|
|
44
|
-
"/
|
|
45
|
-
"/
|
|
51
|
+
"/hex.js",
|
|
52
|
+
"/utf8.js"
|
|
46
53
|
],
|
|
47
54
|
"exports": {
|
|
48
|
-
"./base64.js": "./base64.js",
|
|
49
55
|
"./array.js": "./array.js",
|
|
50
|
-
"./
|
|
56
|
+
"./base32.js": "./base32.js",
|
|
57
|
+
"./base64.js": "./base64.js",
|
|
58
|
+
"./hex.js": "./hex.js",
|
|
59
|
+
"./utf8.js": "./utf8.js"
|
|
51
60
|
},
|
|
52
|
-
"dependencies": {},
|
|
53
61
|
"devDependencies": {
|
|
54
62
|
"@exodus/eslint-config": "^5.24.0",
|
|
55
63
|
"@exodus/prettier": "^1.0.0",
|
|
@@ -62,7 +70,10 @@
|
|
|
62
70
|
"buffer": "^6.0.3",
|
|
63
71
|
"electron": "36.5.0",
|
|
64
72
|
"eslint": "^8.44.0",
|
|
65
|
-
"
|
|
73
|
+
"fast-base64-decode": "^2.0.0",
|
|
74
|
+
"hi-base32": "^0.5.1",
|
|
75
|
+
"jsvu": "^3.0.0",
|
|
76
|
+
"text-encoding": "^0.7.0"
|
|
66
77
|
},
|
|
67
78
|
"prettier": "@exodus/prettier",
|
|
68
79
|
"packageManager": "pnpm@10.12.1+sha256.889bac470ec93ccc3764488a19d6ba8f9c648ad5e50a9a6e4be3768a5de387a3"
|