@exodus/bytes 1.0.0-rc.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Exodus Movement
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # bytes
2
+
3
+ Data structures handling
package/array.js ADDED
@@ -0,0 +1,17 @@
1
+ import { assertTypedArray } from './assert.js'
2
+
3
+ const { Buffer } = globalThis // Buffer is optional
4
+
5
+ export function fromTypedArray(arr, format) {
6
+ assertTypedArray(arr)
7
+ switch (format) {
8
+ case 'uint8':
9
+ if (arr.constructor === Uint8Array) return arr // fast path
10
+ return new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength)
11
+ case 'buffer':
12
+ if (arr.constructor === Buffer && Buffer.isBuffer(arr)) return arr
13
+ return Buffer.from(arr.buffer, arr.byteOffset, arr.byteLength)
14
+ }
15
+
16
+ throw new TypeError('Unexpected format')
17
+ }
package/assert.js ADDED
@@ -0,0 +1,22 @@
1
+ export function assert(x, msg) {
2
+ if (!x) throw new Error(msg || 'Assertion failed')
3
+ }
4
+
5
+ export function assertEmptyRest(rest) {
6
+ if (Object.keys(rest).length > 0) throw new TypeError('Unexpected extra options')
7
+ }
8
+
9
+ // eslint-disable-next-line sonarjs/no-nested-template-literals
10
+ const makeMessage = (name, extra) => `Expected${name ? ` ${name} to be` : ''} an Uint8Array${extra}`
11
+
12
+ const TypedArray = Object.getPrototypeOf(Uint8Array)
13
+
14
+ export function assertTypedArray(arr) {
15
+ assert(arr instanceof TypedArray, 'Expected a TypedArray instance')
16
+ }
17
+
18
+ export function assertUint8(arr, { name, length, ...rest } = {}) {
19
+ assertEmptyRest(rest)
20
+ if (arr instanceof Uint8Array && (length === undefined || arr.length === length)) return
21
+ throw new TypeError(makeMessage(name, length === undefined ? '' : ` of size ${Number(length)}`))
22
+ }
package/base64.js ADDED
@@ -0,0 +1,204 @@
1
+ import { assert, assertUint8 } from './assert.js'
2
+ import { fromTypedArray } from './array.js'
3
+
4
+ // See https://datatracker.ietf.org/doc/html/rfc4648
5
+
6
+ // base64: A-Za-z0-9+/ and =
7
+ // base64url: A-Za-z0-9_-
8
+
9
+ const { Buffer, atob } = globalThis // Buffer is optional, only used when native
10
+ const haveNativeBuffer = Buffer && !Buffer.TYPED_ARRAY_SUPPORT
11
+ const { toBase64: web64 } = Uint8Array.prototype // Modern engines have this
12
+
13
+ export function toBase64(x) {
14
+ 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
19
+ }
20
+
21
+ // NOTE: base64url omits padding
22
+ export function toBase64url(x) {
23
+ 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
26
+ 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
28
+ }
29
+
30
+ // Unlike Buffer.from(), throws on invalid input (non-base64 symbols and incomplete chunks)
31
+ // Unlike Buffer.from() and Uint8Array.fromBase64(), does not allow spaces
32
+ // NOTE: Always operates in strict mode for last chunk
33
+
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')
37
+
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
+ }
44
+
45
+ return fromTypedArray(fromBase64common(str, false), format)
46
+ }
47
+
48
+ // Accepts both only non-padded strict base64url
49
+ export function fromBase64url(str, format = 'uint8') {
50
+ if (typeof str !== 'string') throw new TypeError('Input is not a string')
51
+
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)
57
+ }
58
+
59
+ let fromBase64common
60
+ if (Uint8Array.fromBase64) {
61
+ // 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) => {
63
+ const alphabet = isBase64url ? 'base64url' : 'base64'
64
+ assert(!/\s/u.test(str), `Invalid character in ${alphabet} input`) // all other chars are checked natively
65
+ const padded = str.length % 4 > 0 ? `${str}${'='.repeat(4 - (str.length % 4))}` : str
66
+ return Uint8Array.fromBase64(padded, { alphabet, lastChunkHandling: 'strict' })
67
+ }
68
+ } 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
+
76
+ let arr
77
+ if (!haveNativeBuffer && atob) {
78
+ // atob is faster than manual parsing on Hermes
79
+ const raw = atob(isBase64url ? str.replaceAll('-', '+').replaceAll('_', '/') : str)
80
+ arr = new Uint8Array(raw.length)
81
+ for (let i = 0; i < raw.length; i++) arr[i] = raw.charCodeAt(i)
82
+ } else {
83
+ // base64url is already checked to have no padding via a regex above
84
+ if (!isBase64url) {
85
+ const at = str.indexOf('=')
86
+ if (at >= 0) assert(!/[^=]/iu.test(str.slice(at)), 'Invalid padding')
87
+ }
88
+
89
+ arr = haveNativeBuffer ? Buffer.from(str, 'base64') : fromBase64js(str)
90
+ }
91
+
92
+ if (arr.length % 3 !== 0) {
93
+ // Check last chunk to be strict if it was incomplete
94
+ const expected = toBase64(arr.subarray(-(arr.length % 3)))
95
+ const end = str.length % 4 === 0 ? str.slice(-4) : str.slice(-(str.length % 4)).padEnd(4, '=')
96
+ const actual = isBase64url ? end.replaceAll('-', '+').replaceAll('_', '/') : end
97
+ if (expected !== actual) throw new Error('Invalid last chunk')
98
+ }
99
+
100
+ return arr
101
+ }
102
+ }
103
+
104
+ const BASE64 = [...'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/']
105
+ const BASE64URL = [...'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_']
106
+
107
+ const BASE64_PAIRS = []
108
+ const BASE64URL_PAIRS = []
109
+
110
+ // We construct output by concatenating chars, this seems to be fine enough on modern JS engines
111
+ function toBase64js(arr, alphabet, padding) {
112
+ assertUint8(arr)
113
+ const fullChunks = Math.floor(arr.length / 3)
114
+ const fullChunksBytes = fullChunks * 3
115
+ let o = ''
116
+ let i = 0
117
+
118
+ const pairs = alphabet === BASE64URL ? BASE64URL_PAIRS : BASE64_PAIRS
119
+ if (pairs.length === 0) {
120
+ for (let i = 0; i < 64; i++) {
121
+ for (let j = 0; j < 64; j++) pairs.push(`${alphabet[i]}${alphabet[j]}`)
122
+ }
123
+ }
124
+
125
+ // Fast path for complete blocks
126
+ // This whole loop can be commented out, the algorithm won't change, it's just an optimization of the next loop
127
+ for (; i < fullChunksBytes; i += 3) {
128
+ const a = arr[i]
129
+ const b = arr[i + 1]
130
+ const c = arr[i + 2]
131
+ o += pairs[(a << 4) | (b >> 4)] + pairs[((b & 0x0f) << 8) | c]
132
+ }
133
+
134
+ // If we have something left, process it with a full algo
135
+ let carry = 0
136
+ let shift = 2 // First byte needs to be shifted by 2 to get 6 bits
137
+ const length = arr.length
138
+ for (; i < length; i++) {
139
+ const x = arr[i]
140
+ o += alphabet[carry | (x >> shift)] // shift >= 2, so this fits
141
+ if (shift === 6) {
142
+ shift = 0
143
+ o += alphabet[x & 0x3f]
144
+ }
145
+
146
+ carry = (x << (6 - shift)) & 0x3f
147
+ shift += 2 // Each byte prints 6 bits and leaves 2 bits
148
+ }
149
+
150
+ if (shift !== 2) o += alphabet[carry] // shift 2 means we have no carry left
151
+ if (padding) o += ['', '==', '='][length - fullChunksBytes]
152
+
153
+ return o
154
+ }
155
+
156
+ // Assumes no chars after =, checked
157
+ let fromBase64jsMap
158
+
159
+ function fromBase64js(str) {
160
+ const map = fromBase64jsMap || new Array(256)
161
+ if (!fromBase64jsMap) {
162
+ fromBase64jsMap = map
163
+ BASE64.forEach((c, i) => (map[c.charCodeAt(0)] = i))
164
+ map['-'.charCodeAt(0)] = map['+'.charCodeAt(0)] // for base64url
165
+ map['_'.charCodeAt(0)] = map['/'.charCodeAt(0)] // for base64url
166
+ }
167
+
168
+ let inputLength = str.length
169
+ while (str[inputLength - 1] === '=') inputLength--
170
+
171
+ const arr = new Uint8Array(Math.floor((inputLength * 3) / 4))
172
+ const tailLength = inputLength % 4
173
+ const mainLength = inputLength - tailLength // multiples of 4
174
+
175
+ let at = 0
176
+ let i = 0
177
+ let tmp
178
+
179
+ while (i < mainLength) {
180
+ tmp =
181
+ (map[str.charCodeAt(i)] << 18) |
182
+ (map[str.charCodeAt(i + 1)] << 12) |
183
+ (map[str.charCodeAt(i + 2)] << 6) |
184
+ map[str.charCodeAt(i + 3)]
185
+ arr[at++] = tmp >> 16
186
+ arr[at++] = (tmp >> 8) & 0xff
187
+ arr[at++] = tmp & 0xff
188
+ i += 4
189
+ }
190
+
191
+ if (tailLength === 3) {
192
+ tmp =
193
+ (map[str.charCodeAt(i)] << 10) |
194
+ (map[str.charCodeAt(i + 1)] << 4) |
195
+ (map[str.charCodeAt(i + 2)] >> 2)
196
+ arr[at++] = (tmp >> 8) & 0xff
197
+ arr[at++] = tmp & 0xff
198
+ } else if (tailLength === 2) {
199
+ tmp = (map[str.charCodeAt(i)] << 2) | (map[str.charCodeAt(i + 1)] >> 4)
200
+ arr[at++] = tmp & 0xff
201
+ }
202
+
203
+ return arr
204
+ }
package/hex.js ADDED
@@ -0,0 +1,78 @@
1
+ import { assertTypedArray, assert } from './assert.js'
2
+ import { fromTypedArray } from './array.js'
3
+
4
+ const { Buffer } = globalThis // Buffer is optional, only used when native
5
+ const haveNativeBuffer = Buffer && !Buffer.TYPED_ARRAY_SUPPORT
6
+ const { toHex: webHex } = Uint8Array.prototype // Modern engines have this
7
+
8
+ let hexArray
9
+ let dehexArray
10
+
11
+ export function toHex(arr) {
12
+ assertTypedArray(arr)
13
+ if (!(arr instanceof Uint8Array)) arr = new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength)
14
+ if (webHex && arr.toHex === webHex) return arr.toHex()
15
+ if (haveNativeBuffer) {
16
+ if (arr.constructor === Buffer && Buffer.isBuffer(arr)) return arr.toString('hex')
17
+ return Buffer.from(arr.buffer, arr.byteOffset, arr.byteLength).toString('hex')
18
+ }
19
+
20
+ if (!hexArray) hexArray = Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, '0'))
21
+ const length = arr.length // this helps Hermes
22
+
23
+ if (length > 3000) {
24
+ // Limit concatenation to avoid excessive GC
25
+ // Thresholds checked on Hermes
26
+ const concat = []
27
+ for (let i = 0; i < length; ) {
28
+ const step = i + 500
29
+ const end = step > length ? length : step
30
+ let chunk = ''
31
+ for (; i < end; i++) chunk += hexArray[arr[i]]
32
+ concat.push(chunk)
33
+ }
34
+
35
+ const res = concat.join('')
36
+ concat.length = 0
37
+ return res
38
+ }
39
+
40
+ let out = ''
41
+ for (let i = 0; i < length; i++) out += hexArray[arr[i]]
42
+ return out
43
+ }
44
+
45
+ // Unlike Buffer.from(), throws on invalid input
46
+ let fromHex
47
+ if (Uint8Array.fromHex) {
48
+ fromHex = (str, format = 'uint8') => fromTypedArray(Uint8Array.fromHex(str), format)
49
+ } else {
50
+ fromHex = (str, format = 'uint8') => {
51
+ if (typeof str !== 'string') throw new TypeError('Input is not a string')
52
+ assert(str.length % 2 === 0, 'Input is not a hex string')
53
+
54
+ // We don't use native Buffer impl, as rechecking input make it slower than pure js
55
+ // This path is used only on older engines though
56
+
57
+ if (!dehexArray) {
58
+ dehexArray = new Array(103) // f is 102
59
+ for (let i = 0; i < 16; i++) {
60
+ const s = i.toString(16)
61
+ dehexArray[s.charCodeAt(0)] = dehexArray[s.toUpperCase().charCodeAt(0)] = i
62
+ }
63
+ }
64
+
65
+ const arr = new Uint8Array(str.length / 2)
66
+ let j = 0
67
+ const length = arr.length // this helps Hermes
68
+ for (let i = 0; i < length; i++) {
69
+ const a = dehexArray[str.charCodeAt(j++)] * 16 + dehexArray[str.charCodeAt(j++)]
70
+ if (!a && Number.isNaN(a)) throw new Error('Input is not a hex string')
71
+ arr[i] = a
72
+ }
73
+
74
+ return fromTypedArray(arr, format)
75
+ }
76
+ }
77
+
78
+ export { fromHex }
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "@exodus/bytes",
3
+ "version": "1.0.0-rc.0",
4
+ "description": "Various operations on Uint8Array data",
5
+ "scripts": {
6
+ "lint": "eslint .",
7
+ "test:v8": "npm run test:d8 --",
8
+ "test:javascriptcore": "npm run test:jsc --",
9
+ "test:d8": "exodus-test --engine=d8:bundle",
10
+ "test:jsc": "exodus-test --engine=jsc:bundle",
11
+ "test:spidermonkey": "exodus-test --engine=spidermonkey:bundle",
12
+ "test:hermes": "exodus-test --engine=hermes:bundle",
13
+ "test:quickjs": "exodus-test --engine=quickjs:bundle",
14
+ "test:electron:bundle": "exodus-test --engine=electron:bundle",
15
+ "test:electron:as-node": "exodus-test --engine=electron-as-node:test",
16
+ "test:chrome:puppeteer": "exodus-test --engine=chrome:puppeteer",
17
+ "test:chromium:playwright": "exodus-test --engine=chromium:playwright",
18
+ "test:webkit:playwright": "exodus-test --engine=webkit:playwright",
19
+ "test:firefox:puppeteer": "exodus-test --engine=firefox:puppeteer",
20
+ "test:firefox:playwright": "exodus-test --engine=firefox:playwright",
21
+ "test": "exodus-test",
22
+ "jsvu": "jsvu",
23
+ "playwright": "exodus-test --playwright",
24
+ "benchmark": "exodus-test --concurrency=1 benchmarks/*.bench.js",
25
+ "coverage": "exodus-test --coverage"
26
+ },
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/ExodusMovement/bytes.git"
30
+ },
31
+ "author": "Exodus Movement, Inc.",
32
+ "license": "MIT",
33
+ "bugs": {
34
+ "url": "https://github.com/ExodusMovement/bytes/issues"
35
+ },
36
+ "homepage": "https://github.com/ExodusMovement/bytes#readme",
37
+ "engines": {
38
+ "node": "^20.19.0 || >=22.13.0"
39
+ },
40
+ "type": "module",
41
+ "files": [
42
+ "/assert.js",
43
+ "/base64.js",
44
+ "/array.js",
45
+ "/hex.js"
46
+ ],
47
+ "exports": {
48
+ "./base64.js": "./base64.js",
49
+ "./array.js": "./array.js",
50
+ "./hex.js": "./hex.js"
51
+ },
52
+ "dependencies": {},
53
+ "devDependencies": {
54
+ "@exodus/eslint-config": "^5.24.0",
55
+ "@exodus/prettier": "^1.0.0",
56
+ "@exodus/test": "^1.0.0-rc.105",
57
+ "@scure/base": "^1.2.6",
58
+ "@types/node": "^24.0.10",
59
+ "base-x": "^5.0.1",
60
+ "base32.js": "^0.1.0",
61
+ "base64-js": "^1.5.1",
62
+ "buffer": "^6.0.3",
63
+ "electron": "36.5.0",
64
+ "eslint": "^8.44.0",
65
+ "jsvu": "^3.0.0"
66
+ },
67
+ "prettier": "@exodus/prettier",
68
+ "packageManager": "pnpm@10.12.1+sha256.889bac470ec93ccc3764488a19d6ba8f9c648ad5e50a9a6e4be3768a5de387a3"
69
+ }