@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 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/array.js CHANGED
@@ -2,7 +2,7 @@ import { assertTypedArray } from './assert.js'
2
2
 
3
3
  const { Buffer } = globalThis // Buffer is optional
4
4
 
5
- export function fromTypedArray(arr, format) {
5
+ export function typedView(arr, format) {
6
6
  assertTypedArray(arr)
7
7
  switch (format) {
8
8
  case 'uint8':
package/assert.js CHANGED
@@ -12,10 +12,18 @@ const makeMessage = (name, extra) => `Expected${name ? ` ${name} to be` : ''} an
12
12
  const TypedArray = Object.getPrototypeOf(Uint8Array)
13
13
 
14
14
  export function assertTypedArray(arr) {
15
- assert(arr instanceof TypedArray, 'Expected a TypedArray instance')
15
+ if (arr instanceof TypedArray) return
16
+ throw new TypeError('Expected a TypedArray instance')
16
17
  }
17
18
 
18
- export function assertUint8(arr, { name, length, ...rest } = {}) {
19
+ export function assertUint8(arr, options) {
20
+ if (!options) {
21
+ // fast path
22
+ if (arr instanceof Uint8Array) return
23
+ throw new TypeError('Expected an Uint8Array')
24
+ }
25
+
26
+ const { name, length, ...rest } = options
19
27
  assertEmptyRest(rest)
20
28
  if (arr instanceof Uint8Array && (length === undefined || arr.length === length)) return
21
29
  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/base64.js CHANGED
@@ -1,93 +1,134 @@
1
- import { assert, assertUint8 } from './assert.js'
2
- import { fromTypedArray } from './array.js'
1
+ import { assertUint8, assertEmptyRest } from './assert.js'
2
+ import { typedView } from './array.js'
3
+ import * as js from './fallback/base64.js'
3
4
 
4
5
  // See https://datatracker.ietf.org/doc/html/rfc4648
5
6
 
6
- // base64: A-Za-z0-9+/ and =
7
- // 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
8
9
 
9
10
  const { Buffer, atob } = globalThis // Buffer is optional, only used when native
10
11
  const haveNativeBuffer = Buffer && !Buffer.TYPED_ARRAY_SUPPORT
11
12
  const { toBase64: web64 } = Uint8Array.prototype // Modern engines have this
12
13
 
13
- 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 } = {}) {
14
23
  assertUint8(x)
15
- if (web64 && x.toBase64 === web64) return x.toBase64() // Modern
16
- if (!haveNativeBuffer) return toBase64js(x, BASE64, true) // Fallback
17
- if (x.constructor === Buffer && Buffer.isBuffer(x)) return x.toString('base64') // Older Node.js
18
- 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)
19
33
  }
20
34
 
21
- // NOTE: base64url omits padding
22
- export function toBase64url(x) {
35
+ // NOTE: base64url omits padding by default
36
+ export function toBase64url(x, { padding = false } = {}) {
23
37
  assertUint8(x)
24
- if (web64 && x.toBase64 === web64) return x.toBase64({ alphabet: 'base64url', omitPadding: true }) // Modern
25
- if (!haveNativeBuffer) return toBase64js(x, BASE64URL, 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
26
43
  if (x.constructor === Buffer && Buffer.isBuffer(x)) return x.toString('base64url') // Older Node.js
27
- 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
28
46
  }
29
47
 
30
48
  // Unlike Buffer.from(), throws on invalid input (non-base64 symbols and incomplete chunks)
31
49
  // Unlike Buffer.from() and Uint8Array.fromBase64(), does not allow spaces
32
50
  // NOTE: Always operates in strict mode for last chunk
33
51
 
34
- // Accepts both padded and non-padded variants, only strict base64
35
- export function fromBase64(str, format = 'uint8') {
36
- 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
+ }
37
58
 
38
- // These checks should be needed only for Buffer path, not Uint8Array.fromBase64 path, but JSC lacks proper checks
39
- assert(str.length % 4 !== 1, 'Invalid base64 length') // JSC misses this in fromBase64
40
- if (str.endsWith('=')) {
41
- assert(str.length % 4 === 0, 'Invalid padded length') // JSC misses this too
42
- assert(str[str.length - 3] !== '=', 'Excessive padding') // no more than two = at the end
43
- }
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
+ }
44
63
 
45
- return fromTypedArray(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)
46
68
  }
47
69
 
48
- // Accepts both only non-padded strict base64url
49
- export function fromBase64url(str, format = 'uint8') {
70
+ function fromBase64common(str, isBase64url, padding, format, rest) {
50
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
+ }
51
86
 
52
- // These checks should be needed only for Buffer path, not Uint8Array.fromBase64 path, but JSC lacks proper checks
53
- assert(str.length % 4 !== 1, 'Invalid base64 length') // JSC misses this in fromBase64
54
- assert(!str.endsWith('='), 'Did not expect padding in base64url input') // inclusion is checked separately
55
-
56
- return fromTypedArray(fromBase64common(str, true), format)
87
+ return typedView(fromBase64impl(str, isBase64url), format)
57
88
  }
58
89
 
59
- 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
60
94
  if (Uint8Array.fromBase64) {
61
95
  // NOTICE: this is actually slower than our JS impl in older JavaScriptCore and (slightly) in SpiderMonkey, but faster on V8 and new JavaScriptCore
62
- fromBase64common = (str, isBase64url) => {
96
+ fromBase64impl = (str, isBase64url) => {
63
97
  const alphabet = isBase64url ? 'base64url' : 'base64'
64
- 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
65
99
  const padded = str.length % 4 > 0 ? `${str}${'='.repeat(4 - (str.length % 4))}` : str
66
100
  return Uint8Array.fromBase64(padded, { alphabet, lastChunkHandling: 'strict' })
67
101
  }
68
102
  } else {
69
- fromBase64common = (str, isBase64url) => {
70
- if (isBase64url) {
71
- assert(!/[^0-9a-z_-]/iu.test(str), 'Invalid character in base64url input')
72
- } else {
73
- assert(!/[^0-9a-z=+/]/iu.test(str), 'Invalid character in base64 input')
74
- }
75
-
103
+ fromBase64impl = (str, isBase64url) => {
76
104
  let arr
77
- 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) {
78
112
  // atob is faster than manual parsing on Hermes
79
- 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
+
80
127
  const length = raw.length
81
128
  arr = new Uint8Array(length)
82
129
  for (let i = 0; i < length; i++) arr[i] = raw.charCodeAt(i)
83
130
  } else {
84
- // base64url is already checked to have no padding via a regex above
85
- if (!isBase64url) {
86
- const at = str.indexOf('=')
87
- if (at >= 0) assert(!/[^=]/iu.test(str.slice(at)), 'Invalid padding')
88
- }
89
-
90
- arr = haveNativeBuffer ? Buffer.from(str, 'base64') : fromBase64js(str)
131
+ return js.fromBase64(str, isBase64url) // early return to skip last chunk verification, it's already validated in js
91
132
  }
92
133
 
93
134
  if (arr.length % 3 !== 0) {
@@ -95,111 +136,9 @@ if (Uint8Array.fromBase64) {
95
136
  const expected = toBase64(arr.subarray(-(arr.length % 3)))
96
137
  const end = str.length % 4 === 0 ? str.slice(-4) : str.slice(-(str.length % 4)).padEnd(4, '=')
97
138
  const actual = isBase64url ? end.replaceAll('-', '+').replaceAll('_', '/') : end
98
- if (expected !== actual) throw new Error('Invalid last chunk')
139
+ if (expected !== actual) throw new SyntaxError(E_LAST)
99
140
  }
100
141
 
101
142
  return arr
102
143
  }
103
144
  }
104
-
105
- const BASE64 = [...'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/']
106
- const BASE64URL = [...'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_']
107
-
108
- const BASE64_PAIRS = []
109
- const BASE64URL_PAIRS = []
110
-
111
- // We construct output by concatenating chars, this seems to be fine enough on modern JS engines
112
- function toBase64js(arr, alphabet, padding) {
113
- assertUint8(arr)
114
- const fullChunks = Math.floor(arr.length / 3)
115
- const fullChunksBytes = fullChunks * 3
116
- let o = ''
117
- let i = 0
118
-
119
- const pairs = alphabet === BASE64URL ? BASE64URL_PAIRS : BASE64_PAIRS
120
- if (pairs.length === 0) {
121
- for (let i = 0; i < 64; i++) {
122
- for (let j = 0; j < 64; j++) pairs.push(`${alphabet[i]}${alphabet[j]}`)
123
- }
124
- }
125
-
126
- // Fast path for complete blocks
127
- // This whole loop can be commented out, the algorithm won't change, it's just an optimization of the next loop
128
- for (; i < fullChunksBytes; i += 3) {
129
- const a = arr[i]
130
- const b = arr[i + 1]
131
- const c = arr[i + 2]
132
- o += pairs[(a << 4) | (b >> 4)] + pairs[((b & 0x0f) << 8) | c]
133
- }
134
-
135
- // If we have something left, process it with a full algo
136
- let carry = 0
137
- let shift = 2 // First byte needs to be shifted by 2 to get 6 bits
138
- const length = arr.length
139
- for (; i < length; i++) {
140
- const x = arr[i]
141
- o += alphabet[carry | (x >> shift)] // shift >= 2, so this fits
142
- if (shift === 6) {
143
- shift = 0
144
- o += alphabet[x & 0x3f]
145
- }
146
-
147
- carry = (x << (6 - shift)) & 0x3f
148
- shift += 2 // Each byte prints 6 bits and leaves 2 bits
149
- }
150
-
151
- if (shift !== 2) o += alphabet[carry] // shift 2 means we have no carry left
152
- if (padding) o += ['', '==', '='][length - fullChunksBytes]
153
-
154
- return o
155
- }
156
-
157
- // Assumes no chars after =, checked
158
- let fromBase64jsMap
159
-
160
- function fromBase64js(str) {
161
- const map = fromBase64jsMap || new Array(256)
162
- if (!fromBase64jsMap) {
163
- fromBase64jsMap = map
164
- BASE64.forEach((c, i) => (map[c.charCodeAt(0)] = i))
165
- map['-'.charCodeAt(0)] = map['+'.charCodeAt(0)] // for base64url
166
- map['_'.charCodeAt(0)] = map['/'.charCodeAt(0)] // for base64url
167
- }
168
-
169
- let inputLength = str.length
170
- while (str[inputLength - 1] === '=') inputLength--
171
-
172
- const arr = new Uint8Array(Math.floor((inputLength * 3) / 4))
173
- const tailLength = inputLength % 4
174
- const mainLength = inputLength - tailLength // multiples of 4
175
-
176
- let at = 0
177
- let i = 0
178
- let tmp
179
-
180
- while (i < mainLength) {
181
- tmp =
182
- (map[str.charCodeAt(i)] << 18) |
183
- (map[str.charCodeAt(i + 1)] << 12) |
184
- (map[str.charCodeAt(i + 2)] << 6) |
185
- map[str.charCodeAt(i + 3)]
186
- arr[at++] = tmp >> 16
187
- arr[at++] = (tmp >> 8) & 0xff
188
- arr[at++] = tmp & 0xff
189
- i += 4
190
- }
191
-
192
- if (tailLength === 3) {
193
- tmp =
194
- (map[str.charCodeAt(i)] << 10) |
195
- (map[str.charCodeAt(i + 1)] << 4) |
196
- (map[str.charCodeAt(i + 2)] >> 2)
197
- arr[at++] = (tmp >> 8) & 0xff
198
- arr[at++] = tmp & 0xff
199
- } else if (tailLength === 2) {
200
- tmp = (map[str.charCodeAt(i)] << 2) | (map[str.charCodeAt(i + 1)] >> 4)
201
- arr[at++] = tmp & 0xff
202
- }
203
-
204
- return arr
205
- }
@@ -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 }
@@ -0,0 +1,198 @@
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 BASE32 = [...'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'] // RFC 4648, #6
7
+ const BASE32HEX = [...'0123456789ABCDEFGHIJKLMNOPQRSTUV'] // RFC 4648, #7
8
+ const BASE32_HELPERS = {}
9
+ const BASE32HEX_HELPERS = {}
10
+
11
+ export const E_CHAR = 'Invalid character in base32 input'
12
+ export const E_PADDING = 'Invalid base32 padding'
13
+ export const E_LENGTH = 'Invalid base32 length'
14
+ export const E_LAST = 'Invalid last chunk'
15
+
16
+ // We construct output by concatenating chars, this seems to be fine enough on modern JS engines
17
+ export function toBase32(arr, isBase32Hex, padding) {
18
+ assertUint8(arr)
19
+ const fullChunks = Math.floor(arr.length / 5)
20
+ const fullChunksBytes = fullChunks * 5
21
+ let o = ''
22
+ let i = 0
23
+
24
+ const alphabet = isBase32Hex ? BASE32HEX : BASE32
25
+ const helpers = isBase32Hex ? BASE32HEX_HELPERS : BASE32_HELPERS
26
+ if (!helpers.pairs) {
27
+ helpers.pairs = []
28
+ if (nativeDecoder) {
29
+ // Lazy to save memory in case if this is not needed
30
+ helpers.codepairs = new Uint16Array(32 * 32)
31
+ const u16 = helpers.codepairs
32
+ const u8 = new Uint8Array(u16.buffer, u16.byteOffset, u16.byteLength) // write as 1-byte to ignore BE/LE difference
33
+ for (let i = 0; i < 32; i++) {
34
+ const ic = alphabet[i].charCodeAt(0)
35
+ for (let j = 0; j < 32; j++) u8[(i << 6) | (j << 1)] = u8[(j << 6) | ((i << 1) + 1)] = ic
36
+ }
37
+ } else {
38
+ const p = helpers.pairs
39
+ for (let i = 0; i < 32; i++) {
40
+ for (let j = 0; j < 32; j++) p.push(`${alphabet[i]}${alphabet[j]}`)
41
+ }
42
+ }
43
+ }
44
+
45
+ const { pairs, codepairs } = helpers
46
+
47
+ // Fast path for complete blocks
48
+ // This whole loop can be commented out, the algorithm won't change, it's just an optimization of the next loop
49
+ if (nativeDecoder) {
50
+ const oa = new Uint16Array(fullChunks * 4)
51
+ for (let j = 0; i < fullChunksBytes; i += 5) {
52
+ const a = arr[i]
53
+ const b = arr[i + 1]
54
+ const c = arr[i + 2]
55
+ const d = arr[i + 3]
56
+ 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
61
+ }
62
+
63
+ o = nativeDecoder.decode(oa)
64
+ } else {
65
+ for (; i < fullChunksBytes; i += 5) {
66
+ const a = arr[i]
67
+ const b = arr[i + 1]
68
+ const c = arr[i + 2]
69
+ const d = arr[i + 3]
70
+ 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
75
+ }
76
+ }
77
+
78
+ // If we have something left, process it with a full algo
79
+ let carry = 0
80
+ let shift = 3 // First byte needs to be shifted by 3 to get 5 bits
81
+ for (; i < arr.length; i++) {
82
+ const x = arr[i]
83
+ o += alphabet[carry | (x >> shift)] // shift >= 3, so this fits
84
+ if (shift >= 5) {
85
+ shift -= 5
86
+ o += alphabet[(x >> shift) & 0x1f]
87
+ }
88
+
89
+ carry = (x << (5 - shift)) & 0x1f
90
+ shift += 3 // Each byte prints 5 bits and leaves 3 bits
91
+ }
92
+
93
+ if (shift !== 3) o += alphabet[carry] // shift 3 means we have no carry left
94
+ if (padding) o += ['', '======', '====', '===', '='][arr.length - fullChunksBytes]
95
+
96
+ return o
97
+ }
98
+
99
+ // 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
101
+
102
+ export function fromBase32(str, isBase32Hex) {
103
+ let inputLength = str.length
104
+ while (str[inputLength - 1] === '=') inputLength--
105
+ const paddingLength = str.length - inputLength
106
+ const tailLength = inputLength % 8
107
+ const mainLength = inputLength - tailLength // multiples of 8
108
+ if (![0, 2, 4, 5, 7].includes(tailLength)) throw new SyntaxError(E_LENGTH) // fast verification
109
+ if (paddingLength > 7 || (paddingLength !== 0 && str.length % 8 !== 0)) {
110
+ throw new SyntaxError(E_PADDING)
111
+ }
112
+
113
+ const alphabet = isBase32Hex ? BASE32HEX : BASE32
114
+ const helpers = isBase32Hex ? BASE32HEX_HELPERS : BASE32_HELPERS
115
+
116
+ if (!helpers.fromMap) {
117
+ helpers.fromMap = new Int8Array(mapSize).fill(-1) // no regex input validation here, so we map all other bytes to -1 and recheck sign
118
+ alphabet.forEach((c, i) => {
119
+ helpers.fromMap[c.charCodeAt(0)] = helpers.fromMap[c.toLowerCase().charCodeAt(0)] = i
120
+ })
121
+ }
122
+
123
+ const m = helpers.fromMap
124
+
125
+ const arr = new Uint8Array(Math.floor((inputLength * 5) / 8))
126
+ let at = 0
127
+ let i = 0
128
+
129
+ 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) {
133
+ // 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++]]
136
+ 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
142
+ }
143
+ } else {
144
+ while (i < mainLength) {
145
+ // 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++)]
156
+ 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
162
+ }
163
+ }
164
+
165
+ // Last block, valid tailLength: 0 2 4 5 7, checked already
166
+ // We check last chunk to be strict
167
+ if (tailLength < 2) return arr
168
+ const ab = (m[str.charCodeAt(i++)] << 5) | m[str.charCodeAt(i++)]
169
+ if (ab < 0) throw new SyntaxError(E_CHAR)
170
+ arr[at++] = ab >> 2
171
+ if (tailLength < 4) {
172
+ if (ab & 0x3) throw new SyntaxError(E_LAST)
173
+ return arr
174
+ }
175
+
176
+ const cd = (m[str.charCodeAt(i++)] << 5) | m[str.charCodeAt(i++)]
177
+ if (cd < 0) throw new SyntaxError(E_CHAR)
178
+ arr[at++] = ((ab << 6) & 0xff) | (cd >> 4)
179
+ if (tailLength < 5) {
180
+ if (cd & 0xf) throw new SyntaxError(E_LAST)
181
+ return arr
182
+ }
183
+
184
+ const e = m[str.charCodeAt(i++)]
185
+ if (e < 0) throw new SyntaxError(E_CHAR)
186
+ arr[at++] = ((cd << 4) & 0xff) | (e >> 1) // 4 + 4
187
+ if (tailLength < 7) {
188
+ if (e & 0x1) throw new SyntaxError(E_LAST)
189
+ return arr
190
+ }
191
+
192
+ const fg = (m[str.charCodeAt(i++)] << 5) | m[str.charCodeAt(i++)]
193
+ if (fg < 0) throw new SyntaxError(E_CHAR)
194
+ arr[at++] = ((e << 7) & 0xff) | (fg >> 3) // 1 + 5 + 2
195
+ // Can't be 8, so no h
196
+ if (fg & 0x7) throw new SyntaxError(E_LAST)
197
+ return arr
198
+ }