@exodus/bytes 1.0.0-rc.6 → 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/base58.js CHANGED
@@ -1,7 +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 } from './fallback/latin1.js'
4
+ import { encodeAscii, decodeAscii } from './fallback/latin1.js'
5
5
 
6
6
  const alphabet = [...'123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz']
7
7
  const codes = new Uint8Array(alphabet.map((x) => x.charCodeAt(0)))
@@ -104,7 +104,7 @@ export function toBase58(arr) {
104
104
  while (carry) {
105
105
  const c = carry
106
106
  carry = Math.floor(c / 58)
107
- res[i++] = c - carry * 58
107
+ res.push(c - carry * 58)
108
108
  }
109
109
  }
110
110
 
@@ -112,7 +112,7 @@ export function toBase58(arr) {
112
112
  const oa = new Uint8Array(res.length)
113
113
  let j = 0
114
114
  for (let i = res.length - 1; i >= 0; i--) oa[j++] = codes[res[i]]
115
- return ZERO.repeat(zeros) + nativeDecoder.decode(oa)
115
+ return ZERO.repeat(zeros) + decodeAscii(oa)
116
116
  }
117
117
 
118
118
  let out = ''
package/base64.js CHANGED
@@ -39,7 +39,7 @@ const haveWeb = (x) => web64 && x.toBase64 === web64
39
39
  export function toBase64(x, { padding = true } = {}) {
40
40
  assertUint8(x)
41
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
42
+ if (haveNativeBuffer) return maybeUnpad(toBuffer(x).base64Slice(0, x.byteLength), padding) // Older Node.js
43
43
  if (shouldUseBtoa) return maybeUnpad(btoa(decodeLatin1(x)), padding)
44
44
  return js.toBase64(x, false, padding) // Fallback
45
45
  }
@@ -48,7 +48,7 @@ export function toBase64(x, { padding = true } = {}) {
48
48
  export function toBase64url(x, { padding = false } = {}) {
49
49
  assertUint8(x)
50
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
51
+ if (haveNativeBuffer) return maybePad(toBuffer(x).base64urlSlice(0, x.byteLength), padding) // Older Node.js
52
52
  if (shouldUseBtoa) return maybeUnpad(toUrl(btoa(decodeLatin1(x))), padding)
53
53
  return js.toBase64(x, true, padding) // Fallback
54
54
  }
@@ -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,6 +1,6 @@
1
1
  import { assertUint8 } from '../assert.js'
2
2
  import { nativeEncoder, nativeDecoder } from './_utils.js'
3
- import { encodeAscii } from './latin1.js'
3
+ import { encodeAscii, decodeAscii } from './latin1.js'
4
4
 
5
5
  // See https://datatracker.ietf.org/doc/html/rfc4648
6
6
 
@@ -68,7 +68,7 @@ export function toBase32(arr, isBase32Hex, padding) {
68
68
  j += 4
69
69
  }
70
70
 
71
- o = nativeDecoder.decode(oa)
71
+ o = decodeAscii(oa)
72
72
  } else if (useTemplates) {
73
73
  // Templates are faster only on Hermes and JSC. Browsers have TextDecoder anyway
74
74
  for (; i < fullChunksBytes; i += 5) {
@@ -1,6 +1,6 @@
1
1
  import { assertUint8 } from '../assert.js'
2
2
  import { nativeEncoder, nativeDecoder } from './_utils.js'
3
- import { encodeAscii } from './latin1.js'
3
+ import { encodeAscii, decodeAscii } from './latin1.js'
4
4
 
5
5
  // See https://datatracker.ietf.org/doc/html/rfc4648
6
6
 
@@ -82,7 +82,7 @@ export function toBase64(arr, isURL, padding) {
82
82
  oa[j + 1] = codepairs[((b & 0x0f) << 8) | c]
83
83
  }
84
84
 
85
- o = nativeDecoder.decode(oa)
85
+ o = decodeAscii(oa)
86
86
  } else {
87
87
  // This can be optimized by ~25% with templates on Hermes, but this codepath is not called on Hermes, it uses btoa
88
88
  // Check git history for templates version
package/fallback/hex.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { assertUint8 } from '../assert.js'
2
2
  import { nativeDecoder, nativeEncoder } from './_utils.js'
3
- import { encodeAscii } from './latin1.js'
3
+ import { encodeAscii, decodeAscii } from './latin1.js'
4
4
 
5
5
  let hexArray // array of 256 bytes converted to two-char hex strings
6
6
  let hexCodes // hexArray converted to u16 code pairs
@@ -97,7 +97,7 @@ export function toHex(arr) {
97
97
  }
98
98
 
99
99
  for (; i < length; i++) oa[i] = hexCodes[arr[i]]
100
- return nativeDecoder.decode(oa)
100
+ return decodeAscii(oa)
101
101
  }
102
102
 
103
103
  if (length > 30_000) {
@@ -1,4 +1,4 @@
1
- import { nativeEncoder } from './_utils.js'
1
+ import { nativeEncoder, nativeDecoder, nativeDecoderLatin1, nativeBuffer } from './_utils.js'
2
2
 
3
3
  // See http://stackoverflow.com/a/22747272/680742, which says that lowest limit is in Chrome, with 0xffff args
4
4
  // On Hermes, actual max is 0x20_000 minus current stack depth, 1/16 of that should be safe
@@ -32,6 +32,7 @@ export function asciiPrefix(arr) {
32
32
  return length
33
33
  }
34
34
 
35
+ // Capable of decoding Uint16Array to UTF-16 as well as Uint8Array to Latin-1
35
36
  export function decodeLatin1(arr, start = 0, stop = arr.length) {
36
37
  start |= 0
37
38
  stop |= 0
@@ -52,10 +53,23 @@ export function decodeLatin1(arr, start = 0, stop = arr.length) {
52
53
  return String.fromCharCode.apply(String, sliced)
53
54
  }
54
55
 
55
- export const encodeLatin1 = globalThis.HermesInternal
56
- ? (str) => {
56
+ // Does not check input, uses best available method
57
+ // Building an array for this is only faster than proper string concatenation when TextDecoder or native Buffer are available
58
+ export const decodeAscii = nativeBuffer
59
+ ? (a) =>
60
+ // Buffer is faster on Node.js (but only for long enough data), if we know that output is ascii
61
+ a.byteLength >= 0x3_00
62
+ ? nativeBuffer.from(a.buffer, a.byteOffset, a.byteLength).latin1Slice(0, a.byteLength) // .latin1Slice is faster than .asciiSlice
63
+ : nativeDecoder.decode(a) // On Node.js, utf8 decoder is faster than latin1
64
+ : nativeDecoderLatin1
65
+ ? (a) => nativeDecoderLatin1.decode(a) // On browsers (specifically WebKit), latin1 decoder is faster than utf8
66
+ : (a) => decodeLatin1(new Uint8Array(a.buffer, a.byteOffset, a.byteLength)) // Fallback. We shouldn't get here, constructing with strings directly is faster
67
+
68
+ /* eslint-disable @exodus/mutable/no-param-reassign-prop-only */
69
+
70
+ export const encodeCharcodes = globalThis.HermesInternal
71
+ ? (str, arr) => {
57
72
  const length = str.length
58
- const arr = new Uint8Array(length)
59
73
  if (length > 64) {
60
74
  const at = str.charCodeAt.bind(str) // faster on strings from ~64 chars on Hermes, but can be 10x slower on e.g. JSC
61
75
  for (let i = 0; i < length; i++) arr[i] = at(i)
@@ -65,14 +79,17 @@ export const encodeLatin1 = globalThis.HermesInternal
65
79
 
66
80
  return arr
67
81
  }
68
- : (str) => {
82
+ : (str, arr) => {
69
83
  const length = str.length
70
- const arr = new Uint8Array(length)
71
84
  // Can be optimized with unrolling, but this is not used on non-Hermes atm
72
85
  for (let i = 0; i < length; i++) arr[i] = str.charCodeAt(i)
73
86
  return arr
74
87
  }
75
88
 
89
+ /* eslint-enable @exodus/mutable/no-param-reassign-prop-only */
90
+
91
+ export const encodeLatin1 = (str) => encodeCharcodes(str, new Uint8Array(str.length))
92
+
76
93
  // Expects nativeEncoder to be present
77
94
  export const encodeAscii = globalThis.HermesInternal
78
95
  ? (str, ERR) => {
@@ -82,8 +99,15 @@ export const encodeAscii = globalThis.HermesInternal
82
99
  if (info.read !== str.length || info.written !== str.length) throw new SyntaxError(ERR) // non-ascii
83
100
  return codes.subarray(0, str.length)
84
101
  }
85
- : (str, ERR) => {
86
- const codes = nativeEncoder.encode(str)
87
- if (codes.length !== str.length) throw new SyntaxError(ERR) // non-ascii
88
- return codes
89
- }
102
+ : nativeBuffer
103
+ ? (str, ERR) => {
104
+ // TextEncoder is slow on Node.js 24 / 25 (was ok on 22)
105
+ const codes = nativeBuffer.from(str, 'utf8') // ascii/latin1 coerces, we need to check
106
+ if (codes.length !== str.length) throw new SyntaxError(ERR) // non-ascii
107
+ return new Uint8Array(codes.buffer, codes.byteOffset, codes.byteLength)
108
+ }
109
+ : (str, ERR) => {
110
+ const codes = nativeEncoder.encode(str)
111
+ if (codes.length !== str.length) throw new SyntaxError(ERR) // non-ascii
112
+ return codes
113
+ }
package/hex.js CHANGED
@@ -2,30 +2,16 @@ import { assertUint8 } from './assert.js'
2
2
  import { typedView } from './array.js'
3
3
  import * as js from './fallback/hex.js'
4
4
 
5
- const { Buffer } = globalThis // Buffer is optional, only used when native
6
- const haveNativeBuffer = Buffer && !Buffer.TYPED_ARRAY_SUPPORT
7
5
  const { toHex: webHex } = Uint8Array.prototype // Modern engines have this
8
6
 
9
- const { E_HEX } = js
10
-
11
7
  export function toHex(arr) {
12
8
  assertUint8(arr)
13
9
  if (arr.length === 0) return ''
14
10
  if (webHex && arr.toHex === webHex) return arr.toHex()
15
- if (!haveNativeBuffer) return js.toHex(arr)
16
- if (arr.constructor === Buffer && Buffer.isBuffer(arr)) return arr.toString('hex')
17
- return Buffer.from(arr.buffer, arr.byteOffset, arr.byteLength).toString('hex')
11
+ return js.toHex(arr)
18
12
  }
19
13
 
20
14
  // Unlike Buffer.from(), throws on invalid input
21
15
  export const fromHex = Uint8Array.fromHex
22
16
  ? (str, format = 'uint8') => typedView(Uint8Array.fromHex(str), format)
23
- : haveNativeBuffer
24
- ? (str, format = 'uint8') => {
25
- if (typeof str !== 'string') throw new TypeError('Input is not a string')
26
- if (str.length % 2 !== 0) throw new SyntaxError(E_HEX)
27
- const buf = Buffer.from(str, 'hex') // will stop on first non-hex character, so we can just validate length
28
- if (buf.length * 2 !== str.length) throw new SyntaxError(E_HEX)
29
- return typedView(buf, format)
30
- }
31
- : (str, format = 'uint8') => typedView(js.fromHex(str), format)
17
+ : (str, format = 'uint8') => typedView(js.fromHex(str), format)
package/hex.node.js ADDED
@@ -0,0 +1,26 @@
1
+ import { assertUint8 } from './assert.js'
2
+ import { typedView } from './array.js'
3
+ import { E_HEX } from './fallback/hex.js'
4
+
5
+ if (Buffer.TYPED_ARRAY_SUPPORT) throw new Error('Unexpected Buffer polyfill')
6
+
7
+ const { toHex: webHex } = Uint8Array.prototype // Modern engines have this
8
+
9
+ export function toHex(arr) {
10
+ assertUint8(arr)
11
+ if (arr.length === 0) return ''
12
+ if (webHex && arr.toHex === webHex) return arr.toHex()
13
+ if (arr.constructor === Buffer && Buffer.isBuffer(arr)) return arr.hexSlice(0, arr.byteLength)
14
+ return Buffer.from(arr.buffer, arr.byteOffset, arr.byteLength).hexSlice(0, arr.byteLength)
15
+ }
16
+
17
+ // Unlike Buffer.from(), throws on invalid input
18
+ export const fromHex = Uint8Array.fromHex
19
+ ? (str, format = 'uint8') => typedView(Uint8Array.fromHex(str), format)
20
+ : (str, format = 'uint8') => {
21
+ if (typeof str !== 'string') throw new TypeError('Input is not a string')
22
+ if (str.length % 2 !== 0) throw new SyntaxError(E_HEX)
23
+ const buf = Buffer.from(str, 'hex') // will stop on first non-hex character, so we can just validate length
24
+ if (buf.length * 2 !== str.length) throw new SyntaxError(E_HEX)
25
+ return typedView(buf, format)
26
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/bytes",
3
- "version": "1.0.0-rc.6",
3
+ "version": "1.0.0-rc.7",
4
4
  "description": "Various operations on Uint8Array data",
5
5
  "scripts": {
6
6
  "lint": "eslint .",
@@ -50,7 +50,9 @@
50
50
  "/base58.js",
51
51
  "/base58check.js",
52
52
  "/base64.js",
53
+ "/hex.node.js",
53
54
  "/hex.js",
55
+ "/utf8.node.js",
54
56
  "/utf8.js"
55
57
  ],
56
58
  "exports": {
@@ -59,8 +61,18 @@
59
61
  "./base58.js": "./base58.js",
60
62
  "./base58check.js": "./base58check.js",
61
63
  "./base64.js": "./base64.js",
62
- "./hex.js": "./hex.js",
63
- "./utf8.js": "./utf8.js"
64
+ "./hex.js": {
65
+ "node": "./hex.node.js",
66
+ "default": "./hex.js"
67
+ },
68
+ "./utf16.js": {
69
+ "node": "./utf16.node.js",
70
+ "default": "./utf16.js"
71
+ },
72
+ "./utf8.js": {
73
+ "node": "./utf8.node.js",
74
+ "default": "./utf8.js"
75
+ }
64
76
  },
65
77
  "peerDependencies": {
66
78
  "@exodus/crypto": "^1.0.0-rc.4"
@@ -94,6 +106,7 @@
94
106
  "fast-base64-encode": "^1.0.0",
95
107
  "hextreme": "^1.0.7",
96
108
  "hi-base32": "^0.5.1",
109
+ "iconv-lite": "^0.7.0",
97
110
  "jsvu": "^3.0.0",
98
111
  "text-encoding": "^0.7.0",
99
112
  "typescript": "^5.9.3"
package/utf8.node.js ADDED
@@ -0,0 +1,33 @@
1
+ import { assertUint8 } from './assert.js'
2
+ import { typedView } from './array.js'
3
+ import { E_STRICT_UNICODE } from './fallback/utf8.js'
4
+ import { isAscii } from 'node:buffer'
5
+
6
+ if (Buffer.TYPED_ARRAY_SUPPORT) throw new Error('Unexpected Buffer polyfill')
7
+
8
+ const decoderFatal = new TextDecoder('utf8', { ignoreBOM: true, fatal: true })
9
+ const decoderLoose = new TextDecoder('utf8', { ignoreBOM: true })
10
+ const { isWellFormed } = String.prototype
11
+
12
+ function encode(str, loose = false) {
13
+ if (typeof str !== 'string') throw new TypeError('Input is not a string')
14
+ const res = Buffer.from(str)
15
+ if (loose || str.length === res.length || isWellFormed.call(str)) return res // length is equal only for ascii, which is automatically fine
16
+ throw new TypeError(E_STRICT_UNICODE)
17
+ }
18
+
19
+ function decode(arr, loose = false) {
20
+ assertUint8(arr)
21
+ if (isAscii(arr)) {
22
+ // On non-ascii strings, this loses ~10% * [relative position of the first non-ascii byte] (up to 10% total)
23
+ // On ascii strings, this wins 1.5x on loose = false and 1.3x on loose = true
24
+ return Buffer.from(arr.buffer, arr.byteOffset, arr.byteLength).latin1Slice(0, arr.byteLength) // .latin1Slice is faster than .asciiSlice
25
+ }
26
+
27
+ return loose ? decoderLoose.decode(arr) : decoderFatal.decode(arr)
28
+ }
29
+
30
+ export const utf8fromString = (str, format = 'uint8') => typedView(encode(str, false), format)
31
+ export const utf8fromStringLoose = (str, format = 'uint8') => typedView(encode(str, true), format)
32
+ export const utf8toString = (arr) => decode(arr, false)
33
+ export const utf8toStringLoose = (arr) => decode(arr, true)