@exodus/bytes 1.0.0-rc.5 → 1.0.0-rc.7

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/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 CHANGED
@@ -1,6 +1,7 @@
1
1
  import { typedView } from './array.js'
2
2
  import { assertUint8 } from './assert.js'
3
3
  import { nativeDecoder, nativeEncoder } from './fallback/_utils.js'
4
+ import { encodeAscii, decodeAscii } from './fallback/latin1.js'
4
5
 
5
6
  const alphabet = [...'123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz']
6
7
  const codes = new Uint8Array(alphabet.map((x) => x.charCodeAt(0)))
@@ -103,7 +104,7 @@ export function toBase58(arr) {
103
104
  while (carry) {
104
105
  const c = carry
105
106
  carry = Math.floor(c / 58)
106
- res[i++] = c - carry * 58
107
+ res.push(c - carry * 58)
107
108
  }
108
109
  }
109
110
 
@@ -111,7 +112,7 @@ export function toBase58(arr) {
111
112
  const oa = new Uint8Array(res.length)
112
113
  let j = 0
113
114
  for (let i = res.length - 1; i >= 0; i--) oa[j++] = codes[res[i]]
114
- return ZERO.repeat(zeros) + nativeDecoder.decode(oa)
115
+ return ZERO.repeat(zeros) + decodeAscii(oa)
115
116
  }
116
117
 
117
118
  let out = ''
@@ -142,8 +143,7 @@ export function fromBase58(str, format = 'uint8') {
142
143
 
143
144
  // nativeEncoder gives a benefit here
144
145
  if (nativeEncoder) {
145
- const codes = nativeEncoder.encode(str)
146
- if (codes.length !== str.length) throw new SyntaxError(E_CHAR) // non-ascii
146
+ const codes = encodeAscii(str, E_CHAR)
147
147
  for (let i = zeros; i < length; i++) {
148
148
  const c = fromMap[codes[i]]
149
149
  if (c < 0) throw new SyntaxError(E_CHAR)
package/base58check.js CHANGED
@@ -6,25 +6,63 @@ import { hashSync } from '@exodus/crypto/hash'
6
6
  // Note: while API is async, we use hashSync for now until we improve webcrypto perf for hash256
7
7
  // Inputs to base58 are typically very small, and that makes a difference
8
8
 
9
- const hash256 = (x) => hashSync('sha256', hashSync('sha256', x, 'uint8'), 'uint8')
10
-
11
9
  const E_CHECKSUM = 'Invalid checksum'
12
10
 
13
- export async function toBase58check(arr) {
14
- assertUint8(arr)
15
- const checksum = hash256(arr)
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
16
15
  const res = new Uint8Array(arr.length + 4)
17
16
  res.set(arr, 0)
18
17
  res.set(checksum.subarray(0, 4), arr.length)
19
18
  return toBase58(res)
20
19
  }
21
20
 
22
- export async function fromBase58check(str, format = 'uint8') {
21
+ function decodeWithChecksum(str) {
23
22
  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)
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) {
28
29
  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
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).base64Slice(0, x.byteLength), 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).base64urlSlice(0, x.byteLength), 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
 
@@ -3,4 +3,13 @@ const haveNativeBuffer = Buffer && !Buffer.TYPED_ARRAY_SUPPORT
3
3
  const isNative = (x) => x && (haveNativeBuffer || `${x}`.includes('[native code]')) // we consider Node.js TextDecoder/TextEncoder native
4
4
  const nativeEncoder = isNative(TextEncoder) ? new TextEncoder() : null
5
5
  const nativeDecoder = isNative(TextDecoder) ? new TextDecoder('utf8', { ignoreBOM: true }) : null
6
- export { nativeEncoder, nativeDecoder }
6
+ const nativeBuffer = haveNativeBuffer ? Buffer : null
7
+
8
+ // Actually windows-1252, compatible with ascii and latin1 decoding
9
+ // Beware that on non-latin1, i.e. on windows-1252, this is broken in ~all Node.js versions released
10
+ // in 2025 due to a regression, so we call it Latin1 as it's usable only for that
11
+ const nativeDecoderLatin1 = isNative(TextDecoder)
12
+ ? new TextDecoder('latin1', { ignoreBOM: true })
13
+ : null
14
+
15
+ export { nativeEncoder, nativeDecoder, nativeDecoderLatin1, nativeBuffer }
@@ -1,5 +1,6 @@
1
1
  import { assertUint8 } from '../assert.js'
2
2
  import { nativeEncoder, nativeDecoder } from './_utils.js'
3
+ import { encodeAscii, decodeAscii } 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
- o = nativeDecoder.decode(oa)
71
+ o = decodeAscii(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
 
@@ -1,5 +1,6 @@
1
1
  import { assertUint8 } from '../assert.js'
2
2
  import { nativeEncoder, nativeDecoder } from './_utils.js'
3
+ import { encodeAscii, decodeAscii } from './latin1.js'
3
4
 
4
5
  // See https://datatracker.ietf.org/doc/html/rfc4648
5
6
 
@@ -13,13 +14,10 @@ export const E_PADDING = 'Invalid base64 padding'
13
14
  export const E_LENGTH = 'Invalid base64 length'
14
15
  export const E_LAST = 'Invalid last chunk'
15
16
 
16
- // Alternatively, we could have mapped 0-255 bytes to charcodes and just used btoa(ascii),
17
- // but that approach is _slower_ than our toBase64js function, even on Hermes
18
-
19
17
  // We construct output by concatenating chars, this seems to be fine enough on modern JS engines
20
18
  export function toBase64(arr, isURL, padding) {
21
19
  assertUint8(arr)
22
- const fullChunks = Math.floor(arr.length / 3)
20
+ const fullChunks = (arr.length / 3) | 0
23
21
  const fullChunksBytes = fullChunks * 3
24
22
  let o = ''
25
23
  let i = 0
@@ -51,21 +49,49 @@ export function toBase64(arr, isURL, padding) {
51
49
  // This whole loop can be commented out, the algorithm won't change, it's just an optimization of the next loop
52
50
  if (nativeDecoder) {
53
51
  const oa = new Uint16Array(fullChunks * 2)
54
- for (let j = 0; i < fullChunksBytes; i += 3) {
52
+ let j = 0
53
+ for (const last = arr.length - 11; i < last; i += 12, j += 8) {
54
+ const x0 = arr[i]
55
+ const x1 = arr[i + 1]
56
+ const x2 = arr[i + 2]
57
+ const x3 = arr[i + 3]
58
+ const x4 = arr[i + 4]
59
+ const x5 = arr[i + 5]
60
+ const x6 = arr[i + 6]
61
+ const x7 = arr[i + 7]
62
+ const x8 = arr[i + 8]
63
+ const x9 = arr[i + 9]
64
+ const x10 = arr[i + 10]
65
+ const x11 = arr[i + 11]
66
+ oa[j] = codepairs[(x0 << 4) | (x1 >> 4)]
67
+ oa[j + 1] = codepairs[((x1 & 0x0f) << 8) | x2]
68
+ oa[j + 2] = codepairs[(x3 << 4) | (x4 >> 4)]
69
+ oa[j + 3] = codepairs[((x4 & 0x0f) << 8) | x5]
70
+ oa[j + 4] = codepairs[(x6 << 4) | (x7 >> 4)]
71
+ oa[j + 5] = codepairs[((x7 & 0x0f) << 8) | x8]
72
+ oa[j + 6] = codepairs[(x9 << 4) | (x10 >> 4)]
73
+ oa[j + 7] = codepairs[((x10 & 0x0f) << 8) | x11]
74
+ }
75
+
76
+ // i < last here is equivalent to i < fullChunksBytes
77
+ for (const last = arr.length - 2; i < last; i += 3, j += 2) {
55
78
  const a = arr[i]
56
79
  const b = arr[i + 1]
57
80
  const c = arr[i + 2]
58
- oa[j++] = codepairs[(a << 4) | (b >> 4)]
59
- oa[j++] = codepairs[((b & 0x0f) << 8) | c]
81
+ oa[j] = codepairs[(a << 4) | (b >> 4)]
82
+ oa[j + 1] = codepairs[((b & 0x0f) << 8) | c]
60
83
  }
61
84
 
62
- o = nativeDecoder.decode(oa)
85
+ o = decodeAscii(oa)
63
86
  } else {
87
+ // This can be optimized by ~25% with templates on Hermes, but this codepath is not called on Hermes, it uses btoa
88
+ // Check git history for templates version
64
89
  for (; i < fullChunksBytes; i += 3) {
65
90
  const a = arr[i]
66
91
  const b = arr[i + 1]
67
92
  const c = arr[i + 2]
68
- o += pairs[(a << 4) | (b >> 4)] + pairs[((b & 0x0f) << 8) | c]
93
+ o += pairs[(a << 4) | (b >> 4)]
94
+ o += pairs[((b & 0x0f) << 8) | c]
69
95
  }
70
96
  }
71
97
 
@@ -92,9 +118,8 @@ export function toBase64(arr, isURL, padding) {
92
118
  }
93
119
 
94
120
  // TODO: can this be optimized? This only affects non-Hermes barebone engines though
95
- const mapSize = nativeEncoder ? 256 : 65_536 // we have to store 64 KiB map or recheck everything if we can't decode to byte array
121
+ const mapSize = nativeEncoder ? 128 : 65_536 // we have to store 64 KiB map or recheck everything if we can't decode to byte array
96
122
 
97
- // Last chunk is rechecked at API
98
123
  export function fromBase64(str, isURL) {
99
124
  let inputLength = str.length
100
125
  while (str[inputLength - 1] === '=') inputLength--
@@ -121,26 +146,31 @@ export function fromBase64(str, isURL) {
121
146
  let i = 0
122
147
 
123
148
  if (nativeEncoder) {
124
- const codes = nativeEncoder.encode(str)
125
- if (codes.length !== str.length) throw new SyntaxError(E_CHAR) // non-ascii
126
- while (i < mainLength) {
127
- const a = (m[codes[i++]] << 18) | (m[codes[i++]] << 12) | (m[codes[i++]] << 6) | m[codes[i++]]
149
+ const codes = encodeAscii(str, E_CHAR)
150
+ for (; i < mainLength; i += 4) {
151
+ const c0 = codes[i]
152
+ const c1 = codes[i + 1]
153
+ const c2 = codes[i + 2]
154
+ const c3 = codes[i + 3]
155
+ const a = (m[c0] << 18) | (m[c1] << 12) | (m[c2] << 6) | m[c3]
128
156
  if (a < 0) throw new SyntaxError(E_CHAR)
129
- arr[at++] = a >> 16
130
- arr[at++] = (a >> 8) & 0xff
131
- arr[at++] = a & 0xff
157
+ arr[at] = a >> 16
158
+ arr[at + 1] = (a >> 8) & 0xff
159
+ arr[at + 2] = a & 0xff
160
+ at += 3
132
161
  }
133
162
  } else {
134
- while (i < mainLength) {
135
- const a =
136
- (m[str.charCodeAt(i++)] << 18) |
137
- (m[str.charCodeAt(i++)] << 12) |
138
- (m[str.charCodeAt(i++)] << 6) |
139
- m[str.charCodeAt(i++)]
163
+ for (; i < mainLength; i += 4) {
164
+ const c0 = str.charCodeAt(i)
165
+ const c1 = str.charCodeAt(i + 1)
166
+ const c2 = str.charCodeAt(i + 2)
167
+ const c3 = str.charCodeAt(i + 3)
168
+ const a = (m[c0] << 18) | (m[c1] << 12) | (m[c2] << 6) | m[c3]
140
169
  if (a < 0) throw new SyntaxError(E_CHAR)
141
- arr[at++] = a >> 16
142
- arr[at++] = (a >> 8) & 0xff
143
- arr[at++] = a & 0xff
170
+ arr[at] = a >> 16
171
+ arr[at + 1] = (a >> 8) & 0xff
172
+ arr[at + 2] = a & 0xff
173
+ at += 3
144
174
  }
145
175
  }
146
176