@exodus/bytes 1.0.0-rc.3 → 1.0.0-rc.5
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/assert.js +8 -5
- package/base32.js +33 -0
- package/base58.js +212 -0
- package/base58check.js +30 -0
- package/base64.js +90 -50
- package/fallback/_utils.js +6 -0
- package/fallback/base32.js +198 -0
- package/fallback/base64.js +86 -51
- package/fallback/hex.js +31 -17
- package/fallback/utf8.js +280 -0
- package/hex.js +13 -4
- package/package.json +37 -8
- package/utf8.js +117 -0
package/README.md
CHANGED
package/assert.js
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
export function assert(x, msg) {
|
|
2
|
-
if (!x) throw new Error(msg || 'Assertion failed')
|
|
3
|
-
}
|
|
4
|
-
|
|
5
1
|
export function assertEmptyRest(rest) {
|
|
6
2
|
if (Object.keys(rest).length > 0) throw new TypeError('Unexpected extra options')
|
|
7
3
|
}
|
|
@@ -16,7 +12,14 @@ export function assertTypedArray(arr) {
|
|
|
16
12
|
throw new TypeError('Expected a TypedArray instance')
|
|
17
13
|
}
|
|
18
14
|
|
|
19
|
-
export function assertUint8(arr,
|
|
15
|
+
export function assertUint8(arr, options) {
|
|
16
|
+
if (!options) {
|
|
17
|
+
// fast path
|
|
18
|
+
if (arr instanceof Uint8Array) return
|
|
19
|
+
throw new TypeError('Expected an Uint8Array')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const { name, length, ...rest } = options
|
|
20
23
|
assertEmptyRest(rest)
|
|
21
24
|
if (arr instanceof Uint8Array && (length === undefined || arr.length === length)) return
|
|
22
25
|
throw new TypeError(makeMessage(name, length === undefined ? '' : ` of size ${Number(length)}`))
|
package/base32.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { assertEmptyRest } from './assert.js'
|
|
2
|
+
import { typedView } from './array.js'
|
|
3
|
+
import * as js from './fallback/base32.js'
|
|
4
|
+
|
|
5
|
+
// See https://datatracker.ietf.org/doc/html/rfc4648
|
|
6
|
+
|
|
7
|
+
// 8 chars per 5 bytes
|
|
8
|
+
|
|
9
|
+
const { E_PADDING } = js
|
|
10
|
+
|
|
11
|
+
export const toBase32 = (arr, { padding = false } = {}) => js.toBase32(arr, false, padding)
|
|
12
|
+
export const toBase32hex = (arr, { padding = false } = {}) => js.toBase32(arr, true, padding)
|
|
13
|
+
|
|
14
|
+
// By default, valid padding is accepted but not required
|
|
15
|
+
export const fromBase32 = (str, { format = 'uint8', padding = 'both', ...rest } = {}) =>
|
|
16
|
+
fromBase32common(str, false, padding, format, rest)
|
|
17
|
+
export const fromBase32hex = (str, { format = 'uint8', padding = 'both', ...rest } = {}) =>
|
|
18
|
+
fromBase32common(str, true, padding, format, rest)
|
|
19
|
+
|
|
20
|
+
function fromBase32common(str, isBase32Hex, padding, format, rest) {
|
|
21
|
+
if (typeof str !== 'string') throw new TypeError('Input is not a string')
|
|
22
|
+
assertEmptyRest(rest)
|
|
23
|
+
|
|
24
|
+
if (padding === true) {
|
|
25
|
+
if (str.length % 8 !== 0) throw new SyntaxError(E_PADDING)
|
|
26
|
+
} else if (padding === false) {
|
|
27
|
+
if (str.endsWith('=')) throw new SyntaxError('Did not expect padding in base32 input')
|
|
28
|
+
} else if (padding !== 'both') {
|
|
29
|
+
throw new TypeError('Invalid padding option')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return typedView(js.fromBase32(str, isBase32Hex), format)
|
|
33
|
+
}
|
package/base58.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { typedView } from './array.js'
|
|
2
|
+
import { assertUint8 } from './assert.js'
|
|
3
|
+
import { nativeDecoder, nativeEncoder } from './fallback/_utils.js'
|
|
4
|
+
|
|
5
|
+
const alphabet = [...'123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz']
|
|
6
|
+
const codes = new Uint8Array(alphabet.map((x) => x.charCodeAt(0)))
|
|
7
|
+
const ZERO = alphabet[0]
|
|
8
|
+
const zeroC = codes[0]
|
|
9
|
+
|
|
10
|
+
const _0n = BigInt(0)
|
|
11
|
+
const _1n = BigInt(1)
|
|
12
|
+
const _8n = BigInt(8)
|
|
13
|
+
const _32n = BigInt(32)
|
|
14
|
+
const _58n = BigInt(58)
|
|
15
|
+
const _0xffffffffn = BigInt(0xff_ff_ff_ff)
|
|
16
|
+
|
|
17
|
+
let table // 15 * 82, diagonal, <1kb
|
|
18
|
+
let fromMap
|
|
19
|
+
|
|
20
|
+
const E_CHAR = 'Invalid character in base58 input'
|
|
21
|
+
|
|
22
|
+
const shouldUseBigIntFrom = Boolean(globalThis.HermesInternal) // faster only on Hermes, numbers path beats it on normal engines
|
|
23
|
+
|
|
24
|
+
export function toBase58(arr) {
|
|
25
|
+
assertUint8(arr)
|
|
26
|
+
const length = arr.length
|
|
27
|
+
if (length === 0) return ''
|
|
28
|
+
|
|
29
|
+
let zeros = 0
|
|
30
|
+
while (zeros < length && arr[zeros] === 0) zeros++
|
|
31
|
+
|
|
32
|
+
if (length > 60) {
|
|
33
|
+
// Slow path. Can be optimized ~10%, but the main factor is /58n division anyway, so doesn't matter much
|
|
34
|
+
let x = _0n
|
|
35
|
+
for (let i = 0; i < arr.length; i++) x = (x << _8n) | BigInt(arr[i])
|
|
36
|
+
|
|
37
|
+
let out = ''
|
|
38
|
+
while (x) {
|
|
39
|
+
const d = x / _58n
|
|
40
|
+
out = alphabet[Number(x - _58n * d)] + out
|
|
41
|
+
x = d
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return ZERO.repeat(zeros) + out
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// We run fast mode operations only on short (<=60 bytes) inputs, via precomputation table
|
|
48
|
+
if (!table) {
|
|
49
|
+
table = []
|
|
50
|
+
let x = _1n
|
|
51
|
+
for (let i = 0; i < 15; i++) {
|
|
52
|
+
// Convert x to base 58 digits
|
|
53
|
+
const in58 = []
|
|
54
|
+
let y = x
|
|
55
|
+
while (y) {
|
|
56
|
+
const d = y / _58n
|
|
57
|
+
in58.push(Number(y - _58n * d))
|
|
58
|
+
y = d
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
table.push(new Uint8Array(in58))
|
|
62
|
+
x <<= _32n
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const res = []
|
|
67
|
+
{
|
|
68
|
+
let j = 0
|
|
69
|
+
// We group each 4 bytes into 32-bit chunks
|
|
70
|
+
// Not using u32arr to not deal with remainder + BE/LE differences
|
|
71
|
+
for (let i = length - 1; i >= 0; i -= 4) {
|
|
72
|
+
let c
|
|
73
|
+
if (i > 2) {
|
|
74
|
+
c = (arr[i] | (arr[i - 1] << 8) | (arr[i - 2] << 16) | (arr[i - 3] << 24)) >>> 0
|
|
75
|
+
} else if (i > 1) {
|
|
76
|
+
c = arr[i] | (arr[i - 1] << 8) | (arr[i - 2] << 16)
|
|
77
|
+
} else {
|
|
78
|
+
c = i === 1 ? arr[i] | (arr[i - 1] << 8) : arr[i]
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const row = table[j++]
|
|
82
|
+
if (c === 0) continue
|
|
83
|
+
const olen = res.length
|
|
84
|
+
const nlen = row.length
|
|
85
|
+
let k = 0
|
|
86
|
+
for (; k < olen; k++) res[k] += c * row[k]
|
|
87
|
+
while (k < nlen) res.push(c * row[k++])
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// We can now do a single scan over regular numbers under MAX_SAFE_INTEGER
|
|
92
|
+
// Note: can't use int32 operations on them, as they are outside of 2**32 range
|
|
93
|
+
// This is faster though
|
|
94
|
+
{
|
|
95
|
+
let carry = 0
|
|
96
|
+
let i = 0
|
|
97
|
+
while (i < res.length) {
|
|
98
|
+
const c = res[i] + carry
|
|
99
|
+
carry = Math.floor(c / 58)
|
|
100
|
+
res[i++] = c - carry * 58
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
while (carry) {
|
|
104
|
+
const c = carry
|
|
105
|
+
carry = Math.floor(c / 58)
|
|
106
|
+
res[i++] = c - carry * 58
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (nativeDecoder) {
|
|
111
|
+
const oa = new Uint8Array(res.length)
|
|
112
|
+
let j = 0
|
|
113
|
+
for (let i = res.length - 1; i >= 0; i--) oa[j++] = codes[res[i]]
|
|
114
|
+
return ZERO.repeat(zeros) + nativeDecoder.decode(oa)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let out = ''
|
|
118
|
+
for (let i = res.length - 1; i >= 0; i--) out += alphabet[res[i]]
|
|
119
|
+
return ZERO.repeat(zeros) + out
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// TODO: test on 'z'.repeat(from 1 to smth)
|
|
123
|
+
export function fromBase58(str, format = 'uint8') {
|
|
124
|
+
if (typeof str !== 'string') throw new TypeError('Input is not a string')
|
|
125
|
+
const length = str.length
|
|
126
|
+
if (length === 0) return typedView(new Uint8Array(), format)
|
|
127
|
+
|
|
128
|
+
let zeros = 0
|
|
129
|
+
while (zeros < length && str.charCodeAt(zeros) === zeroC) zeros++
|
|
130
|
+
|
|
131
|
+
if (!fromMap) {
|
|
132
|
+
fromMap = new Int8Array(256).fill(-1)
|
|
133
|
+
for (let i = 0; i < 58; i++) fromMap[alphabet[i].charCodeAt(0)] = i
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const size = zeros + (((length - zeros) * 3 + 1) >> 2) // 3/4 rounded up, larger than ~0.73 coef to fit everything
|
|
137
|
+
const res = new Uint8Array(size)
|
|
138
|
+
let at = size // where is the first significant byte written
|
|
139
|
+
|
|
140
|
+
if (shouldUseBigIntFrom) {
|
|
141
|
+
let x = _0n
|
|
142
|
+
|
|
143
|
+
// nativeEncoder gives a benefit here
|
|
144
|
+
if (nativeEncoder) {
|
|
145
|
+
const codes = nativeEncoder.encode(str)
|
|
146
|
+
if (codes.length !== str.length) throw new SyntaxError(E_CHAR) // non-ascii
|
|
147
|
+
for (let i = zeros; i < length; i++) {
|
|
148
|
+
const c = fromMap[codes[i]]
|
|
149
|
+
if (c < 0) throw new SyntaxError(E_CHAR)
|
|
150
|
+
x = x * _58n + BigInt(c)
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
for (let i = zeros; i < length; i++) {
|
|
154
|
+
const charCode = str.charCodeAt(i)
|
|
155
|
+
const c = fromMap[charCode]
|
|
156
|
+
if (charCode > 255 || c < 0) throw new SyntaxError(E_CHAR)
|
|
157
|
+
x = x * _58n + BigInt(c)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
while (x) {
|
|
162
|
+
let y = Number(x & _0xffffffffn)
|
|
163
|
+
x >>= 32n
|
|
164
|
+
res[--at] = y & 0xff
|
|
165
|
+
y >>>= 8
|
|
166
|
+
if (!x && !y) break
|
|
167
|
+
res[--at] = y & 0xff
|
|
168
|
+
y >>>= 8
|
|
169
|
+
if (!x && !y) break
|
|
170
|
+
res[--at] = y & 0xff
|
|
171
|
+
y >>>= 8
|
|
172
|
+
if (!x && !y) break
|
|
173
|
+
res[--at] = y & 0xff
|
|
174
|
+
}
|
|
175
|
+
} else {
|
|
176
|
+
for (let i = zeros; i < length; i++) {
|
|
177
|
+
const charCode = str.charCodeAt(i)
|
|
178
|
+
let c = fromMap[charCode]
|
|
179
|
+
if (charCode > 255 || c < 0) throw new SyntaxError(E_CHAR)
|
|
180
|
+
|
|
181
|
+
let k = size - 1
|
|
182
|
+
for (;;) {
|
|
183
|
+
if (c === 0 && k < at) break
|
|
184
|
+
c += 58 * res[k]
|
|
185
|
+
res[k] = c & 0xff
|
|
186
|
+
c >>>= 8
|
|
187
|
+
k--
|
|
188
|
+
// unroll a bit
|
|
189
|
+
if (c === 0 && k < at) break
|
|
190
|
+
c += 58 * res[k]
|
|
191
|
+
res[k] = c & 0xff
|
|
192
|
+
c >>>= 8
|
|
193
|
+
k--
|
|
194
|
+
if (c === 0 && k < at) break
|
|
195
|
+
c += 58 * res[k]
|
|
196
|
+
res[k] = c & 0xff
|
|
197
|
+
c >>>= 8
|
|
198
|
+
k--
|
|
199
|
+
if (c === 0 && k < at) break
|
|
200
|
+
c += 58 * res[k]
|
|
201
|
+
res[k] = c & 0xff
|
|
202
|
+
c >>>= 8
|
|
203
|
+
k--
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
at = k + 1
|
|
207
|
+
if (c !== 0 || at < zeros) throw new Error('Unexpected') // unreachable
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return typedView(res.slice(at - zeros), format) // slice is faster for small sizes than subarray
|
|
212
|
+
}
|
package/base58check.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { typedView } from './array.js'
|
|
2
|
+
import { assertUint8 } from './assert.js'
|
|
3
|
+
import { toBase58, fromBase58 } from './base58.js'
|
|
4
|
+
import { hashSync } from '@exodus/crypto/hash'
|
|
5
|
+
|
|
6
|
+
// Note: while API is async, we use hashSync for now until we improve webcrypto perf for hash256
|
|
7
|
+
// Inputs to base58 are typically very small, and that makes a difference
|
|
8
|
+
|
|
9
|
+
const hash256 = (x) => hashSync('sha256', hashSync('sha256', x, 'uint8'), 'uint8')
|
|
10
|
+
|
|
11
|
+
const E_CHECKSUM = 'Invalid checksum'
|
|
12
|
+
|
|
13
|
+
export async function toBase58check(arr) {
|
|
14
|
+
assertUint8(arr)
|
|
15
|
+
const checksum = hash256(arr)
|
|
16
|
+
const res = new Uint8Array(arr.length + 4)
|
|
17
|
+
res.set(arr, 0)
|
|
18
|
+
res.set(checksum.subarray(0, 4), arr.length)
|
|
19
|
+
return toBase58(res)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function fromBase58check(str, format = 'uint8') {
|
|
23
|
+
const arr = fromBase58(str) // checks input
|
|
24
|
+
const len4 = arr.length - 4
|
|
25
|
+
const payload = arr.subarray(0, len4)
|
|
26
|
+
const c = arr.subarray(len4)
|
|
27
|
+
const r = hash256(payload)
|
|
28
|
+
if ((c[0] ^ r[0]) | (c[1] ^ r[1]) | (c[2] ^ r[2]) | (c[3] ^ r[3])) throw new Error(E_CHECKSUM)
|
|
29
|
+
return typedView(payload, format)
|
|
30
|
+
}
|
package/base64.js
CHANGED
|
@@ -1,94 +1,134 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { assertUint8, assertEmptyRest } from './assert.js'
|
|
2
2
|
import { typedView } from './array.js'
|
|
3
3
|
import * as js from './fallback/base64.js'
|
|
4
4
|
|
|
5
5
|
// See https://datatracker.ietf.org/doc/html/rfc4648
|
|
6
6
|
|
|
7
|
-
// base64: A-Za-z0-9+/ and =
|
|
8
|
-
// base64url: A-Za-z0-9_-
|
|
7
|
+
// base64: A-Za-z0-9+/ and = if padding not disabled
|
|
8
|
+
// base64url: A-Za-z0-9_- and = if padding enabled
|
|
9
9
|
|
|
10
10
|
const { Buffer, atob } = globalThis // Buffer is optional, only used when native
|
|
11
11
|
const haveNativeBuffer = Buffer && !Buffer.TYPED_ARRAY_SUPPORT
|
|
12
12
|
const { toBase64: web64 } = Uint8Array.prototype // Modern engines have this
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
const { E_CHAR, E_PADDING, E_LENGTH, E_LAST } = js
|
|
15
|
+
|
|
16
|
+
const shouldUseAtob = atob && Boolean(globalThis.HermesInternal) // faster only on Hermes (and a little in old Chrome), js path beats it on normal engines
|
|
17
|
+
|
|
18
|
+
// For native Buffer codepaths only
|
|
19
|
+
const isBuffer = (x) => x.constructor === Buffer && Buffer.isBuffer(x)
|
|
20
|
+
const toBuffer = (x) => (isBuffer(x) ? x : Buffer.from(x.buffer, x.byteOffset, x.byteLength))
|
|
21
|
+
|
|
22
|
+
export function toBase64(x, { padding = true } = {}) {
|
|
15
23
|
assertUint8(x)
|
|
16
|
-
if (web64 && x.toBase64 === web64)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
24
|
+
if (web64 && x.toBase64 === web64) {
|
|
25
|
+
return padding ? x.toBase64() : x.toBase64({ omitPadding: !padding }) // Modern, optionless is slightly faster
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!haveNativeBuffer) return js.toBase64(x, false, padding) // Fallback
|
|
29
|
+
const res = toBuffer(x).toString('base64') // Older Node.js
|
|
30
|
+
if (padding) return res
|
|
31
|
+
const at = res.indexOf('=', res.length - 3)
|
|
32
|
+
return at === -1 ? res : res.slice(0, at)
|
|
20
33
|
}
|
|
21
34
|
|
|
22
|
-
// NOTE: base64url omits padding
|
|
23
|
-
export function toBase64url(x) {
|
|
35
|
+
// NOTE: base64url omits padding by default
|
|
36
|
+
export function toBase64url(x, { padding = false } = {}) {
|
|
24
37
|
assertUint8(x)
|
|
25
|
-
if (web64 && x.toBase64 === web64)
|
|
26
|
-
|
|
38
|
+
if (web64 && x.toBase64 === web64) {
|
|
39
|
+
return x.toBase64({ alphabet: 'base64url', omitPadding: !padding }) // Modern
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!haveNativeBuffer) return js.toBase64(x, true, padding) // Fallback
|
|
27
43
|
if (x.constructor === Buffer && Buffer.isBuffer(x)) return x.toString('base64url') // Older Node.js
|
|
28
|
-
|
|
44
|
+
const res = toBuffer(x).toString('base64url') // Older Node.js
|
|
45
|
+
return padding && res.length % 4 !== 0 ? res + '='.repeat(4 - (res.length % 4)) : res
|
|
29
46
|
}
|
|
30
47
|
|
|
31
48
|
// Unlike Buffer.from(), throws on invalid input (non-base64 symbols and incomplete chunks)
|
|
32
49
|
// Unlike Buffer.from() and Uint8Array.fromBase64(), does not allow spaces
|
|
33
50
|
// NOTE: Always operates in strict mode for last chunk
|
|
34
51
|
|
|
35
|
-
//
|
|
36
|
-
export function fromBase64(str,
|
|
37
|
-
if (typeof
|
|
52
|
+
// By default accepts both padded and non-padded variants, only strict base64
|
|
53
|
+
export function fromBase64(str, options = {}) {
|
|
54
|
+
if (typeof options === 'string') options = { format: options } // Compat due to usage, TODO: remove
|
|
55
|
+
const { format = 'uint8', padding = 'both', ...rest } = options
|
|
56
|
+
return fromBase64common(str, false, padding, format, rest)
|
|
57
|
+
}
|
|
38
58
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
assert(str[str.length - 3] !== '=', 'Excessive padding') // no more than two = at the end
|
|
44
|
-
}
|
|
59
|
+
// By default accepts only non-padded strict base64url
|
|
60
|
+
export function fromBase64url(str, { format = 'uint8', padding = false, ...rest } = {}) {
|
|
61
|
+
return fromBase64common(str, true, padding, format, rest)
|
|
62
|
+
}
|
|
45
63
|
|
|
46
|
-
|
|
64
|
+
// By default accepts both padded and non-padded variants, base64 or base64url
|
|
65
|
+
export function fromBase64any(str, { format = 'uint8', padding = 'both', ...rest } = {}) {
|
|
66
|
+
const isBase64url = !str.includes('+') && !str.includes('/') // likely to fail fast, as most input is non-url, also double scan is faster than regex
|
|
67
|
+
return fromBase64common(str, isBase64url, padding, format, rest)
|
|
47
68
|
}
|
|
48
69
|
|
|
49
|
-
|
|
50
|
-
export function fromBase64url(str, format = 'uint8') {
|
|
70
|
+
function fromBase64common(str, isBase64url, padding, format, rest) {
|
|
51
71
|
if (typeof str !== 'string') throw new TypeError('Input is not a string')
|
|
72
|
+
assertEmptyRest(rest)
|
|
73
|
+
const auto = padding === 'both' ? str.endsWith('=') : undefined
|
|
74
|
+
// Older JSC supporting Uint8Array.fromBase64 lacks proper checks
|
|
75
|
+
if (padding === true || auto === true) {
|
|
76
|
+
if (str.length % 4 !== 0) throw new SyntaxError(E_PADDING) // JSC misses this
|
|
77
|
+
if (str[str.length - 3] === '=') throw new SyntaxError(E_PADDING) // no more than two = at the end
|
|
78
|
+
} else if (padding === false || auto === false) {
|
|
79
|
+
if (str.length % 4 === 1) throw new SyntaxError(E_LENGTH) // JSC misses this in fromBase64
|
|
80
|
+
if (padding === false && str.endsWith('=')) {
|
|
81
|
+
throw new SyntaxError('Did not expect padding in base64 input') // inclusion is checked separately
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
throw new TypeError('Invalid padding option')
|
|
85
|
+
}
|
|
52
86
|
|
|
53
|
-
|
|
54
|
-
assert(str.length % 4 !== 1, 'Invalid base64 length') // JSC misses this in fromBase64
|
|
55
|
-
assert(!str.endsWith('='), 'Did not expect padding in base64url input') // inclusion is checked separately
|
|
56
|
-
|
|
57
|
-
return typedView(fromBase64common(str, true), format)
|
|
87
|
+
return typedView(fromBase64impl(str, isBase64url), format)
|
|
58
88
|
}
|
|
59
89
|
|
|
60
|
-
|
|
90
|
+
// ASCII whitespace is U+0009 TAB, U+000A LF, U+000C FF, U+000D CR, or U+0020 SPACE
|
|
91
|
+
const ASCII_WHITESPACE = /[\t\n\f\r ]/ // non-u for JSC perf
|
|
92
|
+
|
|
93
|
+
let fromBase64impl
|
|
61
94
|
if (Uint8Array.fromBase64) {
|
|
62
95
|
// NOTICE: this is actually slower than our JS impl in older JavaScriptCore and (slightly) in SpiderMonkey, but faster on V8 and new JavaScriptCore
|
|
63
|
-
|
|
96
|
+
fromBase64impl = (str, isBase64url) => {
|
|
64
97
|
const alphabet = isBase64url ? 'base64url' : 'base64'
|
|
65
|
-
|
|
98
|
+
if (ASCII_WHITESPACE.test(str)) throw new SyntaxError(E_CHAR) // all other chars are checked natively
|
|
66
99
|
const padded = str.length % 4 > 0 ? `${str}${'='.repeat(4 - (str.length % 4))}` : str
|
|
67
100
|
return Uint8Array.fromBase64(padded, { alphabet, lastChunkHandling: 'strict' })
|
|
68
101
|
}
|
|
69
102
|
} else {
|
|
70
|
-
|
|
71
|
-
if (isBase64url) {
|
|
72
|
-
assert(!/[^0-9a-z_-]/iu.test(str), 'Invalid character in base64url input')
|
|
73
|
-
} else {
|
|
74
|
-
assert(!/[^0-9a-z=+/]/iu.test(str), 'Invalid character in base64 input')
|
|
75
|
-
}
|
|
76
|
-
|
|
103
|
+
fromBase64impl = (str, isBase64url) => {
|
|
77
104
|
let arr
|
|
78
|
-
if (
|
|
105
|
+
if (haveNativeBuffer) {
|
|
106
|
+
const invalidRegex = isBase64url ? /[^0-9a-z=_-]/iu : /[^0-9a-z=+/]/iu
|
|
107
|
+
if (invalidRegex.test(str)) throw new SyntaxError(E_CHAR)
|
|
108
|
+
const at = str.indexOf('=')
|
|
109
|
+
if (at >= 0 && /[^=]/iu.test(str.slice(at))) throw new SyntaxError(E_PADDING)
|
|
110
|
+
arr = Buffer.from(str, 'base64')
|
|
111
|
+
} else if (shouldUseAtob) {
|
|
79
112
|
// atob is faster than manual parsing on Hermes
|
|
80
|
-
|
|
113
|
+
if (isBase64url) {
|
|
114
|
+
if (/[\t\n\f\r +/]/.test(str)) throw new SyntaxError(E_CHAR) // atob verifies other invalid input
|
|
115
|
+
str = str.replaceAll('-', '+').replaceAll('_', '/')
|
|
116
|
+
} else {
|
|
117
|
+
if (ASCII_WHITESPACE.test(str)) throw new SyntaxError(E_CHAR) // all other chars are checked natively
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let raw
|
|
121
|
+
try {
|
|
122
|
+
raw = atob(str)
|
|
123
|
+
} catch {
|
|
124
|
+
throw new SyntaxError(E_CHAR) // convert atob errors
|
|
125
|
+
}
|
|
126
|
+
|
|
81
127
|
const length = raw.length
|
|
82
128
|
arr = new Uint8Array(length)
|
|
83
129
|
for (let i = 0; i < length; i++) arr[i] = raw.charCodeAt(i)
|
|
84
130
|
} else {
|
|
85
|
-
|
|
86
|
-
if (!isBase64url) {
|
|
87
|
-
const at = str.indexOf('=')
|
|
88
|
-
if (at >= 0) assert(!/[^=]/iu.test(str.slice(at)), 'Invalid padding')
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
arr = haveNativeBuffer ? Buffer.from(str, 'base64') : js.fromBase64(str)
|
|
131
|
+
return js.fromBase64(str, isBase64url) // early return to skip last chunk verification, it's already validated in js
|
|
92
132
|
}
|
|
93
133
|
|
|
94
134
|
if (arr.length % 3 !== 0) {
|
|
@@ -96,7 +136,7 @@ if (Uint8Array.fromBase64) {
|
|
|
96
136
|
const expected = toBase64(arr.subarray(-(arr.length % 3)))
|
|
97
137
|
const end = str.length % 4 === 0 ? str.slice(-4) : str.slice(-(str.length % 4)).padEnd(4, '=')
|
|
98
138
|
const actual = isBase64url ? end.replaceAll('-', '+').replaceAll('_', '/') : end
|
|
99
|
-
if (expected !== actual) throw new
|
|
139
|
+
if (expected !== actual) throw new SyntaxError(E_LAST)
|
|
100
140
|
}
|
|
101
141
|
|
|
102
142
|
return arr
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
const { Buffer, TextEncoder, TextDecoder } = globalThis
|
|
2
|
+
const haveNativeBuffer = Buffer && !Buffer.TYPED_ARRAY_SUPPORT
|
|
3
|
+
const isNative = (x) => x && (haveNativeBuffer || `${x}`.includes('[native code]')) // we consider Node.js TextDecoder/TextEncoder native
|
|
4
|
+
const nativeEncoder = isNative(TextEncoder) ? new TextEncoder() : null
|
|
5
|
+
const nativeDecoder = isNative(TextDecoder) ? new TextDecoder('utf8', { ignoreBOM: true }) : null
|
|
6
|
+
export { nativeEncoder, nativeDecoder }
|