@exodus/bytes 1.9.0 → 1.11.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/utf8.js CHANGED
@@ -27,16 +27,21 @@ function deLoose(str, loose, res) {
27
27
 
28
28
  // Recheck if the string was encoded correctly
29
29
  let start = 0
30
- const last = res.length - 2
31
- // Search for EFBFBD
32
- while (start < last) {
30
+ const last = res.length - 3
31
+ // Search for EFBFBD (3-byte sequence)
32
+ while (start <= last) {
33
33
  const pos = res.indexOf(0xef, start)
34
- if (pos === -1) break
34
+ if (pos === -1 || pos > last) break
35
35
  start = pos + 1
36
36
  if (res[pos + 1] === 0xbf && res[pos + 2] === 0xbd) {
37
37
  // Found a replacement char in output, need to recheck if we encoded the input correctly
38
- if (str !== decode(res)) throw new TypeError(E_STRICT_UNICODE)
39
- return res
38
+ if (!nativeDecoder && str.length < 1e7) {
39
+ // This is ~2x faster than decode in Hermes
40
+ try {
41
+ if (encodeURI(str) !== null) return res // guard against optimizing out
42
+ } catch {}
43
+ } else if (str === decode(res)) return res
44
+ throw new TypeError(E_STRICT_UNICODE)
40
45
  }
41
46
  }
42
47
 
package/utf8.node.js CHANGED
@@ -9,7 +9,7 @@ if (Buffer.TYPED_ARRAY_SUPPORT) throw new Error('Unexpected Buffer polyfill')
9
9
  let decoderFatal
10
10
  const decoderLoose = new TextDecoder('utf-8', { ignoreBOM: true })
11
11
  const { isWellFormed } = String.prototype
12
- const isDeno = Boolean(globalThis.Deno)
12
+ const isDeno = !!globalThis.Deno
13
13
 
14
14
  try {
15
15
  decoderFatal = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true })
package/whatwg.d.ts ADDED
@@ -0,0 +1,48 @@
1
+ /**
2
+ * WHATWG helpers
3
+ *
4
+ * ```js
5
+ * import '@exodus/bytes/encoding.js' // For full legacy multi-byte encodings support
6
+ * import { percentEncodeAfterEncoding } from '@exodus/bytes/whatwg.js'
7
+ * ```
8
+ *
9
+ * @module @exodus/bytes/whatwg.js
10
+ */
11
+
12
+ /**
13
+ * Implements [percent-encode after encoding](https://url.spec.whatwg.org/#string-percent-encode-after-encoding)
14
+ * per WHATWG URL specification.
15
+ *
16
+ * > [!IMPORTANT]
17
+ * > You must import `@exodus/bytes/encoding.js` for this API to accept legacy multi-byte encodings.
18
+ *
19
+ * Encodings `utf16-le`, `utf16-be`, and `replacement` are not accepted.
20
+ *
21
+ * [C0 control percent-encode set](https://url.spec.whatwg.org/#c0-control-percent-encode-set) is
22
+ * always percent-encoded.
23
+ *
24
+ * `percentEncodeSet` is an addition to that, and must be a string of unique increasing codepoints
25
+ * in range 0x20 - 0x7e, e.g. `' "#<>'`.
26
+ *
27
+ * This method accepts [DOMStrings](https://webidl.spec.whatwg.org/#idl-DOMString) and converts them
28
+ * to [USVStrings](https://webidl.spec.whatwg.org/#idl-USVString).
29
+ * This is different from e.g. `encodeURI` and `encodeURIComponent` which throw on surrogates:
30
+ * ```js
31
+ * > percentEncodeAfterEncoding('utf8', '\ud800', ' "#$%&+,/:;<=>?@[\\]^`{|}') // component
32
+ * '%EF%BF%BD'
33
+ * > encodeURIComponent('\ud800')
34
+ * Uncaught URIError: URI malformed
35
+ * ```
36
+ *
37
+ * @param encoding - The encoding label per WHATWG Encoding spec
38
+ * @param input - Input scalar-value string to encode
39
+ * @param percentEncodeSet - A string of ASCII chars to escape in addition to C0 control percent-encode set
40
+ * @param spaceAsPlus - Whether to encode space as `'+'` instead of `'%20'` or `' '` (default: false)
41
+ * @returns The percent-encoded string
42
+ */
43
+ export function percentEncodeAfterEncoding(
44
+ encoding: string,
45
+ input: string,
46
+ percentEncodeSet: string,
47
+ spaceAsPlus?: boolean
48
+ ): string;
package/whatwg.js ADDED
@@ -0,0 +1,76 @@
1
+ import { utf8fromStringLoose } from '@exodus/bytes/utf8.js'
2
+ import { createSinglebyteEncoder } from '@exodus/bytes/single-byte.js'
3
+ import { isMultibyte, getMultibyteEncoder } from './fallback/encoding.js'
4
+ import { normalizeEncoding, E_ENCODING } from './fallback/encoding.api.js'
5
+ import { percentEncoder } from './fallback/percent.js'
6
+ import { encodeMap } from './fallback/single-byte.js'
7
+ import { E_STRING } from './fallback/_utils.js'
8
+
9
+ // https://url.spec.whatwg.org/#string-percent-encode-after-encoding
10
+ // Codepoints below 0x20, 0x7F specifically, and above 0x7F (non-ASCII) are always encoded
11
+ // > A C0 control is a code point in the range U+0000 NULL to U+001F INFORMATION SEPARATOR ONE, inclusive.
12
+ // > The C0 control percent-encode set are the C0 controls and all code points greater than U+007E (~).
13
+ export function percentEncodeAfterEncoding(encoding, input, percentEncodeSet, spaceAsPlus = false) {
14
+ const enc = normalizeEncoding(encoding)
15
+ // Ref: https://encoding.spec.whatwg.org/#get-an-encoder
16
+ if (!enc || enc === 'replacement' || enc === 'utf-16le' || enc === 'utf-16be') {
17
+ throw new RangeError(E_ENCODING)
18
+ }
19
+
20
+ const percent = percentEncoder(percentEncodeSet, spaceAsPlus)
21
+ if (enc === 'utf-8') return percent(utf8fromStringLoose(input))
22
+
23
+ const multi = isMultibyte(enc)
24
+ const encoder = multi ? getMultibyteEncoder() : createSinglebyteEncoder
25
+ const fatal = encoder(enc)
26
+ try {
27
+ return percent(fatal(input))
28
+ } catch {}
29
+
30
+ let res = ''
31
+ let last = 0
32
+ if (multi) {
33
+ const rep = enc === 'gb18030' ? percent(fatal('\uFFFD')) : `%26%23${0xff_fd}%3B` // only gb18030 can encode it
34
+ const escaping = encoder(enc, (cp, u, i) => {
35
+ res += percent(u, last, i)
36
+ res += cp >= 0xd8_00 && cp < 0xe0_00 ? rep : `%26%23${cp}%3B` // &#cp;
37
+ last = i
38
+ return 0 // no bytes emitted
39
+ })
40
+
41
+ const u = escaping(input) // has side effects on res
42
+ res += percent(u, last)
43
+ } else {
44
+ if (typeof input !== 'string') throw new TypeError(E_STRING) // all other paths have their own validation
45
+ const m = encodeMap(enc)
46
+ const len = input.length
47
+ const u = new Uint8Array(len)
48
+ for (let i = 0; i < len; i++) {
49
+ const x = input.charCodeAt(i)
50
+ const b = m[x]
51
+ if (!b && x) {
52
+ let cp = x
53
+ const i0 = i
54
+ if (x >= 0xd8_00 && x < 0xe0_00) {
55
+ cp = 0xff_fd
56
+ if (x < 0xdc_00 && i + 1 < len) {
57
+ const x1 = input.charCodeAt(i + 1)
58
+ if (x1 >= 0xdc_00 && x1 < 0xe0_00) {
59
+ cp = 0x1_00_00 + ((x1 & 0x3_ff) | ((x & 0x3_ff) << 10))
60
+ i++
61
+ }
62
+ }
63
+ }
64
+
65
+ res += `${percent(u, last, i0)}%26%23${cp}%3B` // &#cp;
66
+ last = i + 1 // skip current
67
+ } else {
68
+ u[i] = b
69
+ }
70
+ }
71
+
72
+ res += percent(u, last)
73
+ }
74
+
75
+ return res
76
+ }
package/wif.d.ts ADDED
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Wallet Import Format (WIF) encoding and decoding.
3
+ *
4
+ * ```js
5
+ * import { fromWifString, toWifString } from '@exodus/bytes/wif.js'
6
+ * import { fromWifStringSync, toWifStringSync } from '@exodus/bytes/wif.js'
7
+ * ```
8
+ *
9
+ * On non-Node.js, requires peer dependency [@noble/hashes](https://www.npmjs.com/package/@noble/hashes) to be installed.
10
+ *
11
+ * @module @exodus/bytes/wif.js
12
+ */
13
+
14
+ /// <reference types="node" />
15
+
16
+ import type { Uint8ArrayBuffer } from './array.js';
17
+
18
+ /**
19
+ * WIF (Wallet Import Format) data structure
20
+ */
21
+ export interface Wif {
22
+ /** Network version byte */
23
+ version: number;
24
+ /** 32-byte private key */
25
+ privateKey: Uint8ArrayBuffer;
26
+ /** Whether the key is compressed */
27
+ compressed: boolean;
28
+ }
29
+
30
+ /**
31
+ * Decode a WIF string to WIF data
32
+ *
33
+ * Returns a promise that resolves to an object with `{ version, privateKey, compressed }`.
34
+ *
35
+ * The optional `version` parameter validates the version byte.
36
+ *
37
+ * Throws if the WIF string is invalid or version doesn't match.
38
+ *
39
+ * @param string - The WIF encoded string
40
+ * @param version - Optional expected version byte to validate against
41
+ * @returns The decoded WIF data
42
+ * @throws Error if the WIF string is invalid or version doesn't match
43
+ */
44
+ export function fromWifString(string: string, version?: number): Promise<Wif>;
45
+
46
+ /**
47
+ * Decode a WIF string to WIF data (synchronous)
48
+ *
49
+ * Returns an object with `{ version, privateKey, compressed }`.
50
+ *
51
+ * The optional `version` parameter validates the version byte.
52
+ *
53
+ * Throws if the WIF string is invalid or version doesn't match.
54
+ *
55
+ * @param string - The WIF encoded string
56
+ * @param version - Optional expected version byte to validate against
57
+ * @returns The decoded WIF data
58
+ * @throws Error if the WIF string is invalid or version doesn't match
59
+ */
60
+ export function fromWifStringSync(string: string, version?: number): Wif;
61
+
62
+ /**
63
+ * Encode WIF data to a WIF string
64
+ *
65
+ * @param wif - The WIF data to encode
66
+ * @returns The WIF encoded string
67
+ */
68
+ export function toWifString(wif: Wif): Promise<string>;
69
+
70
+ /**
71
+ * Encode WIF data to a WIF string (synchronous)
72
+ *
73
+ * @param wif - The WIF data to encode
74
+ * @returns The WIF encoded string
75
+ */
76
+ export function toWifStringSync(wif: Wif): string;
package/wif.js CHANGED
@@ -6,6 +6,7 @@ import { assertUint8 } from './assert.js'
6
6
 
7
7
  function from(arr, expectedVersion) {
8
8
  assertUint8(arr)
9
+ if (arr.length !== 33 && arr.length !== 34) throw new Error('Invalid WIF length')
9
10
  const version = arr[0]
10
11
  if (expectedVersion !== undefined && version !== expectedVersion) {
11
12
  throw new Error('Invalid network version')
@@ -14,7 +15,6 @@ function from(arr, expectedVersion) {
14
15
  // Makes a copy, regardless of input being a Buffer or a Uint8Array (unlike .slice)
15
16
  const privateKey = Uint8Array.from(arr.subarray(1, 33))
16
17
  if (arr.length === 33) return { version, privateKey, compressed: false }
17
- if (arr.length !== 34) throw new Error('Invalid WIF length')
18
18
  if (arr[33] !== 1) throw new Error('Invalid compression flag')
19
19
  return { version, privateKey, compressed: true }
20
20
  }
@@ -22,7 +22,6 @@ function from(arr, expectedVersion) {
22
22
  function to({ version: v, privateKey, compressed }) {
23
23
  if (!Number.isSafeInteger(v) || v < 0 || v > 0xff) throw new Error('Missing or invalid version')
24
24
  assertUint8(privateKey, { length: 32, name: 'privateKey' })
25
- if (privateKey.length !== 32) throw new TypeError('Invalid privateKey length')
26
25
  const out = new Uint8Array(compressed ? 34 : 33)
27
26
  out[0] = v
28
27
  out.set(privateKey, 1)