@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 CHANGED
@@ -1,3 +1,11 @@
1
- # bytes
1
+ # `@exodus/bytes`
2
2
 
3
- Data structures handling
3
+ `Uint8Array` conversion to and from `base64`, `base32`, `hex` and `utf8`
4
+
5
+ [Fast](./Performance.md)
6
+
7
+ Performs proper input validation
8
+
9
+ ## License
10
+
11
+ [MIT](./LICENSE)
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, { name, length, ...rest } = {}) {
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 { assert, assertUint8 } from './assert.js'
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
- export function toBase64(x) {
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) return x.toBase64() // Modern
17
- if (!haveNativeBuffer) return js.toBase64(x, false, true) // Fallback
18
- if (x.constructor === Buffer && Buffer.isBuffer(x)) return x.toString('base64') // Older Node.js
19
- return Buffer.from(x.buffer, x.byteOffset, x.byteLength).toString('base64') // Older Node.js
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) return x.toBase64({ alphabet: 'base64url', omitPadding: true }) // Modern
26
- if (!haveNativeBuffer) return js.toBase64(x, true, false) // Fallback
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
- return Buffer.from(x.buffer, x.byteOffset, x.byteLength).toString('base64url') // Older Node.js
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
- // Accepts both padded and non-padded variants, only strict base64
36
- export function fromBase64(str, format = 'uint8') {
37
- if (typeof str !== 'string') throw new TypeError('Input is not a string')
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
- // These checks should be needed only for Buffer path, not Uint8Array.fromBase64 path, but JSC lacks proper checks
40
- assert(str.length % 4 !== 1, 'Invalid base64 length') // JSC misses this in fromBase64
41
- if (str.endsWith('=')) {
42
- assert(str.length % 4 === 0, 'Invalid padded length') // JSC misses this too
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
- return typedView(fromBase64common(str, false), format)
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
- // Accepts both only non-padded strict base64url
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
- // These checks should be needed only for Buffer path, not Uint8Array.fromBase64 path, but JSC lacks proper checks
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
- let fromBase64common
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
- fromBase64common = (str, isBase64url) => {
96
+ fromBase64impl = (str, isBase64url) => {
64
97
  const alphabet = isBase64url ? 'base64url' : 'base64'
65
- assert(!/\s/u.test(str), `Invalid character in ${alphabet} input`) // all other chars are checked natively
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
- fromBase64common = (str, isBase64url) => {
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 (!haveNativeBuffer && atob) {
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
- const raw = atob(isBase64url ? str.replaceAll('-', '+').replaceAll('_', '/') : str)
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
- // base64url is already checked to have no padding via a regex above
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 Error('Invalid last chunk')
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 }