@exodus/bytes 1.0.0-rc.4 → 1.0.0-rc.6

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/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
  }
package/base32.js CHANGED
@@ -12,14 +12,21 @@ export const toBase32 = (arr, { padding = false } = {}) => js.toBase32(arr, fals
12
12
  export const toBase32hex = (arr, { padding = false } = {}) => js.toBase32(arr, true, padding)
13
13
 
14
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)
15
+ export function fromBase32(str, options) {
16
+ if (!options) return fromBase32common(str, false, 'both', 'uint8', null)
17
+ const { format = 'uint8', padding = 'both', ...rest } = options
18
+ return fromBase32common(str, false, padding, format, rest)
19
+ }
20
+
21
+ export function fromBase32hex(str, options) {
22
+ if (!options) return fromBase32common(str, true, 'both', 'uint8', null)
23
+ const { format = 'uint8', padding = 'both', ...rest } = options
24
+ return fromBase32common(str, true, padding, format, rest)
25
+ }
19
26
 
20
27
  function fromBase32common(str, isBase32Hex, padding, format, rest) {
21
28
  if (typeof str !== 'string') throw new TypeError('Input is not a string')
22
- assertEmptyRest(rest)
29
+ if (rest !== null) assertEmptyRest(rest)
23
30
 
24
31
  if (padding === true) {
25
32
  if (str.length % 8 !== 0) throw new SyntaxError(E_PADDING)
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
+ import { encodeAscii } from './fallback/latin1.js'
5
+
6
+ const alphabet = [...'123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz']
7
+ const codes = new Uint8Array(alphabet.map((x) => x.charCodeAt(0)))
8
+ const ZERO = alphabet[0]
9
+ const zeroC = codes[0]
10
+
11
+ const _0n = BigInt(0)
12
+ const _1n = BigInt(1)
13
+ const _8n = BigInt(8)
14
+ const _32n = BigInt(32)
15
+ const _58n = BigInt(58)
16
+ const _0xffffffffn = BigInt(0xff_ff_ff_ff)
17
+
18
+ let table // 15 * 82, diagonal, <1kb
19
+ let fromMap
20
+
21
+ const E_CHAR = 'Invalid character in base58 input'
22
+
23
+ const shouldUseBigIntFrom = Boolean(globalThis.HermesInternal) // faster only on Hermes, numbers path beats it on normal engines
24
+
25
+ export function toBase58(arr) {
26
+ assertUint8(arr)
27
+ const length = arr.length
28
+ if (length === 0) return ''
29
+
30
+ let zeros = 0
31
+ while (zeros < length && arr[zeros] === 0) zeros++
32
+
33
+ if (length > 60) {
34
+ // Slow path. Can be optimized ~10%, but the main factor is /58n division anyway, so doesn't matter much
35
+ let x = _0n
36
+ for (let i = 0; i < arr.length; i++) x = (x << _8n) | BigInt(arr[i])
37
+
38
+ let out = ''
39
+ while (x) {
40
+ const d = x / _58n
41
+ out = alphabet[Number(x - _58n * d)] + out
42
+ x = d
43
+ }
44
+
45
+ return ZERO.repeat(zeros) + out
46
+ }
47
+
48
+ // We run fast mode operations only on short (<=60 bytes) inputs, via precomputation table
49
+ if (!table) {
50
+ table = []
51
+ let x = _1n
52
+ for (let i = 0; i < 15; i++) {
53
+ // Convert x to base 58 digits
54
+ const in58 = []
55
+ let y = x
56
+ while (y) {
57
+ const d = y / _58n
58
+ in58.push(Number(y - _58n * d))
59
+ y = d
60
+ }
61
+
62
+ table.push(new Uint8Array(in58))
63
+ x <<= _32n
64
+ }
65
+ }
66
+
67
+ const res = []
68
+ {
69
+ let j = 0
70
+ // We group each 4 bytes into 32-bit chunks
71
+ // Not using u32arr to not deal with remainder + BE/LE differences
72
+ for (let i = length - 1; i >= 0; i -= 4) {
73
+ let c
74
+ if (i > 2) {
75
+ c = (arr[i] | (arr[i - 1] << 8) | (arr[i - 2] << 16) | (arr[i - 3] << 24)) >>> 0
76
+ } else if (i > 1) {
77
+ c = arr[i] | (arr[i - 1] << 8) | (arr[i - 2] << 16)
78
+ } else {
79
+ c = i === 1 ? arr[i] | (arr[i - 1] << 8) : arr[i]
80
+ }
81
+
82
+ const row = table[j++]
83
+ if (c === 0) continue
84
+ const olen = res.length
85
+ const nlen = row.length
86
+ let k = 0
87
+ for (; k < olen; k++) res[k] += c * row[k]
88
+ while (k < nlen) res.push(c * row[k++])
89
+ }
90
+ }
91
+
92
+ // We can now do a single scan over regular numbers under MAX_SAFE_INTEGER
93
+ // Note: can't use int32 operations on them, as they are outside of 2**32 range
94
+ // This is faster though
95
+ {
96
+ let carry = 0
97
+ let i = 0
98
+ while (i < res.length) {
99
+ const c = res[i] + carry
100
+ carry = Math.floor(c / 58)
101
+ res[i++] = c - carry * 58
102
+ }
103
+
104
+ while (carry) {
105
+ const c = carry
106
+ carry = Math.floor(c / 58)
107
+ res[i++] = c - carry * 58
108
+ }
109
+ }
110
+
111
+ if (nativeDecoder) {
112
+ const oa = new Uint8Array(res.length)
113
+ let j = 0
114
+ for (let i = res.length - 1; i >= 0; i--) oa[j++] = codes[res[i]]
115
+ return ZERO.repeat(zeros) + nativeDecoder.decode(oa)
116
+ }
117
+
118
+ let out = ''
119
+ for (let i = res.length - 1; i >= 0; i--) out += alphabet[res[i]]
120
+ return ZERO.repeat(zeros) + out
121
+ }
122
+
123
+ // TODO: test on 'z'.repeat(from 1 to smth)
124
+ export function fromBase58(str, format = 'uint8') {
125
+ if (typeof str !== 'string') throw new TypeError('Input is not a string')
126
+ const length = str.length
127
+ if (length === 0) return typedView(new Uint8Array(), format)
128
+
129
+ let zeros = 0
130
+ while (zeros < length && str.charCodeAt(zeros) === zeroC) zeros++
131
+
132
+ if (!fromMap) {
133
+ fromMap = new Int8Array(256).fill(-1)
134
+ for (let i = 0; i < 58; i++) fromMap[alphabet[i].charCodeAt(0)] = i
135
+ }
136
+
137
+ const size = zeros + (((length - zeros) * 3 + 1) >> 2) // 3/4 rounded up, larger than ~0.73 coef to fit everything
138
+ const res = new Uint8Array(size)
139
+ let at = size // where is the first significant byte written
140
+
141
+ if (shouldUseBigIntFrom) {
142
+ let x = _0n
143
+
144
+ // nativeEncoder gives a benefit here
145
+ if (nativeEncoder) {
146
+ const codes = encodeAscii(str, E_CHAR)
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,68 @@
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 E_CHECKSUM = 'Invalid checksum'
10
+
11
+ // checksum length is 4, i.e. only the first 4 bytes of the hash are used
12
+
13
+ function encodeWithChecksum(arr, checksum) {
14
+ // arr type in already validated in input
15
+ const res = new Uint8Array(arr.length + 4)
16
+ res.set(arr, 0)
17
+ res.set(checksum.subarray(0, 4), arr.length)
18
+ return toBase58(res)
19
+ }
20
+
21
+ function decodeWithChecksum(str) {
22
+ const arr = fromBase58(str) // checks input
23
+ const payloadSize = arr.length - 4
24
+ if (payloadSize < 0) throw new Error(E_CHECKSUM)
25
+ return [arr.subarray(0, payloadSize), arr.subarray(payloadSize)]
26
+ }
27
+
28
+ function assertChecksum(c, r) {
29
+ if ((c[0] ^ r[0]) | (c[1] ^ r[1]) | (c[2] ^ r[2]) | (c[3] ^ r[3])) throw new Error(E_CHECKSUM)
30
+ }
31
+
32
+ export const makeBase58check = (hashAlgo, hashAlgoSync) => {
33
+ const apis = {
34
+ async encode(arr) {
35
+ assertUint8(arr)
36
+ return encodeWithChecksum(arr, await hashAlgo(arr))
37
+ },
38
+ async decode(str, format = 'uint8') {
39
+ const [payload, checksum] = decodeWithChecksum(str)
40
+ assertChecksum(checksum, await hashAlgo(payload))
41
+ return typedView(payload, format)
42
+ },
43
+ }
44
+ if (!hashAlgoSync) return apis
45
+ return {
46
+ ...apis,
47
+ encodeSync(arr) {
48
+ assertUint8(arr)
49
+ return encodeWithChecksum(arr, hashAlgoSync(arr))
50
+ },
51
+ decodeSync(str, format = 'uint8') {
52
+ const [payload, checksum] = decodeWithChecksum(str)
53
+ assertChecksum(checksum, hashAlgoSync(payload))
54
+ return typedView(payload, format)
55
+ },
56
+ }
57
+ }
58
+
59
+ const hash256sync = (x) => hashSync('sha256', hashSync('sha256', x, 'uint8'), 'uint8')
60
+ const hash256 = hash256sync // See note at the top
61
+ const {
62
+ encode: toBase58check,
63
+ decode: fromBase58check,
64
+ encodeSync: toBase58checkSync,
65
+ decodeSync: fromBase58checkSync,
66
+ } = makeBase58check(hash256, hash256sync)
67
+
68
+ export { toBase58check, fromBase58check, toBase58checkSync, fromBase58checkSync }
package/base64.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { assertUint8, assertEmptyRest } from './assert.js'
2
2
  import { typedView } from './array.js'
3
+ import { decodeLatin1, encodeLatin1 } from './fallback/latin1.js'
3
4
  import * as js from './fallback/base64.js'
4
5
 
5
6
  // See https://datatracker.ietf.org/doc/html/rfc4648
@@ -7,42 +8,49 @@ import * as js from './fallback/base64.js'
7
8
  // base64: A-Za-z0-9+/ and = if padding not disabled
8
9
  // base64url: A-Za-z0-9_- and = if padding enabled
9
10
 
10
- const { Buffer, atob } = globalThis // Buffer is optional, only used when native
11
+ const { Buffer, atob, btoa } = globalThis // Buffer is optional, only used when native
11
12
  const haveNativeBuffer = Buffer && !Buffer.TYPED_ARRAY_SUPPORT
12
13
  const { toBase64: web64 } = Uint8Array.prototype // Modern engines have this
13
14
 
14
15
  const { E_CHAR, E_PADDING, E_LENGTH, E_LAST } = js
15
16
 
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
+ // faster only on Hermes (and a little in old Chrome), js path beats it on normal engines
18
+ const shouldUseBtoa = btoa && Boolean(globalThis.HermesInternal)
19
+ const shouldUseAtob = atob && Boolean(globalThis.HermesInternal)
17
20
 
18
21
  // For native Buffer codepaths only
19
22
  const isBuffer = (x) => x.constructor === Buffer && Buffer.isBuffer(x)
20
23
  const toBuffer = (x) => (isBuffer(x) ? x : Buffer.from(x.buffer, x.byteOffset, x.byteLength))
21
24
 
22
- export function toBase64(x, { padding = true } = {}) {
23
- assertUint8(x)
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
25
+ function maybeUnpad(res, padding) {
30
26
  if (padding) return res
31
27
  const at = res.indexOf('=', res.length - 3)
32
28
  return at === -1 ? res : res.slice(0, at)
33
29
  }
34
30
 
31
+ function maybePad(res, padding) {
32
+ return padding && res.length % 4 !== 0 ? res + '='.repeat(4 - (res.length % 4)) : res
33
+ }
34
+
35
+ const toUrl = (x) => x.replaceAll('+', '-').replaceAll('/', '_')
36
+ const fromUrl = (x) => x.replaceAll('-', '+').replaceAll('_', '/')
37
+ const haveWeb = (x) => web64 && x.toBase64 === web64
38
+
39
+ export function toBase64(x, { padding = true } = {}) {
40
+ assertUint8(x)
41
+ if (haveWeb(x)) return padding ? x.toBase64() : x.toBase64({ omitPadding: !padding }) // Modern, optionless is slightly faster
42
+ if (haveNativeBuffer) return maybeUnpad(toBuffer(x).toString('base64'), padding) // Older Node.js
43
+ if (shouldUseBtoa) return maybeUnpad(btoa(decodeLatin1(x)), padding)
44
+ return js.toBase64(x, false, padding) // Fallback
45
+ }
46
+
35
47
  // NOTE: base64url omits padding by default
36
48
  export function toBase64url(x, { padding = false } = {}) {
37
49
  assertUint8(x)
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
43
- if (x.constructor === Buffer && Buffer.isBuffer(x)) return x.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
50
+ if (haveWeb(x)) return x.toBase64({ alphabet: 'base64url', omitPadding: !padding }) // Modern
51
+ if (haveNativeBuffer) return maybePad(toBuffer(x).toString('base64url'), padding) // Older Node.js
52
+ if (shouldUseBtoa) return maybeUnpad(toUrl(btoa(decodeLatin1(x))), padding)
53
+ return js.toBase64(x, true, padding) // Fallback
46
54
  }
47
55
 
48
56
  // Unlike Buffer.from(), throws on invalid input (non-base64 symbols and incomplete chunks)
@@ -50,14 +58,17 @@ export function toBase64url(x, { padding = false } = {}) {
50
58
  // NOTE: Always operates in strict mode for last chunk
51
59
 
52
60
  // By default accepts both padded and non-padded variants, only strict base64
53
- export function fromBase64(str, options = {}) {
61
+ export function fromBase64(str, options) {
54
62
  if (typeof options === 'string') options = { format: options } // Compat due to usage, TODO: remove
63
+ if (!options) return fromBase64common(str, false, 'both', 'uint8', null)
55
64
  const { format = 'uint8', padding = 'both', ...rest } = options
56
65
  return fromBase64common(str, false, padding, format, rest)
57
66
  }
58
67
 
59
68
  // By default accepts only non-padded strict base64url
60
- export function fromBase64url(str, { format = 'uint8', padding = false, ...rest } = {}) {
69
+ export function fromBase64url(str, options) {
70
+ if (!options) return fromBase64common(str, true, false, 'uint8', null)
71
+ const { format = 'uint8', padding = false, ...rest } = options
61
72
  return fromBase64common(str, true, padding, format, rest)
62
73
  }
63
74
 
@@ -69,7 +80,7 @@ export function fromBase64any(str, { format = 'uint8', padding = 'both', ...rest
69
80
 
70
81
  function fromBase64common(str, isBase64url, padding, format, rest) {
71
82
  if (typeof str !== 'string') throw new TypeError('Input is not a string')
72
- assertEmptyRest(rest)
83
+ if (rest !== null) assertEmptyRest(rest)
73
84
  const auto = padding === 'both' ? str.endsWith('=') : undefined
74
85
  // Older JSC supporting Uint8Array.fromBase64 lacks proper checks
75
86
  if (padding === true || auto === true) {
@@ -84,49 +95,70 @@ function fromBase64common(str, isBase64url, padding, format, rest) {
84
95
  throw new TypeError('Invalid padding option')
85
96
  }
86
97
 
87
- return typedView(fromBase64impl(str, isBase64url), format)
98
+ return typedView(fromBase64impl(str, isBase64url, padding), format)
88
99
  }
89
100
 
90
101
  // ASCII whitespace is U+0009 TAB, U+000A LF, U+000C FF, U+000D CR, or U+0020 SPACE
91
102
  const ASCII_WHITESPACE = /[\t\n\f\r ]/ // non-u for JSC perf
92
103
 
104
+ function noWhitespaceSeen(str, arr) {
105
+ const at = str.indexOf('=', str.length - 3)
106
+ const paddingLength = at >= 0 ? str.length - at : 0
107
+ const chars = str.length - paddingLength
108
+ const e = chars % 4 // extra chars past blocks of 4
109
+ const b = arr.length - ((chars - e) / 4) * 3 // remaining bytes not covered by full blocks of chars
110
+ return (e === 0 && b === 0) || (e === 2 && b === 1) || (e === 3 && b === 2)
111
+ }
112
+
93
113
  let fromBase64impl
94
114
  if (Uint8Array.fromBase64) {
95
115
  // NOTICE: this is actually slower than our JS impl in older JavaScriptCore and (slightly) in SpiderMonkey, but faster on V8 and new JavaScriptCore
96
- fromBase64impl = (str, isBase64url) => {
116
+ fromBase64impl = (str, isBase64url, padding) => {
97
117
  const alphabet = isBase64url ? 'base64url' : 'base64'
98
- if (ASCII_WHITESPACE.test(str)) throw new SyntaxError(E_CHAR) // all other chars are checked natively
99
- const padded = str.length % 4 > 0 ? `${str}${'='.repeat(4 - (str.length % 4))}` : str
100
- return Uint8Array.fromBase64(padded, { alphabet, lastChunkHandling: 'strict' })
118
+
119
+ let arr
120
+ if (padding === true) {
121
+ // Padding is required from user, and we already checked that string length is divisible by 4
122
+ // Padding might still be wrong due to whitespace, but in that case native impl throws expected error
123
+ arr = Uint8Array.fromBase64(str, { alphabet, lastChunkHandling: 'strict' })
124
+ } else {
125
+ try {
126
+ const padded = str.length % 4 > 0 ? `${str}${'='.repeat(4 - (str.length % 4))}` : str
127
+ arr = Uint8Array.fromBase64(padded, { alphabet, lastChunkHandling: 'strict' })
128
+ } catch (err) {
129
+ // Normalize error: whitespace in input could have caused added padding to be invalid
130
+ // But reporting that as a padding error would be confusing
131
+ throw ASCII_WHITESPACE.test(str) ? new SyntaxError(E_CHAR) : err
132
+ }
133
+ }
134
+
135
+ // We don't allow whitespace in input, but that can be rechecked based on output length
136
+ // All other chars are checked natively
137
+ if (!noWhitespaceSeen(str, arr)) throw new SyntaxError(E_CHAR)
138
+ return arr
101
139
  }
102
140
  } else {
103
- fromBase64impl = (str, isBase64url) => {
141
+ fromBase64impl = (str, isBase64url, padding) => {
104
142
  let arr
105
143
  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
144
  arr = Buffer.from(str, 'base64')
145
+ // Rechecking is cheaper than regexes on Node.js
146
+ const r = isBase64url ? maybeUnpad(str, padding === false) : maybePad(str, padding !== true)
147
+ if (r !== arr.toString(isBase64url ? 'base64url' : 'base64')) throw new SyntaxError(E_PADDING)
111
148
  } else if (shouldUseAtob) {
112
149
  // atob is faster than manual parsing on Hermes
113
150
  if (isBase64url) {
114
151
  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
152
+ str = fromUrl(str)
118
153
  }
119
154
 
120
- let raw
121
155
  try {
122
- raw = atob(str)
156
+ arr = encodeLatin1(atob(str))
123
157
  } catch {
124
158
  throw new SyntaxError(E_CHAR) // convert atob errors
125
159
  }
126
160
 
127
- const length = raw.length
128
- arr = new Uint8Array(length)
129
- for (let i = 0; i < length; i++) arr[i] = raw.charCodeAt(i)
161
+ if (!isBase64url && !noWhitespaceSeen(str, arr)) throw new SyntaxError(E_CHAR) // base64url checks input above
130
162
  } else {
131
163
  return js.fromBase64(str, isBase64url) // early return to skip last chunk verification, it's already validated in js
132
164
  }
@@ -135,7 +167,7 @@ if (Uint8Array.fromBase64) {
135
167
  // Check last chunk to be strict if it was incomplete
136
168
  const expected = toBase64(arr.subarray(-(arr.length % 3)))
137
169
  const end = str.length % 4 === 0 ? str.slice(-4) : str.slice(-(str.length % 4)).padEnd(4, '=')
138
- const actual = isBase64url ? end.replaceAll('-', '+').replaceAll('_', '/') : end
170
+ const actual = isBase64url ? fromUrl(end) : end
139
171
  if (expected !== actual) throw new SyntaxError(E_LAST)
140
172
  }
141
173
 
@@ -1,5 +1,6 @@
1
1
  import { assertUint8 } from '../assert.js'
2
2
  import { nativeEncoder, nativeDecoder } from './_utils.js'
3
+ import { encodeAscii } from './latin1.js'
3
4
 
4
5
  // See https://datatracker.ietf.org/doc/html/rfc4648
5
6
 
@@ -13,6 +14,8 @@ export const E_PADDING = 'Invalid base32 padding'
13
14
  export const E_LENGTH = 'Invalid base32 length'
14
15
  export const E_LAST = 'Invalid last chunk'
15
16
 
17
+ const useTemplates = Boolean(globalThis.HermesInternal) // Faster on Hermes and JSC, but we use it only on Hermes
18
+
16
19
  // We construct output by concatenating chars, this seems to be fine enough on modern JS engines
17
20
  export function toBase32(arr, isBase32Hex, padding) {
18
21
  assertUint8(arr)
@@ -54,13 +57,32 @@ export function toBase32(arr, isBase32Hex, padding) {
54
57
  const c = arr[i + 2]
55
58
  const d = arr[i + 3]
56
59
  const e = arr[i + 4]
57
- oa[j++] = codepairs[(a << 2) | (b >> 6)] // 8 + 8 - 5 - 5 = 6 left
58
- oa[j++] = codepairs[((b & 0x3f) << 4) | (c >> 4)] // 6 + 8 - 5 - 5 = 4 left
59
- oa[j++] = codepairs[((c & 0xf) << 6) | (d >> 2)] // 4 + 8 - 5 - 5 = 2 left
60
- oa[j++] = codepairs[((d & 0x3) << 8) | e] // 2 + 8 - 5 - 5 = 0 left
60
+ const x0 = (a << 2) | (b >> 6) // 8 + 8 - 5 - 5 = 6 left
61
+ const x1 = ((b & 0x3f) << 4) | (c >> 4) // 6 + 8 - 5 - 5 = 4 left
62
+ const x2 = ((c & 0xf) << 6) | (d >> 2) // 4 + 8 - 5 - 5 = 2 left
63
+ const x3 = ((d & 0x3) << 8) | e // 2 + 8 - 5 - 5 = 0 left
64
+ oa[j] = codepairs[x0]
65
+ oa[j + 1] = codepairs[x1]
66
+ oa[j + 2] = codepairs[x2]
67
+ oa[j + 3] = codepairs[x3]
68
+ j += 4
61
69
  }
62
70
 
63
71
  o = nativeDecoder.decode(oa)
72
+ } else if (useTemplates) {
73
+ // Templates are faster only on Hermes and JSC. Browsers have TextDecoder anyway
74
+ for (; i < fullChunksBytes; i += 5) {
75
+ const a = arr[i]
76
+ const b = arr[i + 1]
77
+ const c = arr[i + 2]
78
+ const d = arr[i + 3]
79
+ const e = arr[i + 4]
80
+ const x0 = (a << 2) | (b >> 6) // 8 + 8 - 5 - 5 = 6 left
81
+ const x1 = ((b & 0x3f) << 4) | (c >> 4) // 6 + 8 - 5 - 5 = 4 left
82
+ const x2 = ((c & 0xf) << 6) | (d >> 2) // 4 + 8 - 5 - 5 = 2 left
83
+ const x3 = ((d & 0x3) << 8) | e // 2 + 8 - 5 - 5 = 0 left
84
+ o += `${pairs[x0]}${pairs[x1]}${pairs[x2]}${pairs[x3]}`
85
+ }
64
86
  } else {
65
87
  for (; i < fullChunksBytes; i += 5) {
66
88
  const a = arr[i]
@@ -68,10 +90,14 @@ export function toBase32(arr, isBase32Hex, padding) {
68
90
  const c = arr[i + 2]
69
91
  const d = arr[i + 3]
70
92
  const e = arr[i + 4]
71
- o += pairs[(a << 2) | (b >> 6)] // 8 + 8 - 5 - 5 = 6 left
72
- o += pairs[((b & 0x3f) << 4) | (c >> 4)] // 6 + 8 - 5 - 5 = 4 left
73
- o += pairs[((c & 0xf) << 6) | (d >> 2)] // 4 + 8 - 5 - 5 = 2 left
74
- o += pairs[((d & 0x3) << 8) | e] // 2 + 8 - 5 - 5 = 0 left
93
+ const x0 = (a << 2) | (b >> 6) // 8 + 8 - 5 - 5 = 6 left
94
+ const x1 = ((b & 0x3f) << 4) | (c >> 4) // 6 + 8 - 5 - 5 = 4 left
95
+ const x2 = ((c & 0xf) << 6) | (d >> 2) // 4 + 8 - 5 - 5 = 2 left
96
+ const x3 = ((d & 0x3) << 8) | e // 2 + 8 - 5 - 5 = 0 left
97
+ o += pairs[x0]
98
+ o += pairs[x1]
99
+ o += pairs[x2]
100
+ o += pairs[x3]
75
101
  }
76
102
  }
77
103
 
@@ -97,7 +123,7 @@ export function toBase32(arr, isBase32Hex, padding) {
97
123
  }
98
124
 
99
125
  // TODO: can this be optimized? This only affects non-Hermes barebone engines though
100
- const mapSize = nativeEncoder ? 256 : 65_536 // we have to store 64 KiB map or recheck everything if we can't decode to byte array
126
+ const mapSize = nativeEncoder ? 128 : 65_536 // we have to store 64 KiB map or recheck everything if we can't decode to byte array
101
127
 
102
128
  export function fromBase32(str, isBase32Hex) {
103
129
  let inputLength = str.length
@@ -127,38 +153,47 @@ export function fromBase32(str, isBase32Hex) {
127
153
  let i = 0
128
154
 
129
155
  if (nativeEncoder) {
130
- const codes = nativeEncoder.encode(str)
131
- if (codes.length !== str.length) throw new SyntaxError(E_CHAR) // non-ascii
132
- while (i < mainLength) {
156
+ const codes = encodeAscii(str, E_CHAR)
157
+ for (; i < mainLength; i += 8) {
133
158
  // each 5 bits, grouped 5 * 4 = 20
134
- const a = (m[codes[i++]] << 15) | (m[codes[i++]] << 10) | (m[codes[i++]] << 5) | m[codes[i++]]
135
- const b = (m[codes[i++]] << 15) | (m[codes[i++]] << 10) | (m[codes[i++]] << 5) | m[codes[i++]]
159
+ const x0 = codes[i]
160
+ const x1 = codes[i + 1]
161
+ const x2 = codes[i + 2]
162
+ const x3 = codes[i + 3]
163
+ const x4 = codes[i + 4]
164
+ const x5 = codes[i + 5]
165
+ const x6 = codes[i + 6]
166
+ const x7 = codes[i + 7]
167
+ const a = (m[x0] << 15) | (m[x1] << 10) | (m[x2] << 5) | m[x3]
168
+ const b = (m[x4] << 15) | (m[x5] << 10) | (m[x6] << 5) | m[x7]
136
169
  if (a < 0 || b < 0) throw new SyntaxError(E_CHAR)
137
- arr[at++] = a >> 12
138
- arr[at++] = (a >> 4) & 0xff
139
- arr[at++] = ((a << 4) & 0xff) | (b >> 16)
140
- arr[at++] = (b >> 8) & 0xff
141
- arr[at++] = b & 0xff
170
+ arr[at] = a >> 12
171
+ arr[at + 1] = (a >> 4) & 0xff
172
+ arr[at + 2] = ((a << 4) & 0xff) | (b >> 16)
173
+ arr[at + 3] = (b >> 8) & 0xff
174
+ arr[at + 4] = b & 0xff
175
+ at += 5
142
176
  }
143
177
  } else {
144
- while (i < mainLength) {
178
+ for (; i < mainLength; i += 8) {
145
179
  // each 5 bits, grouped 5 * 4 = 20
146
- const a =
147
- (m[str.charCodeAt(i++)] << 15) |
148
- (m[str.charCodeAt(i++)] << 10) |
149
- (m[str.charCodeAt(i++)] << 5) |
150
- m[str.charCodeAt(i++)]
151
- const b =
152
- (m[str.charCodeAt(i++)] << 15) |
153
- (m[str.charCodeAt(i++)] << 10) |
154
- (m[str.charCodeAt(i++)] << 5) |
155
- m[str.charCodeAt(i++)]
180
+ const x0 = str.charCodeAt(i)
181
+ const x1 = str.charCodeAt(i + 1)
182
+ const x2 = str.charCodeAt(i + 2)
183
+ const x3 = str.charCodeAt(i + 3)
184
+ const x4 = str.charCodeAt(i + 4)
185
+ const x5 = str.charCodeAt(i + 5)
186
+ const x6 = str.charCodeAt(i + 6)
187
+ const x7 = str.charCodeAt(i + 7)
188
+ const a = (m[x0] << 15) | (m[x1] << 10) | (m[x2] << 5) | m[x3]
189
+ const b = (m[x4] << 15) | (m[x5] << 10) | (m[x6] << 5) | m[x7]
156
190
  if (a < 0 || b < 0) throw new SyntaxError(E_CHAR)
157
- arr[at++] = a >> 12
158
- arr[at++] = (a >> 4) & 0xff
159
- arr[at++] = ((a << 4) & 0xff) | (b >> 16)
160
- arr[at++] = (b >> 8) & 0xff
161
- arr[at++] = b & 0xff
191
+ arr[at] = a >> 12
192
+ arr[at + 1] = (a >> 4) & 0xff
193
+ arr[at + 2] = ((a << 4) & 0xff) | (b >> 16)
194
+ arr[at + 3] = (b >> 8) & 0xff
195
+ arr[at + 4] = b & 0xff
196
+ at += 5
162
197
  }
163
198
  }
164
199