@exodus/bytes 1.1.0 → 1.3.0

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
@@ -41,6 +41,7 @@ Spec compliant, passing WPT and covered with extra tests.
41
41
  Moreover, tests for this library uncovered [bugs in all major implementations](https://docs.google.com/spreadsheets/d/1pdEefRG6r9fZy61WHGz0TKSt8cO4ISWqlpBN5KntIvQ/edit).
42
42
 
43
43
  [Faster than Node.js native implementation on Node.js](https://github.com/nodejs/node/issues/61041#issuecomment-3649242024).
44
+ Runs (and passes WPT) on Node.js built without ICU.
44
45
 
45
46
  ### Caveat: `TextDecoder` / `TextEncoder` APIs are lossy by default per spec
46
47
 
@@ -160,6 +161,8 @@ Same as `windows1252toString = createSinglebyteDecoder('windows-1252')`.
160
161
 
161
162
  ### `@exodus/bytes/base58check.js`
162
163
 
164
+ On non-Node.js, requires peer dependency [@exodus/crypto](https://www.npmjs.com/package/@exodus/crypto) to be installed.
165
+
163
166
  ##### `async toBase58check(arr)`
164
167
  ##### `toBase58checkSync(arr)`
165
168
  ##### `async fromBase58check(str, format = 'uint8')`
@@ -184,7 +187,7 @@ some [hooks](https://encoding.spec.whatwg.org/#specification-hooks) (see below).
184
187
  import { TextDecoder, TextDecoder } from '@exodus/bytes/encoding.js'
185
188
 
186
189
  // Hooks for standards
187
- import { getBOMEncoding, legacyHookDecode, normalizeEncoding } from '@exodus/bytes/encoding.js'
190
+ import { getBOMEncoding, legacyHookDecode, labelToName, normalizeEncoding } from '@exodus/bytes/encoding.js'
188
191
  ```
189
192
 
190
193
  #### `new TextDecoder(label = 'utf-8', { fatal = false, ignoreBOM = false })`
@@ -195,10 +198,19 @@ import { getBOMEncoding, legacyHookDecode, normalizeEncoding } from '@exodus/byt
195
198
 
196
199
  [TextEncoder](https://encoding.spec.whatwg.org/#interface-textdecoder) implementation/polyfill.
197
200
 
198
- #### `normalizeEncoding(label)`
201
+ #### `labelToName(label)`
199
202
 
200
203
  Implements [get an encoding from a string `label`](https://encoding.spec.whatwg.org/#concept-encoding-get).
201
204
 
205
+ Converts an encoding [label](https://encoding.spec.whatwg.org/#names-and-labels) to its name,
206
+ as a case-sensitive string.
207
+
208
+ If an encoding with that label does not exist, returns `null`.
209
+
210
+ All encoding names are also valid labels for corresponding encodings.
211
+
212
+ #### `normalizeEncoding(label)`
213
+
202
214
  Converts an encoding [label](https://encoding.spec.whatwg.org/#names-and-labels) to its name,
203
215
  as an ASCII-lowercased string.
204
216
 
@@ -210,6 +222,11 @@ except that it:
210
222
  [labels](https://encoding.spec.whatwg.org/#ref-for-replacement%E2%91%A1)
211
223
  2. Does not throw for invalid labels and instead returns `null`
212
224
 
225
+ It is identical to:
226
+ ```js
227
+ labelToName(label)?.toLowerCase() ?? null
228
+ ```
229
+
213
230
  All encoding names are also valid labels for corresponding encodings.
214
231
 
215
232
  #### `getBOMEncoding(input)`
@@ -251,7 +268,7 @@ new TextDecoder(getBOMEncoding(input) ?? fallbackEncoding).decode(input)
251
268
  import { TextDecoder, TextDecoder } from '@exodus/bytes/encoding-lite.js'
252
269
 
253
270
  // Hooks for standards
254
- import { getBOMEncoding, legacyHookDecode, normalizeEncoding } from '@exodus/bytes/encoding-lite.js'
271
+ import { getBOMEncoding, legacyHookDecode, labelToName, normalizeEncoding } from '@exodus/bytes/encoding-lite.js'
255
272
  ```
256
273
 
257
274
  The exact same exports as `@exodus/bytes/encoding.js` are also exported as
@@ -263,7 +280,7 @@ and their [labels](https://encoding.spec.whatwg.org/#names-and-labels) when used
263
280
 
264
281
  Legacy single-byte encodingds are loaded by default in both cases.
265
282
 
266
- `TextEncoder` and hooks for standards (including `normalizeEncoding`) do not have any behavior
283
+ `TextEncoder` and hooks for standards (including `labelToName` / `normalizeEncoding`) do not have any behavior
267
284
  differences in the lite version and support full range if inputs.
268
285
 
269
286
  To avoid inconsistencies, the exported classes and methods are exactly the same objects.
@@ -274,6 +291,7 @@ To avoid inconsistencies, the exported classes and methods are exactly the same
274
291
  TextDecoder: [class TextDecoder],
275
292
  TextEncoder: [class TextEncoder],
276
293
  getBOMEncoding: [Function: getBOMEncoding],
294
+ labelToName: [Function: labelToName],
277
295
  legacyHookDecode: [Function: legacyHookDecode],
278
296
  normalizeEncoding: [Function: normalizeEncoding]
279
297
  }
@@ -286,6 +304,7 @@ Error: Legacy multi-byte encodings are disabled in /encoding-lite.js, use /encod
286
304
  TextDecoder: [class TextDecoder],
287
305
  TextEncoder: [class TextEncoder],
288
306
  getBOMEncoding: [Function: getBOMEncoding],
307
+ labelToName: [Function: labelToName],
289
308
  legacyHookDecode: [Function: legacyHookDecode],
290
309
  normalizeEncoding: [Function: normalizeEncoding]
291
310
  }
package/base58check.js CHANGED
@@ -1,63 +1,12 @@
1
- import { typedView } from './array.js'
2
- import { assertUint8 } from './assert.js'
3
- import { toBase58, fromBase58 } from './base58.js'
4
1
  import { hashSync } from '@exodus/crypto/hash' // eslint-disable-line @exodus/import/no-deprecated
2
+ import { makeBase58check } from './fallback/base58check.js'
5
3
 
6
4
  // Note: while API is async, we use hashSync for now until we improve webcrypto perf for hash256
7
5
  // Inputs to base58 are typically very small, and that makes a difference
8
6
 
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
7
  // eslint-disable-next-line @exodus/import/no-deprecated
60
- const hash256sync = (x) => hashSync('sha256', hashSync('sha256', x, 'uint8'), 'uint8')
8
+ const sha256 = (x) => hashSync('sha256', x, 'uint8')
9
+ const hash256sync = (x) => sha256(sha256(x))
61
10
  const hash256 = hash256sync // See note at the top
62
11
  const {
63
12
  encode: toBase58check,
@@ -66,4 +15,5 @@ const {
66
15
  decodeSync: fromBase58checkSync,
67
16
  } = makeBase58check(hash256, hash256sync)
68
17
 
18
+ export { makeBase58check } from './fallback/base58check.js'
69
19
  export { toBase58check, fromBase58check, toBase58checkSync, fromBase58checkSync }
@@ -0,0 +1,14 @@
1
+ import { hash } from 'node:crypto'
2
+ import { makeBase58check } from './fallback/base58check.js'
3
+
4
+ const sha256 = (x) => hash('sha256', x, 'buffer')
5
+ const hash256 = (x) => sha256(sha256(x))
6
+ const {
7
+ encode: toBase58check,
8
+ decode: fromBase58check,
9
+ encodeSync: toBase58checkSync,
10
+ decodeSync: fromBase58checkSync,
11
+ } = makeBase58check(hash256, hash256)
12
+
13
+ export { makeBase58check } from './fallback/base58check.js'
14
+ export { toBase58check, fromBase58check, toBase58checkSync, fromBase58checkSync }
package/encoding-lite.js CHANGED
@@ -3,5 +3,6 @@ export {
3
3
  TextEncoder,
4
4
  normalizeEncoding,
5
5
  getBOMEncoding,
6
+ labelToName,
6
7
  legacyHookDecode,
7
8
  } from './fallback/encoding.js'
package/encoding.js CHANGED
@@ -8,5 +8,6 @@ export {
8
8
  TextEncoder,
9
9
  normalizeEncoding,
10
10
  getBOMEncoding,
11
+ labelToName,
11
12
  legacyHookDecode,
12
13
  } from './fallback/encoding.js'
@@ -0,0 +1,53 @@
1
+ import { typedView } from '@exodus/bytes/array.js'
2
+ import { toBase58, fromBase58 } from '@exodus/bytes/base58.js'
3
+ import { assertUint8 } from '../assert.js'
4
+
5
+ const E_CHECKSUM = 'Invalid checksum'
6
+
7
+ // checksum length is 4, i.e. only the first 4 bytes of the hash are used
8
+
9
+ function encodeWithChecksum(arr, checksum) {
10
+ // arr type in already validated in input
11
+ const res = new Uint8Array(arr.length + 4)
12
+ res.set(arr, 0)
13
+ res.set(checksum.subarray(0, 4), arr.length)
14
+ return toBase58(res)
15
+ }
16
+
17
+ function decodeWithChecksum(str) {
18
+ const arr = fromBase58(str) // checks input
19
+ const payloadSize = arr.length - 4
20
+ if (payloadSize < 0) throw new Error(E_CHECKSUM)
21
+ return [arr.subarray(0, payloadSize), arr.subarray(payloadSize)]
22
+ }
23
+
24
+ function assertChecksum(c, r) {
25
+ if ((c[0] ^ r[0]) | (c[1] ^ r[1]) | (c[2] ^ r[2]) | (c[3] ^ r[3])) throw new Error(E_CHECKSUM)
26
+ }
27
+
28
+ export const makeBase58check = (hashAlgo, hashAlgoSync) => {
29
+ const apis = {
30
+ async encode(arr) {
31
+ assertUint8(arr)
32
+ return encodeWithChecksum(arr, await hashAlgo(arr))
33
+ },
34
+ async decode(str, format = 'uint8') {
35
+ const [payload, checksum] = decodeWithChecksum(str)
36
+ assertChecksum(checksum, await hashAlgo(payload))
37
+ return typedView(payload, format)
38
+ },
39
+ }
40
+ if (!hashAlgoSync) return apis
41
+ return {
42
+ ...apis,
43
+ encodeSync(arr) {
44
+ assertUint8(arr)
45
+ return encodeWithChecksum(arr, hashAlgoSync(arr))
46
+ },
47
+ decodeSync(str, format = 'uint8') {
48
+ const [payload, checksum] = decodeWithChecksum(str)
49
+ assertChecksum(checksum, hashAlgoSync(payload))
50
+ return typedView(payload, format)
51
+ },
52
+ }
53
+ }
@@ -288,3 +288,16 @@ export function legacyHookDecode(input, fallbackEncoding = 'utf-8') {
288
288
 
289
289
  return createSinglebyteDecoder(enc, true)(u8)
290
290
  }
291
+
292
+ const uppercasePrefixes = new Set(['utf', 'iso', 'koi', 'euc', 'ibm', 'gbk'])
293
+
294
+ // Unlike normalizeEncoding, case-sensitive
295
+ // https://encoding.spec.whatwg.org/#names-and-labels
296
+ export function labelToName(label) {
297
+ const enc = normalizeEncoding(label)
298
+ if (!enc) return enc
299
+ if (uppercasePrefixes.has(enc.slice(0, 3))) return enc.toUpperCase()
300
+ if (enc === 'big5') return 'Big5'
301
+ if (enc === 'shift_jis') return 'Shift_JIS'
302
+ return enc
303
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/bytes",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "Various operations on Uint8Array data",
5
5
  "scripts": {
6
6
  "lint": "eslint .",
@@ -45,6 +45,7 @@
45
45
  "files": [
46
46
  "/fallback/_utils.js",
47
47
  "/fallback/base32.js",
48
+ "/fallback/base58check.js",
48
49
  "/fallback/base64.js",
49
50
  "/fallback/encoding.js",
50
51
  "/fallback/encoding.labels.js",
@@ -65,6 +66,7 @@
65
66
  "/base32.js",
66
67
  "/base58.js",
67
68
  "/base58check.js",
69
+ "/base58check.node.js",
68
70
  "/base64.js",
69
71
  "/base64.d.ts",
70
72
  "/bech32.js",
@@ -92,7 +94,10 @@
92
94
  },
93
95
  "./base32.js": "./base32.js",
94
96
  "./base58.js": "./base58.js",
95
- "./base58check.js": "./base58check.js",
97
+ "./base58check.js": {
98
+ "node": "./base58check.node.js",
99
+ "default": "./base58check.js"
100
+ },
96
101
  "./base64.js": {
97
102
  "types": "./base64.d.ts",
98
103
  "default": "./base64.js"
package/utf16.js CHANGED
@@ -9,7 +9,7 @@ const decoderFatalBE = canDecoders ? new TextDecoder('utf-16be', { ignoreBOM, fa
9
9
  const decoderLooseBE = canDecoders ? new TextDecoder('utf-16be', { ignoreBOM }) : null
10
10
  const decoderFatal16 = isLE ? decoderFatalLE : decoderFatalBE
11
11
  const decoderLoose16 = isLE ? decoderLooseLE : decoderFatalBE
12
- const { isWellFormed } = String.prototype
12
+ const { isWellFormed, toWellFormed } = String.prototype
13
13
 
14
14
  const { E_STRICT, E_STRICT_UNICODE } = js
15
15
 
@@ -61,8 +61,9 @@ function decode(input, loose = false, format = 'uint16') {
61
61
  throw new TypeError('Unknown format')
62
62
  }
63
63
 
64
- const str = js.decode(u16, loose, !loose && isWellFormed)
64
+ const str = js.decode(u16, loose, (!loose && isWellFormed) || (loose && toWellFormed))
65
65
  if (!loose && isWellFormed && !isWellFormed.call(str)) throw new TypeError(E_STRICT)
66
+ if (loose && toWellFormed) return toWellFormed.call(str)
66
67
 
67
68
  return str
68
69
  }
package/utf16.node.js CHANGED
@@ -1,9 +1,9 @@
1
- import { nativeDecoder, isDeno, isLE } from './fallback/_utils.js'
1
+ import { isDeno, isLE } from './fallback/_utils.js'
2
2
  import { E_STRICT, E_STRICT_UNICODE } from './fallback/utf16.js'
3
3
 
4
4
  if (Buffer.TYPED_ARRAY_SUPPORT) throw new Error('Unexpected Buffer polyfill')
5
5
 
6
- const { isWellFormed } = String.prototype
6
+ const { isWellFormed, toWellFormed } = String.prototype
7
7
  const to8 = (a) => new Uint8Array(a.buffer, a.byteOffset, a.byteLength)
8
8
 
9
9
  // Unlike utf8, operates on Uint16Arrays by default
@@ -14,9 +14,10 @@ function encode(str, loose = false, format = 'uint16') {
14
14
  throw new TypeError('Unknown format')
15
15
  }
16
16
 
17
- if (!isWellFormed.call(str)) {
18
- if (!loose) throw new TypeError(E_STRICT_UNICODE)
19
- str = nativeDecoder.decode(Buffer.from(str)) // well, let's fix up (Buffer doesn't do this with utf16 encoding)
17
+ if (loose) {
18
+ str = toWellFormed.call(str) // Buffer doesn't do this with utf16 encoding
19
+ } else if (!isWellFormed.call(str)) {
20
+ throw new TypeError(E_STRICT_UNICODE)
20
21
  }
21
22
 
22
23
  const ble = Buffer.from(str, 'utf-16le')
@@ -36,6 +37,7 @@ const swapped = (x, swap) =>
36
37
  swap ? Buffer.from(x).swap16() : Buffer.from(x.buffer, x.byteOffset, x.byteLength)
37
38
 
38
39
  // We skip TextDecoder on Node.js, as it's is somewhy significantly slower than Buffer for utf16
40
+ // Also, it incorrectly misses replacements with Node.js is built without ICU, we fix that
39
41
  function decodeNode(input, loose = false, format = 'uint16') {
40
42
  let ble
41
43
  if (format === 'uint16') {
@@ -50,9 +52,9 @@ function decodeNode(input, loose = false, format = 'uint16') {
50
52
  }
51
53
 
52
54
  const str = ble.ucs2Slice(0, ble.byteLength)
55
+ if (loose) return toWellFormed.call(str)
53
56
  if (isWellFormed.call(str)) return str
54
- if (!loose) throw new TypeError(E_STRICT)
55
- return nativeDecoder.decode(Buffer.from(str)) // fixup (see above)
57
+ throw new TypeError(E_STRICT)
56
58
  }
57
59
 
58
60
  function decodeDecoder(input, loose = false, format = 'uint16') {
package/utf8.node.js CHANGED
@@ -1,15 +1,21 @@
1
1
  import { assertUint8 } from './assert.js'
2
2
  import { typedView } from './array.js'
3
- import { E_STRICT_UNICODE } from './fallback/utf8.js'
3
+ import { E_STRICT, E_STRICT_UNICODE } from './fallback/utf8.js'
4
4
  import { isAscii } from 'node:buffer'
5
5
 
6
6
  if (Buffer.TYPED_ARRAY_SUPPORT) throw new Error('Unexpected Buffer polyfill')
7
7
 
8
- const decoderFatal = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true })
8
+ let decoderFatal
9
9
  const decoderLoose = new TextDecoder('utf-8', { ignoreBOM: true })
10
10
  const { isWellFormed } = String.prototype
11
11
  const isDeno = Boolean(globalThis.Deno)
12
12
 
13
+ try {
14
+ decoderFatal = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true })
15
+ } catch {
16
+ // Without ICU, Node.js doesn't support fatal option for utf-8
17
+ }
18
+
13
19
  function encode(str, loose = false) {
14
20
  if (typeof str !== 'string') throw new TypeError('Input is not a string')
15
21
  const strLength = str.length
@@ -45,7 +51,14 @@ function decode(arr, loose = false) {
45
51
  return buf.latin1Slice(0, arr.byteLength) // .latin1Slice is faster than .asciiSlice
46
52
  }
47
53
 
48
- return loose ? decoderLoose.decode(arr) : decoderFatal.decode(arr)
54
+ if (loose) return decoderLoose.decode(arr)
55
+ if (decoderFatal) return decoderFatal.decode(arr)
56
+
57
+ // We are in an env without native fatal decoder support (non-fixed Node.js without ICU)
58
+ // Well, just recheck against encode if it contains replacement then, this is still faster than js impl
59
+ const str = decoderLoose.decode(arr)
60
+ if (str.includes('\uFFFD') && !Buffer.from(str).equals(arr)) throw new TypeError(E_STRICT)
61
+ return str
49
62
  }
50
63
 
51
64
  export const utf8fromString = (str, format = 'uint8') => typedView(encode(str, false), format)