| … | … | @@ -0,0 +1,64 @@ |
| 1 | # autobase: host.addWriter() with unreachable key causes infinite hang in _drain() | |
| 2 | ||
| 3 | ## the problem | |
| 4 | ||
| 5 | if an apply function calls `host.addWriter(key)` with a key that has no reachable hypercore behind it (wrong key type, unreplicated remote, typo, etc), the autobase enters an unrecoverable infinite loop on the next `update()` call. | |
| 6 | ||
| 7 | the loop is in `_drain()` at index.js:1838-1843: | |
| 8 | ||
| 9 | ```javascript | |
| 10 | if (this._caughtup === false) { | |
| 11 | if (!(await this._catchupApplyState())) { | |
| 12 | await this._runForceFastForward() | |
| 13 | } | |
| 14 | continue | |
| 15 | } | |
| 16 | ``` | |
| 17 | ||
| 18 | what happens: | |
| 19 | 1. `_catchupApplyState()` calls `catchup()` which tries to load writer nodes | |
| 20 | 2. the writer core has no data (`core.length === 0`), so `w.update()` returns nothing | |
| 21 | 3. `catchup()` returns false | |
| 22 | 4. `_runForceFastForward()` runs but also fails (system core length is 0, so `ff._upgrade()` returns false at line 115) | |
| 23 | 5. `fastForwardFailedAt` is set to `Date.now()` | |
| 24 | 6. `continue` loops back to the top | |
| 25 | 7. `_caughtup` is still false, so we try again immediately | |
| 26 | 8. goto 1 — tight infinite loop, `update()` never resolves | |
| 27 | ||
| 28 | the `MIN_FF_WAIT` cooldown (5 minutes) exists but only guards `_queueFastForward()` and `_drainWithInterrupt()` — it's not checked in the catchup path at line 1838. so the loop bypasses the cooldown entirely. | |
| 29 | ||
| 30 | ## how we hit it | |
| 31 | ||
| 32 | we have a p2p git forge (pear-git) that uses autobase for per-repo issue storage. during federation sync, a bug in our code passed an ed25519 server identity key to `host.addWriter()` instead of the actual hypercore writer core key. the key was valid (32 bytes), the op was appended to the log, `system.add()` persisted it — but no hypercore with that key exists anywhere. on every subsequent boot, the autobase replayed the poisoned op and hung. | |
| 33 | ||
| 34 | this corrupted 18 autobases across two servers. recovery required truncating writer cores, resetting system batch state, and clearing boot records. we've written tooling for this (autobase-doctor) but the root cause is the infinite loop in `_drain()`. | |
| 35 | ||
| 36 | ## proposed fix | |
| 37 | ||
| 38 | respect `fastForwardFailedAt` in the catchup path — if fast-forward just failed, break out of the drain loop instead of retrying immediately: | |
| 39 | ||
| 40 | ```javascript | |
| 41 | if (this._caughtup === false) { | |
| 42 | if (!(await this._catchupApplyState())) { | |
| 43 | if (this.fastForwardFailedAt > 0 && Date.now() - this.fastForwardFailedAt < MIN_FF_WAIT) { | |
| 44 | break | |
| 45 | } | |
| 46 | await this._runForceFastForward() | |
| 47 | } | |
| 48 | continue | |
| 49 | } | |
| 50 | ``` | |
| 51 | ||
| 52 | this is a minimal change: | |
| 53 | - only triggers when catchup fails AND fast-forward recently failed | |
| 54 | - breaks out of `_drain()`, letting the next `update()` call retry after cooldown | |
| 55 | - normal operation is unaffected — healthy autobases never hit this path | |
| 56 | - autobases waiting for legitimate remote writers will retry on the next `update()` (triggered by replication events when the remote core arrives) | |
| 57 | ||
| 58 | we've tested this with autobase-test-helpers replication — all 4 scenarios pass: normal replication, safe apply with poisoned op, unsafe apply reopen, and poisoned op replicated to peer. | |
| 59 | ||
| 60 | ## additional context | |
| 61 | ||
| 62 | `host.addWriter(key)` has no validation on the key — it accepts any 32-byte buffer, calls `system.add()` to persist it, then `_addWriter()` to create a writer. if the key doesn't correspond to a real hypercore, the damage is permanent: the op is in the append-only log, replays on every boot, and the system core caches the poisoned state. | |
| 63 | ||
| 64 | a secondary improvement would be for `host.addWriter()` to warn or throw if the core has no manifest after `writer.ready()`, but the drain loop fix is the critical one since it prevents the unrecoverable hang. |
| … | … | @@ -0,0 +1 @@ |
| 1 | ../brittle/bin/node.js |
| … | … | @@ -0,0 +1 @@ |
| 1 | ../brittle/bin/bare.js |
| … | … | @@ -0,0 +1 @@ |
| 1 | ../brittle/bin/node.js |
| … | … | @@ -0,0 +1 @@ |
| 1 | ../brittle/bin/pear.js |
| … | … | @@ -0,0 +1 @@ |
| 1 | ../hyperdht/bin.js |
| … | … | @@ -0,0 +1 @@ |
| 1 | ../@hyperswarm/testnet/bin.js |
| … | … | @@ -21,6 +21,58 @@ |
| 21 | 21 | "unslab": "^1.3.0" |
| 22 | 22 | } |
| 23 | 23 | }, |
| 24 | "node_modules/@hyperswarm/testnet": { | |
| 25 | "version": "3.1.4", | |
| 26 | "resolved": "https://registry.npmjs.org/@hyperswarm/testnet/-/testnet-3.1.4.tgz", | |
| 27 | "integrity": "sha512-Jw8JIZ9kdZ4R9jwJHsqctj5YeRqecw3aeCg2NS12HBXCqMioQIYzVqHs/8BWdOQCf34+BEXkwMQjba9CzaLwkg==", | |
| 28 | "license": "Apache-2.0", | |
| 29 | "dependencies": { | |
| 30 | "hyperdht": "^6.6.0" | |
| 31 | }, | |
| 32 | "bin": { | |
| 33 | "hyperswarm-testnet": "bin.js" | |
| 34 | } | |
| 35 | }, | |
| 36 | "node_modules/@jridgewell/resolve-uri": { | |
| 37 | "version": "3.1.2", | |
| 38 | "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", | |
| 39 | "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", | |
| 40 | "license": "MIT", | |
| 41 | "engines": { | |
| 42 | "node": ">=6.0.0" | |
| 43 | } | |
| 44 | }, | |
| 45 | "node_modules/@jridgewell/sourcemap-codec": { | |
| 46 | "version": "1.5.5", | |
| 47 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", | |
| 48 | "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", | |
| 49 | "license": "MIT" | |
| 50 | }, | |
| 51 | "node_modules/@jridgewell/trace-mapping": { | |
| 52 | "version": "0.3.31", | |
| 53 | "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", | |
| 54 | "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", | |
| 55 | "license": "MIT", | |
| 56 | "dependencies": { | |
| 57 | "@jridgewell/resolve-uri": "^3.1.0", | |
| 58 | "@jridgewell/sourcemap-codec": "^1.4.14" | |
| 59 | } | |
| 60 | }, | |
| 61 | "node_modules/@types/istanbul-lib-coverage": { | |
| 62 | "version": "2.0.6", | |
| 63 | "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", | |
| 64 | "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", | |
| 65 | "license": "MIT" | |
| 66 | }, | |
| 67 | "node_modules/adaptive-timeout": { | |
| 68 | "version": "1.0.1", | |
| 69 | "resolved": "https://registry.npmjs.org/adaptive-timeout/-/adaptive-timeout-1.0.1.tgz", | |
| 70 | "integrity": "sha512-+seGiBtHqbnyZ0Quq/ZGxxwP66153WBABawa07/QeyuPFzbduPBjiGQAyCiriiLDzt3dkbMBskSlfqtRwblK8Q==", | |
| 71 | "license": "Apache-2.0", | |
| 72 | "dependencies": { | |
| 73 | "xache": "^1.2.1" | |
| 74 | } | |
| 75 | }, | |
| 24 | 76 | "node_modules/autobase": { |
| 25 | 77 | "version": "7.27.3", |
| 26 | 78 | "resolved": "https://registry.npmjs.org/autobase/-/autobase-7.27.3.tgz", |
| … | … | @@ -51,6 +103,15 @@ |
| 51 | 103 | "tiny-buffer-map": "^1.1.1" |
| 52 | 104 | } |
| 53 | 105 | }, |
| 106 | "node_modules/autobase-test-helpers": { | |
| 107 | "version": "3.1.0", | |
| 108 | "resolved": "https://registry.npmjs.org/autobase-test-helpers/-/autobase-test-helpers-3.1.0.tgz", | |
| 109 | "integrity": "sha512-3Jk1nWEcMxYOtU6rt4ujpchLz0W/Uv7S6v0tea53wW9rZqwFzgffEYbufNak53/93DBbp3gwX+AgHdfHtpx4RQ==", | |
| 110 | "license": "Apache-2.0", | |
| 111 | "dependencies": { | |
| 112 | "b4a": "^1.6.4" | |
| 113 | } | |
| 114 | }, | |
| 54 | 115 | "node_modules/b4a": { |
| 55 | 116 | "version": "1.8.0", |
| 56 | 117 | "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz", |
| … | … | @@ -115,6 +176,90 @@ |
| 115 | 176 | "bare-inspect": "^3.1.2" |
| 116 | 177 | } |
| 117 | 178 | }, |
| 179 | "node_modules/bare-bundle": { | |
| 180 | "version": "1.10.0", | |
| 181 | "resolved": "https://registry.npmjs.org/bare-bundle/-/bare-bundle-1.10.0.tgz", | |
| 182 | "integrity": "sha512-4LVlnJAHr00Hh6Vu6ZUJS38rcEtJT3b3vChXSsBsJ2mk1TN0lQ+gzd+Dw5L0aV7uqDZv84smuwW+O02X7PfDlw==", | |
| 183 | "license": "Apache-2.0", | |
| 184 | "peerDependencies": { | |
| 185 | "bare-buffer": "*", | |
| 186 | "bare-url": "*" | |
| 187 | }, | |
| 188 | "peerDependenciesMeta": { | |
| 189 | "bare-buffer": { | |
| 190 | "optional": true | |
| 191 | }, | |
| 192 | "bare-url": { | |
| 193 | "optional": true | |
| 194 | } | |
| 195 | } | |
| 196 | }, | |
| 197 | "node_modules/bare-cov": { | |
| 198 | "version": "1.2.1", | |
| 199 | "resolved": "https://registry.npmjs.org/bare-cov/-/bare-cov-1.2.1.tgz", | |
| 200 | "integrity": "sha512-SHNqb/WG9udUEj8m5qGSehoUQ9o0Xzv8nrf4HIdmG+KNU/XdhPLhTP5VPrHnSAMAXhAzEB4Q+2IYZ0x9qPzN1g==", | |
| 201 | "license": "Apache-2.0", | |
| 202 | "dependencies": { | |
| 203 | "bare-fs": "^4.1.2", | |
| 204 | "bare-inspector": "^4.0.1", | |
| 205 | "bare-path": "^3.0.0", | |
| 206 | "bare-process": "^4.2.1", | |
| 207 | "bare-url": "^2.1.5", | |
| 208 | "bare-v8-to-istanbul": "^1.0.2", | |
| 209 | "picomatch": "^4.0.2", | |
| 210 | "which-runtime": "^1.2.1" | |
| 211 | } | |
| 212 | }, | |
| 213 | "node_modules/bare-crypto": { | |
| 214 | "version": "1.13.4", | |
| 215 | "resolved": "https://registry.npmjs.org/bare-crypto/-/bare-crypto-1.13.4.tgz", | |
| 216 | "integrity": "sha512-JiCZ5l2YOG1y8J7yy1BCAKTCZrPnHLb7pDRIdurBTOn5oIwBQDIv8iH5Pl2V85vzjl1NZXRfNY4HZLsE942jJA==", | |
| 217 | "license": "Apache-2.0", | |
| 218 | "dependencies": { | |
| 219 | "bare-assert": "^1.2.0", | |
| 220 | "bare-stream": "^2.6.3" | |
| 221 | }, | |
| 222 | "peerDependencies": { | |
| 223 | "bare-buffer": "*" | |
| 224 | }, | |
| 225 | "peerDependenciesMeta": { | |
| 226 | "bare-buffer": { | |
| 227 | "optional": true | |
| 228 | } | |
| 229 | } | |
| 230 | }, | |
| 231 | "node_modules/bare-debug-log": { | |
| 232 | "version": "2.0.0", | |
| 233 | "resolved": "https://registry.npmjs.org/bare-debug-log/-/bare-debug-log-2.0.0.tgz", | |
| 234 | "integrity": "sha512-Vi42PkMQsNV9PUpx2Gl1hikshx5O9FzMJ6o9Nnopseg7qLBBK7Nl31d0RHcfwLEAfmcPApytpc0ZFfq68u22FQ==", | |
| 235 | "license": "Apache-2.0", | |
| 236 | "dependencies": { | |
| 237 | "bare-os": "^3.0.1" | |
| 238 | } | |
| 239 | }, | |
| 240 | "node_modules/bare-dns": { | |
| 241 | "version": "2.1.4", | |
| 242 | "resolved": "https://registry.npmjs.org/bare-dns/-/bare-dns-2.1.4.tgz", | |
| 243 | "integrity": "sha512-abwjHmpWqSRNB7V5615QxPH92L71AVzFm/kKTs8VYiNTAi2xVdonpv0BjJ0hwXLwomoW+xsSOPjW6PZPO14asg==", | |
| 244 | "license": "Apache-2.0", | |
| 245 | "engines": { | |
| 246 | "bare": ">=1.7.0" | |
| 247 | } | |
| 248 | }, | |
| 249 | "node_modules/bare-encoding": { | |
| 250 | "version": "1.0.3", | |
| 251 | "resolved": "https://registry.npmjs.org/bare-encoding/-/bare-encoding-1.0.3.tgz", | |
| 252 | "integrity": "sha512-Kqf+t/azs13lUeyK4Tb7ha4wdLRXKWCXQ8w1rVmt7KtoPCPdHD/Xwt7LBIsCSwwGglrcmblo5VOLa5avkJqULA==", | |
| 253 | "license": "Apache-2.0", | |
| 254 | "peerDependencies": { | |
| 255 | "bare-buffer": "*" | |
| 256 | }, | |
| 257 | "peerDependenciesMeta": { | |
| 258 | "bare-buffer": { | |
| 259 | "optional": true | |
| 260 | } | |
| 261 | } | |
| 262 | }, | |
| 118 | 263 | "node_modules/bare-env": { |
| 119 | 264 | "version": "3.0.0", |
| 120 | 265 | "resolved": "https://registry.npmjs.org/bare-env/-/bare-env-3.0.0.tgz", |
| … | … | @@ -138,6 +283,15 @@ |
| 138 | 283 | } |
| 139 | 284 | } |
| 140 | 285 | }, |
| 286 | "node_modules/bare-format": { | |
| 287 | "version": "1.0.2", | |
| 288 | "resolved": "https://registry.npmjs.org/bare-format/-/bare-format-1.0.2.tgz", | |
| 289 | "integrity": "sha512-GswdhnOnP9QtwRbrf4wLApw5widkaLMsLe2XOs35fQD2YfEN1ApoGka+cZ7PfvzxMgfYXmMhj/2OGlVn5/Dxgw==", | |
| 290 | "license": "Apache-2.0", | |
| 291 | "dependencies": { | |
| 292 | "bare-inspect": "^3.0.0" | |
| 293 | } | |
| 294 | }, | |
| 141 | 295 | "node_modules/bare-fs": { |
| 142 | 296 | "version": "4.5.6", |
| 143 | 297 | "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.6.tgz", |
| … | … | @@ -168,6 +322,47 @@ |
| 168 | 322 | "integrity": "sha512-VMb3tHo05gsnbu3OXTmkDiwTjMlOsbQmKoysKqKEyR09m77TuDrYFbj3Q5GGk10dAKsUHrnXmwCaeJqzVpB5ZA==", |
| 169 | 323 | "license": "Apache-2.0" |
| 170 | 324 | }, |
| 325 | "node_modules/bare-http-parser": { | |
| 326 | "version": "1.1.3", | |
| 327 | "resolved": "https://registry.npmjs.org/bare-http-parser/-/bare-http-parser-1.1.3.tgz", | |
| 328 | "integrity": "sha512-+dhVvQi6brHq14L/XHNRQ+TLuVE76VjRmMt61wVEtS+Od8xUslfMHWJN/ZjIIt3RtTG6vPuA+x9cOh7KrkBJsA==", | |
| 329 | "license": "Apache-2.0" | |
| 330 | }, | |
| 331 | "node_modules/bare-http1": { | |
| 332 | "version": "4.5.5", | |
| 333 | "resolved": "https://registry.npmjs.org/bare-http1/-/bare-http1-4.5.5.tgz", | |
| 334 | "integrity": "sha512-ADITiRo0huP76JGMbv6Arsh9KehHqjEBoYcmjvAo67IY78+/9mV2MeKLLCkiJSFxA85T/m8cTaE44OwJQCcwdw==", | |
| 335 | "license": "Apache-2.0", | |
| 336 | "dependencies": { | |
| 337 | "bare-events": "^2.6.0", | |
| 338 | "bare-http-parser": "^1.1.1", | |
| 339 | "bare-stream": "^2.10.0", | |
| 340 | "bare-tcp": "^2.2.0" | |
| 341 | }, | |
| 342 | "peerDependencies": { | |
| 343 | "bare-buffer": "*", | |
| 344 | "bare-url": "*" | |
| 345 | }, | |
| 346 | "peerDependenciesMeta": { | |
| 347 | "bare-buffer": { | |
| 348 | "optional": true | |
| 349 | }, | |
| 350 | "bare-url": { | |
| 351 | "optional": true | |
| 352 | } | |
| 353 | } | |
| 354 | }, | |
| 355 | "node_modules/bare-https": { | |
| 356 | "version": "2.1.3", | |
| 357 | "resolved": "https://registry.npmjs.org/bare-https/-/bare-https-2.1.3.tgz", | |
| 358 | "integrity": "sha512-0TI/mJXQGYXmG7UUyWEG+KCJusayIAQLywUjFAskDoKuxfqVnGF+M/mTMrEV8J64DaIdU+5x761FvgmT7M68tA==", | |
| 359 | "license": "Apache-2.0", | |
| 360 | "dependencies": { | |
| 361 | "bare-http1": "^4.4.0", | |
| 362 | "bare-tcp": "^2.2.0", | |
| 363 | "bare-tls": "^2.0.0" | |
| 364 | } | |
| 365 | }, | |
| 171 | 366 | "node_modules/bare-inspect": { |
| 172 | 367 | "version": "3.1.4", |
| 173 | 368 | "resolved": "https://registry.npmjs.org/bare-inspect/-/bare-inspect-3.1.4.tgz", |
| … | … | @@ -181,6 +376,71 @@ |
| 181 | 376 | "bare": ">=1.18.0" |
| 182 | 377 | } |
| 183 | 378 | }, |
| 379 | "node_modules/bare-inspector": { | |
| 380 | "version": "4.0.3", | |
| 381 | "resolved": "https://registry.npmjs.org/bare-inspector/-/bare-inspector-4.0.3.tgz", | |
| 382 | "integrity": "sha512-yYk6+7tTVKJKHOja5+QBn989ETv8oVsmwoH0nCM+QYC6w5MC03GC4aQNK+lipAZSz1EaAj42WZ8Wewjpy0L9TA==", | |
| 383 | "license": "Apache-2.0", | |
| 384 | "dependencies": { | |
| 385 | "bare-events": "^2.1.0", | |
| 386 | "bare-http1": "^4.0.0", | |
| 387 | "bare-stream": "^2.0.0", | |
| 388 | "bare-url": "^2.0.0", | |
| 389 | "bare-ws": "^2.0.0" | |
| 390 | }, | |
| 391 | "engines": { | |
| 392 | "bare": ">=1.2.0" | |
| 393 | }, | |
| 394 | "peerDependencies": { | |
| 395 | "bare-tcp": "*" | |
| 396 | }, | |
| 397 | "peerDependenciesMeta": { | |
| 398 | "bare-tcp": { | |
| 399 | "optional": true | |
| 400 | } | |
| 401 | } | |
| 402 | }, | |
| 403 | "node_modules/bare-module": { | |
| 404 | "version": "6.1.3", | |
| 405 | "resolved": "https://registry.npmjs.org/bare-module/-/bare-module-6.1.3.tgz", | |
| 406 | "integrity": "sha512-5XWsVHsvtWMH4tK4DQWgpNTV0t/sg3ZrAaQLIxrwjrS5+u8Q9vEgc/zQ4QaDPWDse/y/5h+d+YG1Q0JfSMt0zA==", | |
| 407 | "license": "Apache-2.0", | |
| 408 | "dependencies": { | |
| 409 | "bare-bundle": "^1.3.0", | |
| 410 | "bare-module-lexer": "^1.0.0", | |
| 411 | "bare-module-resolve": "^1.8.0", | |
| 412 | "bare-path": "^3.0.0", | |
| 413 | "bare-url": "^2.0.1" | |
| 414 | }, | |
| 415 | "engines": { | |
| 416 | "bare": ">=1.23.0" | |
| 417 | }, | |
| 418 | "peerDependencies": { | |
| 419 | "bare-buffer": "*" | |
| 420 | }, | |
| 421 | "peerDependenciesMeta": { | |
| 422 | "bare-buffer": { | |
| 423 | "optional": true | |
| 424 | } | |
| 425 | } | |
| 426 | }, | |
| 427 | "node_modules/bare-module-lexer": { | |
| 428 | "version": "1.4.7", | |
| 429 | "resolved": "https://registry.npmjs.org/bare-module-lexer/-/bare-module-lexer-1.4.7.tgz", | |
| 430 | "integrity": "sha512-0klU4eMsjh/wcxi8FdHmNom2j2F4kmkXOhyJFL9qTaSFp2lE3m6BtbKgMHY8R5miqC9r8/IfA8wzXnC5Os14WA==", | |
| 431 | "license": "Apache-2.0", | |
| 432 | "dependencies": { | |
| 433 | "require-addon": "^1.0.2" | |
| 434 | }, | |
| 435 | "peerDependencies": { | |
| 436 | "bare-buffer": "*" | |
| 437 | }, | |
| 438 | "peerDependenciesMeta": { | |
| 439 | "bare-buffer": { | |
| 440 | "optional": true | |
| 441 | } | |
| 442 | } | |
| 443 | }, | |
| 184 | 444 | "node_modules/bare-module-resolve": { |
| 185 | 445 | "version": "1.12.1", |
| 186 | 446 | "resolved": "https://registry.npmjs.org/bare-module-resolve/-/bare-module-resolve-1.12.1.tgz", |
| … | … | @@ -198,6 +458,18 @@ |
| 198 | 458 | } |
| 199 | 459 | } |
| 200 | 460 | }, |
| 461 | "node_modules/bare-net": { | |
| 462 | "version": "2.3.1", | |
| 463 | "resolved": "https://registry.npmjs.org/bare-net/-/bare-net-2.3.1.tgz", | |
| 464 | "integrity": "sha512-MypSqDKpDU2Xt7FIfazn5yGvRnV09gFcIPHGWstW0gxuzA4tucTcwJSZeos97C4F89vtU5oGwXDN/HrGN6Y4Jw==", | |
| 465 | "license": "Apache-2.0", | |
| 466 | "dependencies": { | |
| 467 | "bare-events": "^2.2.2", | |
| 468 | "bare-pipe": "^4.0.0", | |
| 469 | "bare-stream": "^2.0.0", | |
| 470 | "bare-tcp": "^2.0.0" | |
| 471 | } | |
| 472 | }, | |
| 201 | 473 | "node_modules/bare-os": { |
| 202 | 474 | "version": "3.8.0", |
| 203 | 475 | "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.8.0.tgz", |
| … | … | @@ -290,6 +562,67 @@ |
| 290 | 562 | } |
| 291 | 563 | } |
| 292 | 564 | }, |
| 565 | "node_modules/bare-stylize": { | |
| 566 | "version": "0.0.1", | |
| 567 | "resolved": "https://registry.npmjs.org/bare-stylize/-/bare-stylize-0.0.1.tgz", | |
| 568 | "integrity": "sha512-l3MjmIl476bWijYWf3RbE+osl4iuXSOMudzp0vAqzIK7gPgn/+G3oAxp8Oin9CFF911KBP0LO9kts8Ci8mGZaQ==", | |
| 569 | "license": "Apache-2.0", | |
| 570 | "dependencies": { | |
| 571 | "bare-ansi-escapes": "^2.2.3", | |
| 572 | "bare-process": "^4.2.1" | |
| 573 | } | |
| 574 | }, | |
| 575 | "node_modules/bare-subprocess": { | |
| 576 | "version": "5.2.3", | |
| 577 | "resolved": "https://registry.npmjs.org/bare-subprocess/-/bare-subprocess-5.2.3.tgz", | |
| 578 | "integrity": "sha512-07wwswlV7M3sC9IykbZRZ/jHAkrXFWVLqdBWGv1y0ojCimtRD9hGwxdHmR5FUFmDUZLNsBmTYJNQqgio5+A85Q==", | |
| 579 | "license": "Apache-2.0", | |
| 580 | "dependencies": { | |
| 581 | "bare-env": "^3.0.0", | |
| 582 | "bare-events": "^2.5.4", | |
| 583 | "bare-os": "^3.0.1", | |
| 584 | "bare-pipe": "^4.0.0", | |
| 585 | "bare-url": "^2.2.2" | |
| 586 | }, | |
| 587 | "engines": { | |
| 588 | "bare": ">=1.7.0" | |
| 589 | }, | |
| 590 | "peerDependencies": { | |
| 591 | "bare-buffer": "*" | |
| 592 | }, | |
| 593 | "peerDependenciesMeta": { | |
| 594 | "bare-buffer": { | |
| 595 | "optional": true | |
| 596 | } | |
| 597 | } | |
| 598 | }, | |
| 599 | "node_modules/bare-tcp": { | |
| 600 | "version": "2.2.7", | |
| 601 | "resolved": "https://registry.npmjs.org/bare-tcp/-/bare-tcp-2.2.7.tgz", | |
| 602 | "integrity": "sha512-rjpqNQ2cOCkNo3NeYA/W4GTK3DRkl8sDHO3uos+AEswUjLC8XXMQF8WrJCSjlIowCbS6NUVxKE92X5RGXjyefg==", | |
| 603 | "license": "Apache-2.0", | |
| 604 | "dependencies": { | |
| 605 | "bare-dns": "^2.0.4", | |
| 606 | "bare-events": "^2.5.4", | |
| 607 | "bare-stream": "^2.6.4" | |
| 608 | }, | |
| 609 | "engines": { | |
| 610 | "bare": ">=1.16.0" | |
| 611 | } | |
| 612 | }, | |
| 613 | "node_modules/bare-tls": { | |
| 614 | "version": "2.2.1", | |
| 615 | "resolved": "https://registry.npmjs.org/bare-tls/-/bare-tls-2.2.1.tgz", | |
| 616 | "integrity": "sha512-hZ+ZqwrUO4dyH7/6WYkYWjgAFNJKjzwEYJiDaMnMs+eRleBDjQ3CvNZawpkw0Ar9jnM9NZK6+f6GqjkZ2FLGmQ==", | |
| 617 | "license": "Apache-2.0", | |
| 618 | "dependencies": { | |
| 619 | "bare-net": "^2.0.1", | |
| 620 | "bare-stream": "^2.6.4" | |
| 621 | }, | |
| 622 | "engines": { | |
| 623 | "bare": ">=1.7.0" | |
| 624 | } | |
| 625 | }, | |
| 293 | 626 | "node_modules/bare-tty": { |
| 294 | 627 | "version": "5.0.3", |
| 295 | 628 | "resolved": "https://registry.npmjs.org/bare-tty/-/bare-tty-5.0.3.tgz", |
| … | … | @@ -318,16 +651,137 @@ |
| 318 | 651 | "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.0.tgz", |
| 319 | 652 | "integrity": "sha512-NSTU5WN+fy/L0DDenfE8SXQna4voXuW0FHM7wH8i3/q9khUSchfPbPezO4zSFMnDGIf9YE+mt/RWhZgNRKRIXA==", |
| 320 | 653 | "license": "Apache-2.0", |
| 654 | "peer": true, | |
| 321 | 655 | "dependencies": { |
| 322 | 656 | "bare-path": "^3.0.0" |
| 323 | 657 | } |
| 324 | 658 | }, |
| 659 | "node_modules/bare-utils": { | |
| 660 | "version": "1.6.0", | |
| 661 | "resolved": "https://registry.npmjs.org/bare-utils/-/bare-utils-1.6.0.tgz", | |
| 662 | "integrity": "sha512-WhQEIkkAxkSnW7u1QgrI0AfNm5JpMruETXeYsb5qnkBJ0TTfNKygZmsh6rkoHBANaV+C/7Jed7bJP9OmEHG7rQ==", | |
| 663 | "license": "Apache-2.0", | |
| 664 | "dependencies": { | |
| 665 | "bare-debug-log": "^2.0.0", | |
| 666 | "bare-encoding": "^1.0.0", | |
| 667 | "bare-format": "^1.0.0", | |
| 668 | "bare-inspect": "^3.0.0", | |
| 669 | "bare-stylize": "^0.0.1", | |
| 670 | "bare-type": "^1.0.6" | |
| 671 | } | |
| 672 | }, | |
| 673 | "node_modules/bare-v8-to-istanbul": { | |
| 674 | "version": "1.0.2", | |
| 675 | "resolved": "https://registry.npmjs.org/bare-v8-to-istanbul/-/bare-v8-to-istanbul-1.0.2.tgz", | |
| 676 | "integrity": "sha512-CM0wPgyQtXTQ7h4q8NjbTZ/FH5he0Jkiwgpq0lFZZA5tkVZzmQ/9ul4R7E+cNlGZJIZlQZ9v4CwbgVaFG3zOAA==", | |
| 677 | "license": "Apache-2.0", | |
| 678 | "dependencies": { | |
| 679 | "bare-assert": "^1.0.2", | |
| 680 | "bare-fs": "^4.1.2", | |
| 681 | "bare-module": "^6.1.2", | |
| 682 | "bare-path": "^3.0.0", | |
| 683 | "bare-process": "^4.2.0", | |
| 684 | "bare-url": "^2.1.5", | |
| 685 | "bare-utils": "^1.2.0", | |
| 686 | "v8-to-istanbul": "^9.3.0", | |
| 687 | "which-runtime": "^1.2.1" | |
| 688 | } | |
| 689 | }, | |
| 690 | "node_modules/bare-ws": { | |
| 691 | "version": "2.1.0", | |
| 692 | "resolved": "https://registry.npmjs.org/bare-ws/-/bare-ws-2.1.0.tgz", | |
| 693 | "integrity": "sha512-2gEWlPK9iyBchACdIY6oQXgmDz3KLrChdwrPgmU3IVXOFTnxTqSUT27WE/+izd4QojHj/SsqDkQiD2HDvuTdAA==", | |
| 694 | "license": "Apache-2.0", | |
| 695 | "dependencies": { | |
| 696 | "bare-crypto": "^1.2.0", | |
| 697 | "bare-events": "^2.3.1", | |
| 698 | "bare-http1": "^4.0.0", | |
| 699 | "bare-https": "^2.0.0", | |
| 700 | "bare-stream": "^2.1.2" | |
| 701 | }, | |
| 702 | "peerDependencies": { | |
| 703 | "bare-buffer": "*", | |
| 704 | "bare-url": "*" | |
| 705 | }, | |
| 706 | "peerDependenciesMeta": { | |
| 707 | "bare-buffer": { | |
| 708 | "optional": true | |
| 709 | }, | |
| 710 | "bare-url": { | |
| 711 | "optional": true | |
| 712 | } | |
| 713 | } | |
| 714 | }, | |
| 325 | 715 | "node_modules/big-sparse-array": { |
| 326 | 716 | "version": "1.0.3", |
| 327 | 717 | "resolved": "https://registry.npmjs.org/big-sparse-array/-/big-sparse-array-1.0.3.tgz", |
| 328 | 718 | "integrity": "sha512-6RjV/3mSZORlMdpUaQ6rUSpG637cZm0//E54YYGtQg1c1O+AbZP8UTdJ/TchsDZcTVLmyWZcseBfp2HBeXUXOQ==", |
| 329 | 719 | "license": "MIT" |
| 330 | 720 | }, |
| 721 | "node_modules/bits-to-bytes": { | |
| 722 | "version": "1.3.0", | |
| 723 | "resolved": "https://registry.npmjs.org/bits-to-bytes/-/bits-to-bytes-1.3.0.tgz", | |
| 724 | "integrity": "sha512-OJoHTpFXS9bXHBCekGTByf3MqM8CGblBDIduKQeeVVeiU9dDWywSSirXIBYGgg3d1zbVuvnMa1vD4r6PA0kOKg==", | |
| 725 | "license": "ISC", | |
| 726 | "dependencies": { | |
| 727 | "b4a": "^1.5.0" | |
| 728 | } | |
| 729 | }, | |
| 730 | "node_modules/blind-relay": { | |
| 731 | "version": "1.4.0", | |
| 732 | "resolved": "https://registry.npmjs.org/blind-relay/-/blind-relay-1.4.0.tgz", | |
| 733 | "integrity": "sha512-6xt7fDfCs6eGmNNym6I9N42jmjcMQn2qwwOVnkP9ZnrkXFk6c4/tdO1xqRmDEzKzV8gigd+DVdCUG/RUYnen7Q==", | |
| 734 | "license": "Apache-2.0", | |
| 735 | "dependencies": { | |
| 736 | "b4a": "^1.6.4", | |
| 737 | "bare-events": "^2.2.0", | |
| 738 | "bits-to-bytes": "^1.3.0", | |
| 739 | "compact-encoding": "^2.12.0", | |
| 740 | "compact-encoding-bitfield": "^1.0.0", | |
| 741 | "protomux": "^3.5.1", | |
| 742 | "sodium-universal": "^5.0.0", | |
| 743 | "streamx": "^2.15.1" | |
| 744 | } | |
| 745 | }, | |
| 746 | "node_modules/bogon": { | |
| 747 | "version": "1.2.0", | |
| 748 | "resolved": "https://registry.npmjs.org/bogon/-/bogon-1.2.0.tgz", | |
| 749 | "integrity": "sha512-FqOBr/1VMzCOsoJd+fzNUMarUYki2+TKt07A2+xaulsNx4r53iJ7MV5k0jbqg7W2U0CsLqxCZOrFibdG6h6HSg==", | |
| 750 | "license": "MIT", | |
| 751 | "dependencies": { | |
| 752 | "compact-encoding": "^2.11.0", | |
| 753 | "compact-encoding-net": "^1.2.0" | |
| 754 | } | |
| 755 | }, | |
| 756 | "node_modules/brittle": { | |
| 757 | "version": "3.19.1", | |
| 758 | "resolved": "https://registry.npmjs.org/brittle/-/brittle-3.19.1.tgz", | |
| 759 | "integrity": "sha512-4Ted1Mt9o9B6oIA6ImJJCtB/Fv++ZO3IDNQgRcHOXWSYGRUidgxchaGrUBaRn+4mlneXrgInPkCPzi0jO8qKbA==", | |
| 760 | "license": "Apache-2.0", | |
| 761 | "dependencies": { | |
| 762 | "b4a": "^1.6.0", | |
| 763 | "bare-assert": "^1.0.2", | |
| 764 | "bare-cov": "^1.1.0", | |
| 765 | "bare-fs": "^4.1.6", | |
| 766 | "bare-os": "^3.6.1", | |
| 767 | "bare-path": "^3.0.0", | |
| 768 | "bare-process": "^4.2.1", | |
| 769 | "bare-subprocess": "^5.0.0", | |
| 770 | "bare-url": "^2.1.6", | |
| 771 | "error-stack-parser": "^2.1.4", | |
| 772 | "globbie": "^1.0.2", | |
| 773 | "paparam": "^1.6.2", | |
| 774 | "same-object": "^1.0.2", | |
| 775 | "test-tmp": "^1.4.0", | |
| 776 | "tmatch": "^5.0.0" | |
| 777 | }, | |
| 778 | "bin": { | |
| 779 | "brittle": "bin/node.js", | |
| 780 | "brittle-bare": "bin/bare.js", | |
| 781 | "brittle-node": "bin/node.js", | |
| 782 | "brittle-pear": "bin/pear.js" | |
| 783 | } | |
| 784 | }, | |
| 331 | 785 | "node_modules/codecs": { |
| 332 | 786 | "version": "3.1.0", |
| 333 | 787 | "resolved": "https://registry.npmjs.org/codecs/-/codecs-3.1.0.tgz", |
| … | … | @@ -346,6 +800,30 @@ |
| 346 | 800 | "b4a": "^1.3.0" |
| 347 | 801 | } |
| 348 | 802 | }, |
| 803 | "node_modules/compact-encoding-bitfield": { | |
| 804 | "version": "1.0.0", | |
| 805 | "resolved": "https://registry.npmjs.org/compact-encoding-bitfield/-/compact-encoding-bitfield-1.0.0.tgz", | |
| 806 | "integrity": "sha512-3nMVKUg+PF72UHfainmCL8uKvyWfxsjqOtUY+HiMPGLPCTjnwzoKfFAMo1Ad7nwTPdjBqtGK5b3BOFTFW4EBTg==", | |
| 807 | "license": "ISC", | |
| 808 | "dependencies": { | |
| 809 | "compact-encoding": "^2.4.1" | |
| 810 | } | |
| 811 | }, | |
| 812 | "node_modules/compact-encoding-net": { | |
| 813 | "version": "1.2.0", | |
| 814 | "resolved": "https://registry.npmjs.org/compact-encoding-net/-/compact-encoding-net-1.2.0.tgz", | |
| 815 | "integrity": "sha512-LVXpNpF7PGQeHRVVLGgYWzuVoYAaDZvKUsUxRioGfkotzvOh4AzoQF1HBH3zMNaSnx7gJXuUr3hkjnijaH/Eng==", | |
| 816 | "license": "ISC", | |
| 817 | "dependencies": { | |
| 818 | "compact-encoding": "^2.4.1" | |
| 819 | } | |
| 820 | }, | |
| 821 | "node_modules/convert-source-map": { | |
| 822 | "version": "2.0.0", | |
| 823 | "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", | |
| 824 | "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", | |
| 825 | "license": "MIT" | |
| 826 | }, | |
| 349 | 827 | "node_modules/core-coupler": { |
| 350 | 828 | "version": "2.0.0", |
| 351 | 829 | "resolved": "https://registry.npmjs.org/core-coupler/-/core-coupler-2.0.0.tgz", |
| … | … | @@ -391,6 +869,26 @@ |
| 391 | 869 | "ready-resource": "^1.2.0" |
| 392 | 870 | } |
| 393 | 871 | }, |
| 872 | "node_modules/dht-rpc": { | |
| 873 | "version": "6.26.3", | |
| 874 | "resolved": "https://registry.npmjs.org/dht-rpc/-/dht-rpc-6.26.3.tgz", | |
| 875 | "integrity": "sha512-KuLfRv/hecUHipQcTXHpVv4/N4Jhpww5sLdsrn3Edm5oHwzK9SgNV34hNt7a2aVZCOcG5SfP4AvcQ7pI+y9YNg==", | |
| 876 | "license": "MIT", | |
| 877 | "dependencies": { | |
| 878 | "adaptive-timeout": "^1.0.1", | |
| 879 | "b4a": "^1.6.1", | |
| 880 | "bare-events": "^2.2.0", | |
| 881 | "compact-encoding": "^2.11.0", | |
| 882 | "compact-encoding-net": "^1.2.0", | |
| 883 | "fast-fifo": "^1.1.0", | |
| 884 | "kademlia-routing-table": "^1.0.1", | |
| 885 | "nat-sampler": "^1.0.1", | |
| 886 | "sodium-universal": "^5.0.0", | |
| 887 | "streamx": "^2.13.2", | |
| 888 | "time-ordered-set": "^2.0.0", | |
| 889 | "udx-native": "^1.5.3" | |
| 890 | } | |
| 891 | }, | |
| 394 | 892 | "node_modules/encryption-encoding": { |
| 395 | 893 | "version": "1.0.3", |
| 396 | 894 | "resolved": "https://registry.npmjs.org/encryption-encoding/-/encryption-encoding-1.0.3.tgz", |
| … | … | @@ -400,6 +898,15 @@ |
| 400 | 898 | "hyperschema": "^1.19.0" |
| 401 | 899 | } |
| 402 | 900 | }, |
| 901 | "node_modules/error-stack-parser": { | |
| 902 | "version": "2.1.4", | |
| 903 | "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", | |
| 904 | "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", | |
| 905 | "license": "MIT", | |
| 906 | "dependencies": { | |
| 907 | "stackframe": "^1.3.4" | |
| 908 | } | |
| 909 | }, | |
| 403 | 910 | "node_modules/events-universal": { |
| 404 | 911 | "version": "1.0.1", |
| 405 | 912 | "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", |
| … | … | @@ -458,6 +965,18 @@ |
| 458 | 965 | "integrity": "sha512-IfTY0dKZM43ACyGvXkbG7De7WY7MxTS5VO6Juhe8oJKpCmrYYXoqp/cJMskkpi0k9H8wuXq0H+eI898/BCqvXg==", |
| 459 | 966 | "license": "MIT" |
| 460 | 967 | }, |
| 968 | "node_modules/globbie": { | |
| 969 | "version": "1.0.3", | |
| 970 | "resolved": "https://registry.npmjs.org/globbie/-/globbie-1.0.3.tgz", | |
| 971 | "integrity": "sha512-hcryJmKcftf82xfBWTWxuUITWIIoYO3ec104V17SMHGFl6VLbm1d1Ju9LX9L+jyJTwICpemX/dmPI/HGYoKr1A==", | |
| 972 | "license": "Apache-2.0", | |
| 973 | "dependencies": { | |
| 974 | "bare-fs": "^4.1.2", | |
| 975 | "bare-path": "^3.0.0", | |
| 976 | "bare-process": "^4.2.1", | |
| 977 | "picomatch": "^4.0.2" | |
| 978 | } | |
| 979 | }, | |
| 461 | 980 | "node_modules/hyperbee": { |
| 462 | 981 | "version": "2.27.3", |
| 463 | 982 | "resolved": "https://registry.npmjs.org/hyperbee/-/hyperbee-2.27.3.tgz", |
| … | … | @@ -558,6 +1077,35 @@ |
| 558 | 1077 | "streamx": "^2.21.1" |
| 559 | 1078 | } |
| 560 | 1079 | }, |
| 1080 | "node_modules/hyperdht": { | |
| 1081 | "version": "6.29.6", | |
| 1082 | "resolved": "https://registry.npmjs.org/hyperdht/-/hyperdht-6.29.6.tgz", | |
| 1083 | "integrity": "sha512-bMMtw02fhiALdsTVLNZ/VFmMZuEY8kxb0/Rprl6oYF4/TCS653i3FDjtJeRbqtnAzwAj6dVZ/EwhoNOSCFGaYQ==", | |
| 1084 | "license": "MIT", | |
| 1085 | "dependencies": { | |
| 1086 | "@hyperswarm/secret-stream": "^6.6.2", | |
| 1087 | "b4a": "^1.3.1", | |
| 1088 | "bare-events": "^2.2.0", | |
| 1089 | "blind-relay": "^1.3.0", | |
| 1090 | "bogon": "^1.0.0", | |
| 1091 | "compact-encoding": "^2.4.1", | |
| 1092 | "dht-rpc": "^6.15.1", | |
| 1093 | "hypercore-crypto": "^3.3.0", | |
| 1094 | "hypercore-id-encoding": "^1.2.0", | |
| 1095 | "noise-curve-ed": "^2.0.0", | |
| 1096 | "noise-handshake": "^4.0.0", | |
| 1097 | "record-cache": "^1.1.1", | |
| 1098 | "safety-catch": "^1.0.1", | |
| 1099 | "signal-promise": "^1.0.3", | |
| 1100 | "sodium-universal": "^5.0.1", | |
| 1101 | "streamx": "^2.16.1", | |
| 1102 | "unslab": "^1.3.0", | |
| 1103 | "xache": "^1.1.0" | |
| 1104 | }, | |
| 1105 | "bin": { | |
| 1106 | "hyperdht": "bin.js" | |
| 1107 | } | |
| 1108 | }, | |
| 561 | 1109 | "node_modules/hyperschema": { |
| 562 | 1110 | "version": "1.20.1", |
| 563 | 1111 | "resolved": "https://registry.npmjs.org/hyperschema/-/hyperschema-1.20.1.tgz", |
| … | … | @@ -570,6 +1118,21 @@ |
| 570 | 1118 | "generate-string": "^1.0.1" |
| 571 | 1119 | } |
| 572 | 1120 | }, |
| 1121 | "node_modules/hyperswarm": { | |
| 1122 | "version": "4.17.0", | |
| 1123 | "resolved": "https://registry.npmjs.org/hyperswarm/-/hyperswarm-4.17.0.tgz", | |
| 1124 | "integrity": "sha512-oe86sK961Ueg7rvDN/veFwG8xH+Iv6vObPhGDkPJcDVxk/NduW41ZhAcVDnHzRbm7S0eLU7WaDUvehOYoKSpRQ==", | |
| 1125 | "license": "MIT", | |
| 1126 | "dependencies": { | |
| 1127 | "b4a": "^1.3.1", | |
| 1128 | "bare-events": "^2.2.0", | |
| 1129 | "hyperdht": "^6.21.0", | |
| 1130 | "safety-catch": "^1.0.2", | |
| 1131 | "shuffled-priority-queue": "^2.1.0", | |
| 1132 | "streamx": "^2.22.1", | |
| 1133 | "unslab": "^1.3.0" | |
| 1134 | } | |
| 1135 | }, | |
| 573 | 1136 | "node_modules/index-encoder": { |
| 574 | 1137 | "version": "3.5.0", |
| 575 | 1138 | "resolved": "https://registry.npmjs.org/index-encoder/-/index-encoder-3.5.0.tgz", |
| … | … | @@ -594,6 +1157,15 @@ |
| 594 | 1157 | "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", |
| 595 | 1158 | "license": "MIT" |
| 596 | 1159 | }, |
| 1160 | "node_modules/kademlia-routing-table": { | |
| 1161 | "version": "1.0.6", | |
| 1162 | "resolved": "https://registry.npmjs.org/kademlia-routing-table/-/kademlia-routing-table-1.0.6.tgz", | |
| 1163 | "integrity": "sha512-Ve6jwIlUCYvUzBnXnzVRHDZCFgXURW9gmF3r7n05kZs/2rNbLHXwGdcq0qIaSwdmJCvtosgR4JensnVU65hzNQ==", | |
| 1164 | "license": "MIT", | |
| 1165 | "dependencies": { | |
| 1166 | "bare-events": "^2.2.0" | |
| 1167 | } | |
| 1168 | }, | |
| 597 | 1169 | "node_modules/mutexify": { |
| 598 | 1170 | "version": "1.4.0", |
| 599 | 1171 | "resolved": "https://registry.npmjs.org/mutexify/-/mutexify-1.4.0.tgz", |
| … | … | @@ -609,6 +1181,12 @@ |
| 609 | 1181 | "integrity": "sha512-7vO7n28+aYO4J+8w96AzhmU8G+Y/xpPDJz/se19ICsqj/momRbb9mh9ZUtkoJ5X3nTnPdhEJyc0qnM6yAsHBaA==", |
| 610 | 1182 | "license": "ISC" |
| 611 | 1183 | }, |
| 1184 | "node_modules/nat-sampler": { | |
| 1185 | "version": "1.0.1", | |
| 1186 | "resolved": "https://registry.npmjs.org/nat-sampler/-/nat-sampler-1.0.1.tgz", | |
| 1187 | "integrity": "sha512-yQvyNN7xbqR8crTKk3U8gRgpcV1Az+vfCEijiHu9oHHsnIl8n3x+yXNHl42M6L3czGynAVoOT9TqBfS87gDdcw==", | |
| 1188 | "license": "MIT" | |
| 1189 | }, | |
| 612 | 1190 | "node_modules/noise-curve-ed": { |
| 613 | 1191 | "version": "2.1.0", |
| 614 | 1192 | "resolved": "https://registry.npmjs.org/noise-curve-ed/-/noise-curve-ed-2.1.0.tgz", |
| … | … | @@ -637,6 +1215,18 @@ |
| 637 | 1215 | "integrity": "sha512-viyQI64VIja0Za3njIzhoEP8ZVkgPowhZPuG0E96NwBfYJ6ZIyrrlhWGtFPkdN7eYLl2L8CTWL+wla0evm1KKQ==", |
| 638 | 1216 | "license": "Apache-2.0" |
| 639 | 1217 | }, |
| 1218 | "node_modules/picomatch": { | |
| 1219 | "version": "4.0.4", | |
| 1220 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", | |
| 1221 | "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", | |
| 1222 | "license": "MIT", | |
| 1223 | "engines": { | |
| 1224 | "node": ">=12" | |
| 1225 | }, | |
| 1226 | "funding": { | |
| 1227 | "url": "https://github.com/sponsors/jonschlinkert" | |
| 1228 | } | |
| 1229 | }, | |
| 640 | 1230 | "node_modules/protocol-buffers-encodings": { |
| 641 | 1231 | "version": "1.2.0", |
| 642 | 1232 | "resolved": "https://registry.npmjs.org/protocol-buffers-encodings/-/protocol-buffers-encodings-1.2.0.tgz", |
| … | … | @@ -723,6 +1313,15 @@ |
| 723 | 1313 | "bare-events": "^2.2.0" |
| 724 | 1314 | } |
| 725 | 1315 | }, |
| 1316 | "node_modules/record-cache": { | |
| 1317 | "version": "1.2.0", | |
| 1318 | "resolved": "https://registry.npmjs.org/record-cache/-/record-cache-1.2.0.tgz", | |
| 1319 | "integrity": "sha512-kyy3HWCez2WrotaL3O4fTn0rsIdfRKOdQQcEJ9KpvmKmbffKVvwsloX063EgRUlpJIXHiDQFhJcTbZequ2uTZw==", | |
| 1320 | "license": "MIT", | |
| 1321 | "dependencies": { | |
| 1322 | "b4a": "^1.3.1" | |
| 1323 | } | |
| 1324 | }, | |
| 726 | 1325 | "node_modules/refcounter": { |
| 727 | 1326 | "version": "1.0.0", |
| 728 | 1327 | "resolved": "https://registry.npmjs.org/refcounter/-/refcounter-1.0.0.tgz", |
| … | … | @@ -777,12 +1376,27 @@ |
| 777 | 1376 | "integrity": "sha512-Zq+J1TefpoEq/HTUabo0YXX5MNvttjWYODGohgPBO2jfko8Wqx3JYMgE823szDFVamdH5PlpByvfiWScTdSYDA==", |
| 778 | 1377 | "license": "MIT" |
| 779 | 1378 | }, |
| 1379 | "node_modules/same-object": { | |
| 1380 | "version": "1.0.2", | |
| 1381 | "resolved": "https://registry.npmjs.org/same-object/-/same-object-1.0.2.tgz", | |
| 1382 | "integrity": "sha512-csHWhvUsLbIOHDM/nP+KHWM+BLPsIzWkFa8HbzaI0G7BqKXgx+7FJpKTGgLXyz5amfdY2OVBcmXTqYOMEk04og==", | |
| 1383 | "license": "MIT" | |
| 1384 | }, | |
| 780 | 1385 | "node_modules/scope-lock": { |
| 781 | 1386 | "version": "1.2.4", |
| 782 | 1387 | "resolved": "https://registry.npmjs.org/scope-lock/-/scope-lock-1.2.4.tgz", |
| 783 | 1388 | "integrity": "sha512-BpSd8VCuCxW9ZitcdIC/vjs3gMaP9bRBL5nkHcyfX2VrS52n13/rHuBA2xJ/S/4DPuRdAO/Bk8pWd8eD/gHCIA==", |
| 784 | 1389 | "license": "Apache-2.0" |
| 785 | 1390 | }, |
| 1391 | "node_modules/shuffled-priority-queue": { | |
| 1392 | "version": "2.1.0", | |
| 1393 | "resolved": "https://registry.npmjs.org/shuffled-priority-queue/-/shuffled-priority-queue-2.1.0.tgz", | |
| 1394 | "integrity": "sha512-xhdh7fHyMsr0m/w2kDfRJuBFRS96b9l8ZPNWGaQ+PMvnUnZ/Eh+gJJ9NsHBd7P9k0399WYlCLzsy18EaMfyadA==", | |
| 1395 | "license": "MIT", | |
| 1396 | "dependencies": { | |
| 1397 | "unordered-set": "^2.0.1" | |
| 1398 | } | |
| 1399 | }, | |
| 786 | 1400 | "node_modules/signal-promise": { |
| 787 | 1401 | "version": "1.0.3", |
| 788 | 1402 | "resolved": "https://registry.npmjs.org/signal-promise/-/signal-promise-1.0.3.tgz", |
| … | … | @@ -862,6 +1476,12 @@ |
| 862 | 1476 | } |
| 863 | 1477 | } |
| 864 | 1478 | }, |
| 1479 | "node_modules/stackframe": { | |
| 1480 | "version": "1.3.4", | |
| 1481 | "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", | |
| 1482 | "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", | |
| 1483 | "license": "MIT" | |
| 1484 | }, | |
| 865 | 1485 | "node_modules/streamx": { |
| 866 | 1486 | "version": "2.25.0", |
| 867 | 1487 | "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz", |
| … | … | @@ -892,6 +1512,17 @@ |
| 892 | 1512 | "streamx": "^2.12.5" |
| 893 | 1513 | } |
| 894 | 1514 | }, |
| 1515 | "node_modules/test-tmp": { | |
| 1516 | "version": "1.4.0", | |
| 1517 | "resolved": "https://registry.npmjs.org/test-tmp/-/test-tmp-1.4.0.tgz", | |
| 1518 | "integrity": "sha512-GVggxGg+jXqP2Wbju50JVLo+9E+nIOPPyWqgr63EbOnNItIKu1cEbJpTWAJeflnyGqXOtcMI7ijHRp88GUkfDA==", | |
| 1519 | "license": "MIT", | |
| 1520 | "dependencies": { | |
| 1521 | "bare-fs": "^4.0.1", | |
| 1522 | "bare-os": "^3.3.0", | |
| 1523 | "bare-path": "^3.0.0" | |
| 1524 | } | |
| 1525 | }, | |
| 895 | 1526 | "node_modules/text-decoder": { |
| 896 | 1527 | "version": "1.2.7", |
| 897 | 1528 | "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", |
| … | … | @@ -901,6 +1532,12 @@ |
| 901 | 1532 | "b4a": "^1.6.4" |
| 902 | 1533 | } |
| 903 | 1534 | }, |
| 1535 | "node_modules/time-ordered-set": { | |
| 1536 | "version": "2.0.1", | |
| 1537 | "resolved": "https://registry.npmjs.org/time-ordered-set/-/time-ordered-set-2.0.1.tgz", | |
| 1538 | "integrity": "sha512-VJEKmgSN2UiOLB8BpN8Sh2b9LGMHTP5OPrQRpnKjvOheOyzk0mufbjzjKTIG2gO4A+Y+vDJ+0TcLbpUmMLsg8A==", | |
| 1539 | "license": "MIT" | |
| 1540 | }, | |
| 904 | 1541 | "node_modules/timeout-refresh": { |
| 905 | 1542 | "version": "2.0.1", |
| 906 | 1543 | "resolved": "https://registry.npmjs.org/timeout-refresh/-/timeout-refresh-2.0.1.tgz", |
| … | … | @@ -916,6 +1553,36 @@ |
| 916 | 1553 | "b4a": "^1.6.0" |
| 917 | 1554 | } |
| 918 | 1555 | }, |
| 1556 | "node_modules/tmatch": { | |
| 1557 | "version": "5.0.0", | |
| 1558 | "resolved": "https://registry.npmjs.org/tmatch/-/tmatch-5.0.0.tgz", | |
| 1559 | "integrity": "sha512-Ib9OtBkpHn07tXP04SlN1SYRxFgTk6wSM2EBmjjxug4u5RXPRVLkdFJSS1PmrQidaSB8Lru9nRtViQBsbxzE5Q==", | |
| 1560 | "license": "ISC", | |
| 1561 | "engines": { | |
| 1562 | "node": ">=8" | |
| 1563 | } | |
| 1564 | }, | |
| 1565 | "node_modules/udx-native": { | |
| 1566 | "version": "1.19.2", | |
| 1567 | "resolved": "https://registry.npmjs.org/udx-native/-/udx-native-1.19.2.tgz", | |
| 1568 | "integrity": "sha512-RNYh+UhfryCsF5hE2ZOuIqcZ+qdipXK3UsarwxWJwsUQZFE3ybwz0mPjwb5ev1PMBcjFahWiepS/q0wwL51c2g==", | |
| 1569 | "license": "Apache-2.0", | |
| 1570 | "dependencies": { | |
| 1571 | "b4a": "^1.5.0", | |
| 1572 | "bare-events": "^2.2.0", | |
| 1573 | "require-addon": "^1.1.0", | |
| 1574 | "streamx": "^2.22.0" | |
| 1575 | }, | |
| 1576 | "engines": { | |
| 1577 | "bare": ">=1.17.4" | |
| 1578 | } | |
| 1579 | }, | |
| 1580 | "node_modules/unordered-set": { | |
| 1581 | "version": "2.0.1", | |
| 1582 | "resolved": "https://registry.npmjs.org/unordered-set/-/unordered-set-2.0.1.tgz", | |
| 1583 | "integrity": "sha512-eUmNTPzdx+q/WvOHW0bgGYLWvWHNT3PTKEQLg0MAQhc0AHASHVHoP/9YytYd4RBVariqno/mEUhVZN98CmD7bg==", | |
| 1584 | "license": "MIT" | |
| 1585 | }, | |
| 919 | 1586 | "node_modules/unslab": { |
| 920 | 1587 | "version": "1.3.0", |
| 921 | 1588 | "resolved": "https://registry.npmjs.org/unslab/-/unslab-1.3.0.tgz", |
| … | … | @@ -925,6 +1592,20 @@ |
| 925 | 1592 | "b4a": "^1.6.6" |
| 926 | 1593 | } |
| 927 | 1594 | }, |
| 1595 | "node_modules/v8-to-istanbul": { | |
| 1596 | "version": "9.3.0", | |
| 1597 | "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", | |
| 1598 | "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", | |
| 1599 | "license": "ISC", | |
| 1600 | "dependencies": { | |
| 1601 | "@jridgewell/trace-mapping": "^0.3.12", | |
| 1602 | "@types/istanbul-lib-coverage": "^2.0.1", | |
| 1603 | "convert-source-map": "^2.0.0" | |
| 1604 | }, | |
| 1605 | "engines": { | |
| 1606 | "node": ">=10.12.0" | |
| 1607 | } | |
| 1608 | }, | |
| 928 | 1609 | "node_modules/varint": { |
| 929 | 1610 | "version": "5.0.0", |
| 930 | 1611 | "resolved": "https://registry.npmjs.org/varint/-/varint-5.0.0.tgz", |
| … | … | @@ -937,6 +1618,12 @@ |
| 937 | 1618 | "integrity": "sha512-5kwCfWml7+b2NO7KrLMhYihjRx0teKkd3yGp1Xk5Vaf2JGdSh+rgVhEALAD9c/59dP+YwJHXoEO7e8QPy7gOkw==", |
| 938 | 1619 | "license": "Apache-2.0" |
| 939 | 1620 | }, |
| 1621 | "node_modules/xache": { | |
| 1622 | "version": "1.2.1", | |
| 1623 | "resolved": "https://registry.npmjs.org/xache/-/xache-1.2.1.tgz", | |
| 1624 | "integrity": "sha512-igRS6jPreJ54ABdzhh4mCDXcz+XMaWO2q1ABRV2yWYuk29jlp8VT7UBdCqNkX7rpYBbXsebVVKkwIuYZjyZNqA==", | |
| 1625 | "license": "MIT" | |
| 1626 | }, | |
| 940 | 1627 | "node_modules/z32": { |
| 941 | 1628 | "version": "1.1.0", |
| 942 | 1629 | "resolved": "https://registry.npmjs.org/z32/-/z32-1.1.0.tgz", |
| … | … | @@ -0,0 +1,23 @@ |
| 1 | name: Build Status | |
| 2 | on: | |
| 3 | push: | |
| 4 | branches: | |
| 5 | - main | |
| 6 | pull_request: | |
| 7 | branches: | |
| 8 | - main | |
| 9 | jobs: | |
| 10 | build: | |
| 11 | strategy: | |
| 12 | matrix: | |
| 13 | node-version: [lts/*] | |
| 14 | os: [ubuntu-latest, macos-latest, windows-latest] | |
| 15 | runs-on: ${{ matrix.os }} | |
| 16 | steps: | |
| 17 | - uses: actions/checkout@v2 | |
| 18 | - name: Use Node.js ${{ matrix.node-version }} | |
| 19 | uses: actions/setup-node@v2 | |
| 20 | with: | |
| 21 | node-version: ${{ matrix.node-version }} | |
| 22 | - run: npm install | |
| 23 | - run: npm test |
| … | … | @@ -0,0 +1,201 @@ |
| 1 | Apache License | |
| 2 | Version 2.0, January 2004 | |
| 3 | http://www.apache.org/licenses/ | |
| 4 | ||
| 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | |
| 6 | ||
| 7 | 1. Definitions. | |
| 8 | ||
| 9 | "License" shall mean the terms and conditions for use, reproduction, | |
| 10 | and distribution as defined by Sections 1 through 9 of this document. | |
| 11 | ||
| 12 | "Licensor" shall mean the copyright owner or entity authorized by | |
| 13 | the copyright owner that is granting the License. | |
| 14 | ||
| 15 | "Legal Entity" shall mean the union of the acting entity and all | |
| 16 | other entities that control, are controlled by, or are under common | |
| 17 | control with that entity. For the purposes of this definition, | |
| 18 | "control" means (i) the power, direct or indirect, to cause the | |
| 19 | direction or management of such entity, whether by contract or | |
| 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the | |
| 21 | outstanding shares, or (iii) beneficial ownership of such entity. | |
| 22 | ||
| 23 | "You" (or "Your") shall mean an individual or Legal Entity | |
| 24 | exercising permissions granted by this License. | |
| 25 | ||
| 26 | "Source" form shall mean the preferred form for making modifications, | |
| 27 | including but not limited to software source code, documentation | |
| 28 | source, and configuration files. | |
| 29 | ||
| 30 | "Object" form shall mean any form resulting from mechanical | |
| 31 | transformation or translation of a Source form, including but | |
| 32 | not limited to compiled object code, generated documentation, | |
| 33 | and conversions to other media types. | |
| 34 | ||
| 35 | "Work" shall mean the work of authorship, whether in Source or | |
| 36 | Object form, made available under the License, as indicated by a | |
| 37 | copyright notice that is included in or attached to the work | |
| 38 | (an example is provided in the Appendix below). | |
| 39 | ||
| 40 | "Derivative Works" shall mean any work, whether in Source or Object | |
| 41 | form, that is based on (or derived from) the Work and for which the | |
| 42 | editorial revisions, annotations, elaborations, or other modifications | |
| 43 | represent, as a whole, an original work of authorship. For the purposes | |
| 44 | of this License, Derivative Works shall not include works that remain | |
| 45 | separable from, or merely link (or bind by name) to the interfaces of, | |
| 46 | the Work and Derivative Works thereof. | |
| 47 | ||
| 48 | "Contribution" shall mean any work of authorship, including | |
| 49 | the original version of the Work and any modifications or additions | |
| 50 | to that Work or Derivative Works thereof, that is intentionally | |
| 51 | submitted to Licensor for inclusion in the Work by the copyright owner | |
| 52 | or by an individual or Legal Entity authorized to submit on behalf of | |
| 53 | the copyright owner. For the purposes of this definition, "submitted" | |
| 54 | means any form of electronic, verbal, or written communication sent | |
| 55 | to the Licensor or its representatives, including but not limited to | |
| 56 | communication on electronic mailing lists, source code control systems, | |
| 57 | and issue tracking systems that are managed by, or on behalf of, the | |
| 58 | Licensor for the purpose of discussing and improving the Work, but | |
| 59 | excluding communication that is conspicuously marked or otherwise | |
| 60 | designated in writing by the copyright owner as "Not a Contribution." | |
| 61 | ||
| 62 | "Contributor" shall mean Licensor and any individual or Legal Entity | |
| 63 | on behalf of whom a Contribution has been received by Licensor and | |
| 64 | subsequently incorporated within the Work. | |
| 65 | ||
| 66 | 2. Grant of Copyright License. Subject to the terms and conditions of | |
| 67 | this License, each Contributor hereby grants to You a perpetual, | |
| 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 69 | copyright license to reproduce, prepare Derivative Works of, | |
| 70 | publicly display, publicly perform, sublicense, and distribute the | |
| 71 | Work and such Derivative Works in Source or Object form. | |
| 72 | ||
| 73 | 3. Grant of Patent License. Subject to the terms and conditions of | |
| 74 | this License, each Contributor hereby grants to You a perpetual, | |
| 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 76 | (except as stated in this section) patent license to make, have made, | |
| 77 | use, offer to sell, sell, import, and otherwise transfer the Work, | |
| 78 | where such license applies only to those patent claims licensable | |
| 79 | by such Contributor that are necessarily infringed by their | |
| 80 | Contribution(s) alone or by combination of their Contribution(s) | |
| 81 | with the Work to which such Contribution(s) was submitted. If You | |
| 82 | institute patent litigation against any entity (including a | |
| 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work | |
| 84 | or a Contribution incorporated within the Work constitutes direct | |
| 85 | or contributory patent infringement, then any patent licenses | |
| 86 | granted to You under this License for that Work shall terminate | |
| 87 | as of the date such litigation is filed. | |
| 88 | ||
| 89 | 4. Redistribution. You may reproduce and distribute copies of the | |
| 90 | Work or Derivative Works thereof in any medium, with or without | |
| 91 | modifications, and in Source or Object form, provided that You | |
| 92 | meet the following conditions: | |
| 93 | ||
| 94 | (a) You must give any other recipients of the Work or | |
| 95 | Derivative Works a copy of this License; and | |
| 96 | ||
| 97 | (b) You must cause any modified files to carry prominent notices | |
| 98 | stating that You changed the files; and | |
| 99 | ||
| 100 | (c) You must retain, in the Source form of any Derivative Works | |
| 101 | that You distribute, all copyright, patent, trademark, and | |
| 102 | attribution notices from the Source form of the Work, | |
| 103 | excluding those notices that do not pertain to any part of | |
| 104 | the Derivative Works; and | |
| 105 | ||
| 106 | (d) If the Work includes a "NOTICE" text file as part of its | |
| 107 | distribution, then any Derivative Works that You distribute must | |
| 108 | include a readable copy of the attribution notices contained | |
| 109 | within such NOTICE file, excluding those notices that do not | |
| 110 | pertain to any part of the Derivative Works, in at least one | |
| 111 | of the following places: within a NOTICE text file distributed | |
| 112 | as part of the Derivative Works; within the Source form or | |
| 113 | documentation, if provided along with the Derivative Works; or, | |
| 114 | within a display generated by the Derivative Works, if and | |
| 115 | wherever such third-party notices normally appear. The contents | |
| 116 | of the NOTICE file are for informational purposes only and | |
| 117 | do not modify the License. You may add Your own attribution | |
| 118 | notices within Derivative Works that You distribute, alongside | |
| 119 | or as an addendum to the NOTICE text from the Work, provided | |
| 120 | that such additional attribution notices cannot be construed | |
| 121 | as modifying the License. | |
| 122 | ||
| 123 | You may add Your own copyright statement to Your modifications and | |
| 124 | may provide additional or different license terms and conditions | |
| 125 | for use, reproduction, or distribution of Your modifications, or | |
| 126 | for any such Derivative Works as a whole, provided Your use, | |
| 127 | reproduction, and distribution of the Work otherwise complies with | |
| 128 | the conditions stated in this License. | |
| 129 | ||
| 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, | |
| 131 | any Contribution intentionally submitted for inclusion in the Work | |
| 132 | by You to the Licensor shall be under the terms and conditions of | |
| 133 | this License, without any additional terms or conditions. | |
| 134 | Notwithstanding the above, nothing herein shall supersede or modify | |
| 135 | the terms of any separate license agreement you may have executed | |
| 136 | with Licensor regarding such Contributions. | |
| 137 | ||
| 138 | 6. Trademarks. This License does not grant permission to use the trade | |
| 139 | names, trademarks, service marks, or product names of the Licensor, | |
| 140 | except as required for reasonable and customary use in describing the | |
| 141 | origin of the Work and reproducing the content of the NOTICE file. | |
| 142 | ||
| 143 | 7. Disclaimer of Warranty. Unless required by applicable law or | |
| 144 | agreed to in writing, Licensor provides the Work (and each | |
| 145 | Contributor provides its Contributions) on an "AS IS" BASIS, | |
| 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |
| 147 | implied, including, without limitation, any warranties or conditions | |
| 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | |
| 149 | PARTICULAR PURPOSE. You are solely responsible for determining the | |
| 150 | appropriateness of using or redistributing the Work and assume any | |
| 151 | risks associated with Your exercise of permissions under this License. | |
| 152 | ||
| 153 | 8. Limitation of Liability. In no event and under no legal theory, | |
| 154 | whether in tort (including negligence), contract, or otherwise, | |
| 155 | unless required by applicable law (such as deliberate and grossly | |
| 156 | negligent acts) or agreed to in writing, shall any Contributor be | |
| 157 | liable to You for damages, including any direct, indirect, special, | |
| 158 | incidental, or consequential damages of any character arising as a | |
| 159 | result of this License or out of the use or inability to use the | |
| 160 | Work (including but not limited to damages for loss of goodwill, | |
| 161 | work stoppage, computer failure or malfunction, or any and all | |
| 162 | other commercial damages or losses), even if such Contributor | |
| 163 | has been advised of the possibility of such damages. | |
| 164 | ||
| 165 | 9. Accepting Warranty or Additional Liability. While redistributing | |
| 166 | the Work or Derivative Works thereof, You may choose to offer, | |
| 167 | and charge a fee for, acceptance of support, warranty, indemnity, | |
| 168 | or other liability obligations and/or rights consistent with this | |
| 169 | License. However, in accepting such obligations, You may act only | |
| 170 | on Your own behalf and on Your sole responsibility, not on behalf | |
| 171 | of any other Contributor, and only if You agree to indemnify, | |
| 172 | defend, and hold each Contributor harmless for any liability | |
| 173 | incurred by, or claims asserted against, such Contributor by reason | |
| 174 | of your accepting any such warranty or additional liability. | |
| 175 | ||
| 176 | END OF TERMS AND CONDITIONS | |
| 177 | ||
| 178 | APPENDIX: How to apply the Apache License to your work. | |
| 179 | ||
| 180 | To apply the Apache License to your work, attach the following | |
| 181 | boilerplate notice, with the fields enclosed by brackets "[]" | |
| 182 | replaced with your own identifying information. (Don't include | |
| 183 | the brackets!) The text should be enclosed in the appropriate | |
| 184 | comment syntax for the file format. We also recommend that a | |
| 185 | file or class name and description of purpose be included on the | |
| 186 | same "printed page" as the copyright notice for easier | |
| 187 | identification within third-party archives. | |
| 188 | ||
| 189 | Copyright [yyyy] [name of copyright owner] | |
| 190 | ||
| 191 | Licensed under the Apache License, Version 2.0 (the "License"); | |
| 192 | you may not use this file except in compliance with the License. | |
| 193 | You may obtain a copy of the License at | |
| 194 | ||
| 195 | http://www.apache.org/licenses/LICENSE-2.0 | |
| 196 | ||
| 197 | Unless required by applicable law or agreed to in writing, software | |
| 198 | distributed under the License is distributed on an "AS IS" BASIS, | |
| 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 200 | See the License for the specific language governing permissions and | |
| 201 | limitations under the License. |
| … | … | @@ -0,0 +1,13 @@ |
| 1 | Copyright 2023 Contributors | |
| 2 | ||
| 3 | Licensed under the Apache License, Version 2.0 (the "License"); | |
| 4 | you may not use this file except in compliance with the License. | |
| 5 | You may obtain a copy of the License at | |
| 6 | ||
| 7 | http://www.apache.org/licenses/LICENSE-2.0 | |
| 8 | ||
| 9 | Unless required by applicable law or agreed to in writing, software | |
| 10 | distributed under the License is distributed on an "AS IS" BASIS, | |
| 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 12 | See the License for the specific language governing permissions and | |
| 13 | limitations under the License. |
| … | … | @@ -0,0 +1,51 @@ |
| 1 | # @hyperswarm/testnet | |
| 2 | ||
| 3 | Small module to help you spin up a local Hyperswarm testnet. | |
| 4 | ||
| 5 | ``` | |
| 6 | npm install @hyperswarm/testnet | |
| 7 | ``` | |
| 8 | ||
| 9 | Use this if you want to use the swarm in tests, etc. | |
| 10 | ||
| 11 | ## Usage | |
| 12 | ||
| 13 | ``` js | |
| 14 | const createTestnet = require('@hyperswarm/testnet') | |
| 15 | ||
| 16 | const testnet = await createTestnet(10) // create a local testnet with 10 dht nodes | |
| 17 | ``` | |
| 18 | ||
| 19 | ## API | |
| 20 | ||
| 21 | #### `const testnet = await createTestnet(size = 10, [options])` | |
| 22 | ||
| 23 | Create a new testnet. `size` is how many DHT nodes you want in it. Options include: | |
| 24 | ||
| 25 | ```js | |
| 26 | { | |
| 27 | port: preferredLocalPort, // defaults to ANY (0) | |
| 28 | host: preferredLocalHost, // defaults to 127.0.0.1 | |
| 29 | teardown // optional async teardown helper ie, t.teardown in brittle/tap etc | |
| 30 | } | |
| 31 | ``` | |
| 32 | ||
| 33 | #### `testnet.nodes` | |
| 34 | ||
| 35 | An array of nodes in the testnet. | |
| 36 | ||
| 37 | #### `testnet.bootstrap` | |
| 38 | ||
| 39 | An array of bootstrap addresses of the testnet. | |
| 40 | ||
| 41 | #### `const node = testnet.createNode([options])` | |
| 42 | ||
| 43 | Create an additional ephemeral node and add it to the testnet. Options are the same as [`new DHT()`](https://github.com/holepunchto/hyperdht#const-node--new-dhtoptions). | |
| 44 | ||
| 45 | #### `for (const node of testnet)` | |
| 46 | ||
| 47 | Iterate over the nodes of the testnet. | |
| 48 | ||
| 49 | ## License | |
| 50 | ||
| 51 | Apache-2.0 |
| … | … | @@ -0,0 +1,25 @@ |
| 1 | #!/usr/bin/env node | |
| 2 | ||
| 3 | const testnet = require('@hyperswarm/testnet') | |
| 4 | ||
| 5 | const size = Number(arg('--size', '-s') || 30) | |
| 6 | const host = arg('--host', '-h') || '127.0.0.1' | |
| 7 | ||
| 8 | testnet(size, { host, port: 49736 }).then(function ({ nodes, bootstrap }) { | |
| 9 | console.log('Running a testnet of ' + nodes.length + ' nodes') | |
| 10 | console.log('Bootstrap:', bootstrap) | |
| 11 | }) | |
| 12 | ||
| 13 | function arg (name, short) { | |
| 14 | for (let i = 2; i < process.argv.length; i++) { | |
| 15 | const arg = process.argv[i] | |
| 16 | ||
| 17 | if (arg === name || arg === short) { | |
| 18 | return process.argv[i + 1] | |
| 19 | } | |
| 20 | if (arg.startsWith(name + '=')) { | |
| 21 | return arg.slice(name.length + 1) | |
| 22 | } | |
| 23 | } | |
| 24 | return '' | |
| 25 | } |
| … | … | @@ -0,0 +1 @@ |
| 1 | module.exports = require('hyperdht/testnet') |
| … | … | @@ -0,0 +1,29 @@ |
| 1 | { | |
| 2 | "name": "@hyperswarm/testnet", | |
| 3 | "version": "3.1.4", | |
| 4 | "description": "Small module to help you spin up a local Hyperswarm testnet", | |
| 5 | "main": "index.js", | |
| 6 | "bin": { | |
| 7 | "hyperswarm-testnet": "./bin.js" | |
| 8 | }, | |
| 9 | "dependencies": { | |
| 10 | "hyperdht": "^6.6.0" | |
| 11 | }, | |
| 12 | "devDependencies": { | |
| 13 | "brittle": "^3.0.0", | |
| 14 | "standard": "^17.0.0" | |
| 15 | }, | |
| 16 | "scripts": { | |
| 17 | "test": "standard && brittle test.mjs" | |
| 18 | }, | |
| 19 | "repository": { | |
| 20 | "type": "git", | |
| 21 | "url": "https://github.com/holepunchto/hyperswarm-testnet.git" | |
| 22 | }, | |
| 23 | "author": "Mathias Buus (@mafintosh)", | |
| 24 | "license": "Apache-2.0", | |
| 25 | "bugs": { | |
| 26 | "url": "https://github.com/holepunchto/hyperswarm-testnet/issues" | |
| 27 | }, | |
| 28 | "homepage": "https://github.com/holepunchto/hyperswarm-testnet" | |
| 29 | } |
| … | … | @@ -0,0 +1,23 @@ |
| 1 | import test from 'brittle' | |
| 2 | import createTestnet from './index.js' | |
| 3 | ||
| 4 | test('basic', async (t) => { | |
| 5 | const testnet = await createTestnet(3, t.teardown) | |
| 6 | ||
| 7 | t.is(testnet.nodes.length, 3, 'has 3 nodes') | |
| 8 | t.alike(testnet.nodes, [...testnet], 'can iterate nodes') | |
| 9 | }) | |
| 10 | ||
| 11 | test('createNode + createServer', async (t) => { | |
| 12 | t.comment('should not cause request timeouts') | |
| 13 | ||
| 14 | const testnet = await createTestnet(3, t.teardown) | |
| 15 | ||
| 16 | const a = testnet.createNode() | |
| 17 | await a.ready() | |
| 18 | ||
| 19 | const server = a.createServer() | |
| 20 | await server.listen() | |
| 21 | ||
| 22 | t.pass() | |
| 23 | }) |
| … | … | @@ -0,0 +1,19 @@ |
| 1 | Copyright 2019 Justin Ridgewell <jridgewell@google.com> | |
| 2 | ||
| 3 | Permission is hereby granted, free of charge, to any person obtaining a copy | |
| 4 | of this software and associated documentation files (the "Software"), to deal | |
| 5 | in the Software without restriction, including without limitation the rights | |
| 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| 7 | copies of the Software, and to permit persons to whom the Software is | |
| 8 | furnished to do so, subject to the following conditions: | |
| 9 | ||
| 10 | The above copyright notice and this permission notice shall be included in | |
| 11 | all copies or substantial portions of the Software. | |
| 12 | ||
| 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
| 19 | SOFTWARE. |
| … | … | @@ -0,0 +1,40 @@ |
| 1 | # @jridgewell/resolve-uri | |
| 2 | ||
| 3 | > Resolve a URI relative to an optional base URI | |
| 4 | ||
| 5 | Resolve any combination of absolute URIs, protocol-realtive URIs, absolute paths, or relative paths. | |
| 6 | ||
| 7 | ## Installation | |
| 8 | ||
| 9 | ```sh | |
| 10 | npm install @jridgewell/resolve-uri | |
| 11 | ``` | |
| 12 | ||
| 13 | ## Usage | |
| 14 | ||
| 15 | ```typescript | |
| 16 | function resolve(input: string, base?: string): string; | |
| 17 | ``` | |
| 18 | ||
| 19 | ```js | |
| 20 | import resolve from '@jridgewell/resolve-uri'; | |
| 21 | ||
| 22 | resolve('foo', 'https://example.com'); // => 'https://example.com/foo' | |
| 23 | ``` | |
| 24 | ||
| 25 | | Input | Base | Resolution | Explanation | | |
| 26 | |-----------------------|-------------------------|--------------------------------|--------------------------------------------------------------| | |
| 27 | | `https://example.com` | _any_ | `https://example.com/` | Input is normalized only | | |
| 28 | | `//example.com` | `https://base.com/` | `https://example.com/` | Input inherits the base's protocol | | |
| 29 | | `//example.com` | _rest_ | `//example.com/` | Input is normalized only | | |
| 30 | | `/example` | `https://base.com/` | `https://base.com/example` | Input inherits the base's origin | | |
| 31 | | `/example` | `//base.com/` | `//base.com/example` | Input inherits the base's host and remains protocol relative | | |
| 32 | | `/example` | _rest_ | `/example` | Input is normalized only | | |
| 33 | | `example` | `https://base.com/dir/` | `https://base.com/dir/example` | Input is joined with the base | | |
| 34 | | `example` | `https://base.com/file` | `https://base.com/example` | Input is joined with the base without its file | | |
| 35 | | `example` | `//base.com/dir/` | `//base.com/dir/example` | Input is joined with the base's last directory | | |
| 36 | | `example` | `//base.com/file` | `//base.com/example` | Input is joined with the base without its file | | |
| 37 | | `example` | `/base/dir/` | `/base/dir/example` | Input is joined with the base's last directory | | |
| 38 | | `example` | `/base/file` | `/base/example` | Input is joined with the base without its file | | |
| 39 | | `example` | `base/dir/` | `base/dir/example` | Input is joined with the base's last directory | | |
| 40 | | `example` | `base/file` | `base/example` | Input is joined with the base without its file | |
| … | … | @@ -0,0 +1,232 @@ |
| 1 | // Matches the scheme of a URL, eg "http://" | |
| 2 | const schemeRegex = /^[\w+.-]+:\/\//; | |
| 3 | /** | |
| 4 | * Matches the parts of a URL: | |
| 5 | * 1. Scheme, including ":", guaranteed. | |
| 6 | * 2. User/password, including "@", optional. | |
| 7 | * 3. Host, guaranteed. | |
| 8 | * 4. Port, including ":", optional. | |
| 9 | * 5. Path, including "/", optional. | |
| 10 | * 6. Query, including "?", optional. | |
| 11 | * 7. Hash, including "#", optional. | |
| 12 | */ | |
| 13 | const urlRegex = /^([\w+.-]+:)\/\/([^@/#?]*@)?([^:/#?]*)(:\d+)?(\/[^#?]*)?(\?[^#]*)?(#.*)?/; | |
| 14 | /** | |
| 15 | * File URLs are weird. They dont' need the regular `//` in the scheme, they may or may not start | |
| 16 | * with a leading `/`, they can have a domain (but only if they don't start with a Windows drive). | |
| 17 | * | |
| 18 | * 1. Host, optional. | |
| 19 | * 2. Path, which may include "/", guaranteed. | |
| 20 | * 3. Query, including "?", optional. | |
| 21 | * 4. Hash, including "#", optional. | |
| 22 | */ | |
| 23 | const fileRegex = /^file:(?:\/\/((?![a-z]:)[^/#?]*)?)?(\/?[^#?]*)(\?[^#]*)?(#.*)?/i; | |
| 24 | function isAbsoluteUrl(input) { | |
| 25 | return schemeRegex.test(input); | |
| 26 | } | |
| 27 | function isSchemeRelativeUrl(input) { | |
| 28 | return input.startsWith('//'); | |
| 29 | } | |
| 30 | function isAbsolutePath(input) { | |
| 31 | return input.startsWith('/'); | |
| 32 | } | |
| 33 | function isFileUrl(input) { | |
| 34 | return input.startsWith('file:'); | |
| 35 | } | |
| 36 | function isRelative(input) { | |
| 37 | return /^[.?#]/.test(input); | |
| 38 | } | |
| 39 | function parseAbsoluteUrl(input) { | |
| 40 | const match = urlRegex.exec(input); | |
| 41 | return makeUrl(match[1], match[2] || '', match[3], match[4] || '', match[5] || '/', match[6] || '', match[7] || ''); | |
| 42 | } | |
| 43 | function parseFileUrl(input) { | |
| 44 | const match = fileRegex.exec(input); | |
| 45 | const path = match[2]; | |
| 46 | return makeUrl('file:', '', match[1] || '', '', isAbsolutePath(path) ? path : '/' + path, match[3] || '', match[4] || ''); | |
| 47 | } | |
| 48 | function makeUrl(scheme, user, host, port, path, query, hash) { | |
| 49 | return { | |
| 50 | scheme, | |
| 51 | user, | |
| 52 | host, | |
| 53 | port, | |
| 54 | path, | |
| 55 | query, | |
| 56 | hash, | |
| 57 | type: 7 /* Absolute */, | |
| 58 | }; | |
| 59 | } | |
| 60 | function parseUrl(input) { | |
| 61 | if (isSchemeRelativeUrl(input)) { | |
| 62 | const url = parseAbsoluteUrl('http:' + input); | |
| 63 | url.scheme = ''; | |
| 64 | url.type = 6 /* SchemeRelative */; | |
| 65 | return url; | |
| 66 | } | |
| 67 | if (isAbsolutePath(input)) { | |
| 68 | const url = parseAbsoluteUrl('http://foo.com' + input); | |
| 69 | url.scheme = ''; | |
| 70 | url.host = ''; | |
| 71 | url.type = 5 /* AbsolutePath */; | |
| 72 | return url; | |
| 73 | } | |
| 74 | if (isFileUrl(input)) | |
| 75 | return parseFileUrl(input); | |
| 76 | if (isAbsoluteUrl(input)) | |
| 77 | return parseAbsoluteUrl(input); | |
| 78 | const url = parseAbsoluteUrl('http://foo.com/' + input); | |
| 79 | url.scheme = ''; | |
| 80 | url.host = ''; | |
| 81 | url.type = input | |
| 82 | ? input.startsWith('?') | |
| 83 | ? 3 /* Query */ | |
| 84 | : input.startsWith('#') | |
| 85 | ? 2 /* Hash */ | |
| 86 | : 4 /* RelativePath */ | |
| 87 | : 1 /* Empty */; | |
| 88 | return url; | |
| 89 | } | |
| 90 | function stripPathFilename(path) { | |
| 91 | // If a path ends with a parent directory "..", then it's a relative path with excess parent | |
| 92 | // paths. It's not a file, so we can't strip it. | |
| 93 | if (path.endsWith('/..')) | |
| 94 | return path; | |
| 95 | const index = path.lastIndexOf('/'); | |
| 96 | return path.slice(0, index + 1); | |
| 97 | } | |
| 98 | function mergePaths(url, base) { | |
| 99 | normalizePath(base, base.type); | |
| 100 | // If the path is just a "/", then it was an empty path to begin with (remember, we're a relative | |
| 101 | // path). | |
| 102 | if (url.path === '/') { | |
| 103 | url.path = base.path; | |
| 104 | } | |
| 105 | else { | |
| 106 | // Resolution happens relative to the base path's directory, not the file. | |
| 107 | url.path = stripPathFilename(base.path) + url.path; | |
| 108 | } | |
| 109 | } | |
| 110 | /** | |
| 111 | * The path can have empty directories "//", unneeded parents "foo/..", or current directory | |
| 112 | * "foo/.". We need to normalize to a standard representation. | |
| 113 | */ | |
| 114 | function normalizePath(url, type) { | |
| 115 | const rel = type <= 4 /* RelativePath */; | |
| 116 | const pieces = url.path.split('/'); | |
| 117 | // We need to preserve the first piece always, so that we output a leading slash. The item at | |
| 118 | // pieces[0] is an empty string. | |
| 119 | let pointer = 1; | |
| 120 | // Positive is the number of real directories we've output, used for popping a parent directory. | |
| 121 | // Eg, "foo/bar/.." will have a positive 2, and we can decrement to be left with just "foo". | |
| 122 | let positive = 0; | |
| 123 | // We need to keep a trailing slash if we encounter an empty directory (eg, splitting "foo/" will | |
| 124 | // generate `["foo", ""]` pieces). And, if we pop a parent directory. But once we encounter a | |
| 125 | // real directory, we won't need to append, unless the other conditions happen again. | |
| 126 | let addTrailingSlash = false; | |
| 127 | for (let i = 1; i < pieces.length; i++) { | |
| 128 | const piece = pieces[i]; | |
| 129 | // An empty directory, could be a trailing slash, or just a double "//" in the path. | |
| 130 | if (!piece) { | |
| 131 | addTrailingSlash = true; | |
| 132 | continue; | |
| 133 | } | |
| 134 | // If we encounter a real directory, then we don't need to append anymore. | |
| 135 | addTrailingSlash = false; | |
| 136 | // A current directory, which we can always drop. | |
| 137 | if (piece === '.') | |
| 138 | continue; | |
| 139 | // A parent directory, we need to see if there are any real directories we can pop. Else, we | |
| 140 | // have an excess of parents, and we'll need to keep the "..". | |
| 141 | if (piece === '..') { | |
| 142 | if (positive) { | |
| 143 | addTrailingSlash = true; | |
| 144 | positive--; | |
| 145 | pointer--; | |
| 146 | } | |
| 147 | else if (rel) { | |
| 148 | // If we're in a relativePath, then we need to keep the excess parents. Else, in an absolute | |
| 149 | // URL, protocol relative URL, or an absolute path, we don't need to keep excess. | |
| 150 | pieces[pointer++] = piece; | |
| 151 | } | |
| 152 | continue; | |
| 153 | } | |
| 154 | // We've encountered a real directory. Move it to the next insertion pointer, which accounts for | |
| 155 | // any popped or dropped directories. | |
| 156 | pieces[pointer++] = piece; | |
| 157 | positive++; | |
| 158 | } | |
| 159 | let path = ''; | |
| 160 | for (let i = 1; i < pointer; i++) { | |
| 161 | path += '/' + pieces[i]; | |
| 162 | } | |
| 163 | if (!path || (addTrailingSlash && !path.endsWith('/..'))) { | |
| 164 | path += '/'; | |
| 165 | } | |
| 166 | url.path = path; | |
| 167 | } | |
| 168 | /** | |
| 169 | * Attempts to resolve `input` URL/path relative to `base`. | |
| 170 | */ | |
| 171 | function resolve(input, base) { | |
| 172 | if (!input && !base) | |
| 173 | return ''; | |
| 174 | const url = parseUrl(input); | |
| 175 | let inputType = url.type; | |
| 176 | if (base && inputType !== 7 /* Absolute */) { | |
| 177 | const baseUrl = parseUrl(base); | |
| 178 | const baseType = baseUrl.type; | |
| 179 | switch (inputType) { | |
| 180 | case 1 /* Empty */: | |
| 181 | url.hash = baseUrl.hash; | |
| 182 | // fall through | |
| 183 | case 2 /* Hash */: | |
| 184 | url.query = baseUrl.query; | |
| 185 | // fall through | |
| 186 | case 3 /* Query */: | |
| 187 | case 4 /* RelativePath */: | |
| 188 | mergePaths(url, baseUrl); | |
| 189 | // fall through | |
| 190 | case 5 /* AbsolutePath */: | |
| 191 | // The host, user, and port are joined, you can't copy one without the others. | |
| 192 | url.user = baseUrl.user; | |
| 193 | url.host = baseUrl.host; | |
| 194 | url.port = baseUrl.port; | |
| 195 | // fall through | |
| 196 | case 6 /* SchemeRelative */: | |
| 197 | // The input doesn't have a schema at least, so we need to copy at least that over. | |
| 198 | url.scheme = baseUrl.scheme; | |
| 199 | } | |
| 200 | if (baseType > inputType) | |
| 201 | inputType = baseType; | |
| 202 | } | |
| 203 | normalizePath(url, inputType); | |
| 204 | const queryHash = url.query + url.hash; | |
| 205 | switch (inputType) { | |
| 206 | // This is impossible, because of the empty checks at the start of the function. | |
| 207 | // case UrlType.Empty: | |
| 208 | case 2 /* Hash */: | |
| 209 | case 3 /* Query */: | |
| 210 | return queryHash; | |
| 211 | case 4 /* RelativePath */: { | |
| 212 | // The first char is always a "/", and we need it to be relative. | |
| 213 | const path = url.path.slice(1); | |
| 214 | if (!path) | |
| 215 | return queryHash || '.'; | |
| 216 | if (isRelative(base || input) && !isRelative(path)) { | |
| 217 | // If base started with a leading ".", or there is no base and input started with a ".", | |
| 218 | // then we need to ensure that the relative path starts with a ".". We don't know if | |
| 219 | // relative starts with a "..", though, so check before prepending. | |
| 220 | return './' + path + queryHash; | |
| 221 | } | |
| 222 | return path + queryHash; | |
| 223 | } | |
| 224 | case 5 /* AbsolutePath */: | |
| 225 | return url.path + queryHash; | |
| 226 | default: | |
| 227 | return url.scheme + '//' + url.user + url.host + url.port + url.path + queryHash; | |
| 228 | } | |
| 229 | } | |
| 230 | ||
| 231 | export { resolve as default }; | |
| 232 | //# sourceMappingURL=resolve-uri.mjs.map |
| … | … | @@ -0,0 +1 @@ |
| 1 | {"version":3,"file":"resolve-uri.mjs","sources":["../src/resolve-uri.ts"],"sourcesContent":["// Matches the scheme of a URL, eg \"http://\"\nconst schemeRegex = /^[\\w+.-]+:\\/\\//;\n\n/**\n * Matches the parts of a URL:\n * 1. Scheme, including \":\", guaranteed.\n * 2. User/password, including \"@\", optional.\n * 3. Host, guaranteed.\n * 4. Port, including \":\", optional.\n * 5. Path, including \"/\", optional.\n * 6. Query, including \"?\", optional.\n * 7. Hash, including \"#\", optional.\n */\nconst urlRegex = /^([\\w+.-]+:)\\/\\/([^@/#?]*@)?([^:/#?]*)(:\\d+)?(\\/[^#?]*)?(\\?[^#]*)?(#.*)?/;\n\n/**\n * File URLs are weird. They dont' need the regular `//` in the scheme, they may or may not start\n * with a leading `/`, they can have a domain (but only if they don't start with a Windows drive).\n *\n * 1. Host, optional.\n * 2. Path, which may include \"/\", guaranteed.\n * 3. Query, including \"?\", optional.\n * 4. Hash, including \"#\", optional.\n */\nconst fileRegex = /^file:(?:\\/\\/((?![a-z]:)[^/#?]*)?)?(\\/?[^#?]*)(\\?[^#]*)?(#.*)?/i;\n\ntype Url = {\n scheme: string;\n user: string;\n host: string;\n port: string;\n path: string;\n query: string;\n hash: string;\n type: UrlType;\n};\n\nconst enum UrlType {\n Empty = 1,\n Hash = 2,\n Query = 3,\n RelativePath = 4,\n AbsolutePath = 5,\n SchemeRelative = 6,\n Absolute = 7,\n}\n\nfunction isAbsoluteUrl(input: string): boolean {\n return schemeRegex.test(input);\n}\n\nfunction isSchemeRelativeUrl(input: string): boolean {\n return input.startsWith('//');\n}\n\nfunction isAbsolutePath(input: string): boolean {\n return input.startsWith('/');\n}\n\nfunction isFileUrl(input: string): boolean {\n return input.startsWith('file:');\n}\n\nfunction isRelative(input: string): boolean {\n return /^[.?#]/.test(input);\n}\n\nfunction parseAbsoluteUrl(input: string): Url {\n const match = urlRegex.exec(input)!;\n return makeUrl(\n match[1],\n match[2] || '',\n match[3],\n match[4] || '',\n match[5] || '/',\n match[6] || '',\n match[7] || '',\n );\n}\n\nfunction parseFileUrl(input: string): Url {\n const match = fileRegex.exec(input)!;\n const path = match[2];\n return makeUrl(\n 'file:',\n '',\n match[1] || '',\n '',\n isAbsolutePath(path) ? path : '/' + path,\n match[3] || '',\n match[4] || '',\n );\n}\n\nfunction makeUrl(\n scheme: string,\n user: string,\n host: string,\n port: string,\n path: string,\n query: string,\n hash: string,\n): Url {\n return {\n scheme,\n user,\n host,\n port,\n path,\n query,\n hash,\n type: UrlType.Absolute,\n };\n}\n\nfunction parseUrl(input: string): Url {\n if (isSchemeRelativeUrl(input)) {\n const url = parseAbsoluteUrl('http:' + input);\n url.scheme = '';\n url.type = UrlType.SchemeRelative;\n return url;\n }\n\n if (isAbsolutePath(input)) {\n const url = parseAbsoluteUrl('http://foo.com' + input);\n url.scheme = '';\n url.host = '';\n url.type = UrlType.AbsolutePath;\n return url;\n }\n\n if (isFileUrl(input)) return parseFileUrl(input);\n\n if (isAbsoluteUrl(input)) return parseAbsoluteUrl(input);\n\n const url = parseAbsoluteUrl('http://foo.com/' + input);\n url.scheme = '';\n url.host = '';\n url.type = input\n ? input.startsWith('?')\n ? UrlType.Query\n : input.startsWith('#')\n ? UrlType.Hash\n : UrlType.RelativePath\n : UrlType.Empty;\n return url;\n}\n\nfunction stripPathFilename(path: string): string {\n // If a path ends with a parent directory \"..\", then it's a relative path with excess parent\n // paths. It's not a file, so we can't strip it.\n if (path.endsWith('/..')) return path;\n const index = path.lastIndexOf('/');\n return path.slice(0, index + 1);\n}\n\nfunction mergePaths(url: Url, base: Url) {\n normalizePath(base, base.type);\n\n // If the path is just a \"/\", then it was an empty path to begin with (remember, we're a relative\n // path).\n if (url.path === '/') {\n url.path = base.path;\n } else {\n // Resolution happens relative to the base path's directory, not the file.\n url.path = stripPathFilename(base.path) + url.path;\n }\n}\n\n/**\n * The path can have empty directories \"//\", unneeded parents \"foo/..\", or current directory\n * \"foo/.\". We need to normalize to a standard representation.\n */\nfunction normalizePath(url: Url, type: UrlType) {\n const rel = type <= UrlType.RelativePath;\n const pieces = url.path.split('/');\n\n // We need to preserve the first piece always, so that we output a leading slash. The item at\n // pieces[0] is an empty string.\n let pointer = 1;\n\n // Positive is the number of real directories we've output, used for popping a parent directory.\n // Eg, \"foo/bar/..\" will have a positive 2, and we can decrement to be left with just \"foo\".\n let positive = 0;\n\n // We need to keep a trailing slash if we encounter an empty directory (eg, splitting \"foo/\" will\n // generate `[\"foo\", \"\"]` pieces). And, if we pop a parent directory. But once we encounter a\n // real directory, we won't need to append, unless the other conditions happen again.\n let addTrailingSlash = false;\n\n for (let i = 1; i < pieces.length; i++) {\n const piece = pieces[i];\n\n // An empty directory, could be a trailing slash, or just a double \"//\" in the path.\n if (!piece) {\n addTrailingSlash = true;\n continue;\n }\n\n // If we encounter a real directory, then we don't need to append anymore.\n addTrailingSlash = false;\n\n // A current directory, which we can always drop.\n if (piece === '.') continue;\n\n // A parent directory, we need to see if there are any real directories we can pop. Else, we\n // have an excess of parents, and we'll need to keep the \"..\".\n if (piece === '..') {\n if (positive) {\n addTrailingSlash = true;\n positive--;\n pointer--;\n } else if (rel) {\n // If we're in a relativePath, then we need to keep the excess parents. Else, in an absolute\n // URL, protocol relative URL, or an absolute path, we don't need to keep excess.\n pieces[pointer++] = piece;\n }\n continue;\n }\n\n // We've encountered a real directory. Move it to the next insertion pointer, which accounts for\n // any popped or dropped directories.\n pieces[pointer++] = piece;\n positive++;\n }\n\n let path = '';\n for (let i = 1; i < pointer; i++) {\n path += '/' + pieces[i];\n }\n if (!path || (addTrailingSlash && !path.endsWith('/..'))) {\n path += '/';\n }\n url.path = path;\n}\n\n/**\n * Attempts to resolve `input` URL/path relative to `base`.\n */\nexport default function resolve(input: string, base: string | undefined): string {\n if (!input && !base) return '';\n\n const url = parseUrl(input);\n let inputType = url.type;\n\n if (base && inputType !== UrlType.Absolute) {\n const baseUrl = parseUrl(base);\n const baseType = baseUrl.type;\n\n switch (inputType) {\n case UrlType.Empty:\n url.hash = baseUrl.hash;\n // fall through\n\n case UrlType.Hash:\n url.query = baseUrl.query;\n // fall through\n\n case UrlType.Query:\n case UrlType.RelativePath:\n mergePaths(url, baseUrl);\n // fall through\n\n case UrlType.AbsolutePath:\n // The host, user, and port are joined, you can't copy one without the others.\n url.user = baseUrl.user;\n url.host = baseUrl.host;\n url.port = baseUrl.port;\n // fall through\n\n case UrlType.SchemeRelative:\n // The input doesn't have a schema at least, so we need to copy at least that over.\n url.scheme = baseUrl.scheme;\n }\n if (baseType > inputType) inputType = baseType;\n }\n\n normalizePath(url, inputType);\n\n const queryHash = url.query + url.hash;\n switch (inputType) {\n // This is impossible, because of the empty checks at the start of the function.\n // case UrlType.Empty:\n\n case UrlType.Hash:\n case UrlType.Query:\n return queryHash;\n\n case UrlType.RelativePath: {\n // The first char is always a \"/\", and we need it to be relative.\n const path = url.path.slice(1);\n\n if (!path) return queryHash || '.';\n\n if (isRelative(base || input) && !isRelative(path)) {\n // If base started with a leading \".\", or there is no base and input started with a \".\",\n // then we need to ensure that the relative path starts with a \".\". We don't know if\n // relative starts with a \"..\", though, so check before prepending.\n return './' + path + queryHash;\n }\n\n return path + queryHash;\n }\n\n case UrlType.AbsolutePath:\n return url.path + queryHash;\n\n default:\n return url.scheme + '//' + url.user + url.host + url.port + url.path + queryHash;\n }\n}\n"],"names":[],"mappings":"AAAA;AACA,MAAM,WAAW,GAAG,gBAAgB,CAAC;AAErC;;;;;;;;;;AAUA,MAAM,QAAQ,GAAG,0EAA0E,CAAC;AAE5F;;;;;;;;;AASA,MAAM,SAAS,GAAG,iEAAiE,CAAC;AAuBpF,SAAS,aAAa,CAAC,KAAa;IAClC,OAAO,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AACjC,CAAC;AAED,SAAS,mBAAmB,CAAC,KAAa;IACxC,OAAO,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;AAChC,CAAC;AAED,SAAS,cAAc,CAAC,KAAa;IACnC,OAAO,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;AAC/B,CAAC;AAED,SAAS,SAAS,CAAC,KAAa;IAC9B,OAAO,KAAK,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;AACnC,CAAC;AAED,SAAS,UAAU,CAAC,KAAa;IAC/B,OAAO,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAC9B,CAAC;AAED,SAAS,gBAAgB,CAAC,KAAa;IACrC,MAAM,KAAK,GAAG,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAE,CAAC;IACpC,OAAO,OAAO,CACZ,KAAK,CAAC,CAAC,CAAC,EACR,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,EACd,KAAK,CAAC,CAAC,CAAC,EACR,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,EACd,KAAK,CAAC,CAAC,CAAC,IAAI,GAAG,EACf,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,EACd,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CACf,CAAC;AACJ,CAAC;AAED,SAAS,YAAY,CAAC,KAAa;IACjC,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,CAAC,KAAK,CAAE,CAAC;IACrC,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;IACtB,OAAO,OAAO,CACZ,OAAO,EACP,EAAE,EACF,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,EACd,EAAE,EACF,cAAc,CAAC,IAAI,CAAC,GAAG,IAAI,GAAG,GAAG,GAAG,IAAI,EACxC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,EACd,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CACf,CAAC;AACJ,CAAC;AAED,SAAS,OAAO,CACd,MAAc,EACd,IAAY,EACZ,IAAY,EACZ,IAAY,EACZ,IAAY,EACZ,KAAa,EACb,IAAY;IAEZ,OAAO;QACL,MAAM;QACN,IAAI;QACJ,IAAI;QACJ,IAAI;QACJ,IAAI;QACJ,KAAK;QACL,IAAI;QACJ,IAAI;KACL,CAAC;AACJ,CAAC;AAED,SAAS,QAAQ,CAAC,KAAa;IAC7B,IAAI,mBAAmB,CAAC,KAAK,CAAC,EAAE;QAC9B,MAAM,GAAG,GAAG,gBAAgB,CAAC,OAAO,GAAG,KAAK,CAAC,CAAC;QAC9C,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC;QAChB,GAAG,CAAC,IAAI,0BAA0B;QAClC,OAAO,GAAG,CAAC;KACZ;IAED,IAAI,cAAc,CAAC,KAAK,CAAC,EAAE;QACzB,MAAM,GAAG,GAAG,gBAAgB,CAAC,gBAAgB,GAAG,KAAK,CAAC,CAAC;QACvD,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC;QAChB,GAAG,CAAC,IAAI,GAAG,EAAE,CAAC;QACd,GAAG,CAAC,IAAI,wBAAwB;QAChC,OAAO,GAAG,CAAC;KACZ;IAED,IAAI,SAAS,CAAC,KAAK,CAAC;QAAE,OAAO,YAAY,CAAC,KAAK,CAAC,CAAC;IAEjD,IAAI,aAAa,CAAC,KAAK,CAAC;QAAE,OAAO,gBAAgB,CAAC,KAAK,CAAC,CAAC;IAEzD,MAAM,GAAG,GAAG,gBAAgB,CAAC,iBAAiB,GAAG,KAAK,CAAC,CAAC;IACxD,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC;IAChB,GAAG,CAAC,IAAI,GAAG,EAAE,CAAC;IACd,GAAG,CAAC,IAAI,GAAG,KAAK;UACZ,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC;;cAEnB,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC;;;wBAGT;IAClB,OAAO,GAAG,CAAC;AACb,CAAC;AAED,SAAS,iBAAiB,CAAC,IAAY;;;IAGrC,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACtC,MAAM,KAAK,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IACpC,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC;AAClC,CAAC;AAED,SAAS,UAAU,CAAC,GAAQ,EAAE,IAAS;IACrC,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;;;IAI/B,IAAI,GAAG,CAAC,IAAI,KAAK,GAAG,EAAE;QACpB,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;KACtB;SAAM;;QAEL,GAAG,CAAC,IAAI,GAAG,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC;KACpD;AACH,CAAC;AAED;;;;AAIA,SAAS,aAAa,CAAC,GAAQ,EAAE,IAAa;IAC5C,MAAM,GAAG,GAAG,IAAI,yBAAyB;IACzC,MAAM,MAAM,GAAG,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;;;IAInC,IAAI,OAAO,GAAG,CAAC,CAAC;;;IAIhB,IAAI,QAAQ,GAAG,CAAC,CAAC;;;;IAKjB,IAAI,gBAAgB,GAAG,KAAK,CAAC;IAE7B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;QACtC,MAAM,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;;QAGxB,IAAI,CAAC,KAAK,EAAE;YACV,gBAAgB,GAAG,IAAI,CAAC;YACxB,SAAS;SACV;;QAGD,gBAAgB,GAAG,KAAK,CAAC;;QAGzB,IAAI,KAAK,KAAK,GAAG;YAAE,SAAS;;;QAI5B,IAAI,KAAK,KAAK,IAAI,EAAE;YAClB,IAAI,QAAQ,EAAE;gBACZ,gBAAgB,GAAG,IAAI,CAAC;gBACxB,QAAQ,EAAE,CAAC;gBACX,OAAO,EAAE,CAAC;aACX;iBAAM,IAAI,GAAG,EAAE;;;gBAGd,MAAM,CAAC,OAAO,EAAE,CAAC,GAAG,KAAK,CAAC;aAC3B;YACD,SAAS;SACV;;;QAID,MAAM,CAAC,OAAO,EAAE,CAAC,GAAG,KAAK,CAAC;QAC1B,QAAQ,EAAE,CAAC;KACZ;IAED,IAAI,IAAI,GAAG,EAAE,CAAC;IACd,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,EAAE,CAAC,EAAE,EAAE;QAChC,IAAI,IAAI,GAAG,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;KACzB;IACD,IAAI,CAAC,IAAI,KAAK,gBAAgB,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE;QACxD,IAAI,IAAI,GAAG,CAAC;KACb;IACD,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC;AAClB,CAAC;AAED;;;SAGwB,OAAO,CAAC,KAAa,EAAE,IAAwB;IACrE,IAAI,CAAC,KAAK,IAAI,CAAC,IAAI;QAAE,OAAO,EAAE,CAAC;IAE/B,MAAM,GAAG,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC5B,IAAI,SAAS,GAAG,GAAG,CAAC,IAAI,CAAC;IAEzB,IAAI,IAAI,IAAI,SAAS,uBAAuB;QAC1C,MAAM,OAAO,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;QAC/B,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;QAE9B,QAAQ,SAAS;YACf;gBACE,GAAG,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;;YAG1B;gBACE,GAAG,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;;YAG5B,mBAAmB;YACnB;gBACE,UAAU,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;;YAG3B;;gBAEE,GAAG,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;gBACxB,GAAG,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;gBACxB,GAAG,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;;YAG1B;;gBAEE,GAAG,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;SAC/B;QACD,IAAI,QAAQ,GAAG,SAAS;YAAE,SAAS,GAAG,QAAQ,CAAC;KAChD;IAED,aAAa,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;IAE9B,MAAM,SAAS,GAAG,GAAG,CAAC,KAAK,GAAG,GAAG,CAAC,IAAI,CAAC;IACvC,QAAQ,SAAS;;;QAIf,kBAAkB;QAClB;YACE,OAAO,SAAS,CAAC;QAEnB,2BAA2B;;YAEzB,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;YAE/B,IAAI,CAAC,IAAI;gBAAE,OAAO,SAAS,IAAI,GAAG,CAAC;YAEnC,IAAI,UAAU,CAAC,IAAI,IAAI,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE;;;;gBAIlD,OAAO,IAAI,GAAG,IAAI,GAAG,SAAS,CAAC;aAChC;YAED,OAAO,IAAI,GAAG,SAAS,CAAC;SACzB;QAED;YACE,OAAO,GAAG,CAAC,IAAI,GAAG,SAAS,CAAC;QAE9B;YACE,OAAO,GAAG,CAAC,MAAM,GAAG,IAAI,GAAG,GAAG,CAAC,IAAI,GAAG,GAAG,CAAC,IAAI,GAAG,GAAG,CAAC,IAAI,GAAG,GAAG,CAAC,IAAI,GAAG,SAAS,CAAC;KACpF;AACH;;;;"} |
| … | … | @@ -0,0 +1,240 @@ |
| 1 | (function (global, factory) { | |
| 2 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : | |
| 3 | typeof define === 'function' && define.amd ? define(factory) : | |
| 4 | (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.resolveURI = factory()); | |
| 5 | })(this, (function () { 'use strict'; | |
| 6 | ||
| 7 | // Matches the scheme of a URL, eg "http://" | |
| 8 | const schemeRegex = /^[\w+.-]+:\/\//; | |
| 9 | /** | |
| 10 | * Matches the parts of a URL: | |
| 11 | * 1. Scheme, including ":", guaranteed. | |
| 12 | * 2. User/password, including "@", optional. | |
| 13 | * 3. Host, guaranteed. | |
| 14 | * 4. Port, including ":", optional. | |
| 15 | * 5. Path, including "/", optional. | |
| 16 | * 6. Query, including "?", optional. | |
| 17 | * 7. Hash, including "#", optional. | |
| 18 | */ | |
| 19 | const urlRegex = /^([\w+.-]+:)\/\/([^@/#?]*@)?([^:/#?]*)(:\d+)?(\/[^#?]*)?(\?[^#]*)?(#.*)?/; | |
| 20 | /** | |
| 21 | * File URLs are weird. They dont' need the regular `//` in the scheme, they may or may not start | |
| 22 | * with a leading `/`, they can have a domain (but only if they don't start with a Windows drive). | |
| 23 | * | |
| 24 | * 1. Host, optional. | |
| 25 | * 2. Path, which may include "/", guaranteed. | |
| 26 | * 3. Query, including "?", optional. | |
| 27 | * 4. Hash, including "#", optional. | |
| 28 | */ | |
| 29 | const fileRegex = /^file:(?:\/\/((?![a-z]:)[^/#?]*)?)?(\/?[^#?]*)(\?[^#]*)?(#.*)?/i; | |
| 30 | function isAbsoluteUrl(input) { | |
| 31 | return schemeRegex.test(input); | |
| 32 | } | |
| 33 | function isSchemeRelativeUrl(input) { | |
| 34 | return input.startsWith('//'); | |
| 35 | } | |
| 36 | function isAbsolutePath(input) { | |
| 37 | return input.startsWith('/'); | |
| 38 | } | |
| 39 | function isFileUrl(input) { | |
| 40 | return input.startsWith('file:'); | |
| 41 | } | |
| 42 | function isRelative(input) { | |
| 43 | return /^[.?#]/.test(input); | |
| 44 | } | |
| 45 | function parseAbsoluteUrl(input) { | |
| 46 | const match = urlRegex.exec(input); | |
| 47 | return makeUrl(match[1], match[2] || '', match[3], match[4] || '', match[5] || '/', match[6] || '', match[7] || ''); | |
| 48 | } | |
| 49 | function parseFileUrl(input) { | |
| 50 | const match = fileRegex.exec(input); | |
| 51 | const path = match[2]; | |
| 52 | return makeUrl('file:', '', match[1] || '', '', isAbsolutePath(path) ? path : '/' + path, match[3] || '', match[4] || ''); | |
| 53 | } | |
| 54 | function makeUrl(scheme, user, host, port, path, query, hash) { | |
| 55 | return { | |
| 56 | scheme, | |
| 57 | user, | |
| 58 | host, | |
| 59 | port, | |
| 60 | path, | |
| 61 | query, | |
| 62 | hash, | |
| 63 | type: 7 /* Absolute */, | |
| 64 | }; | |
| 65 | } | |
| 66 | function parseUrl(input) { | |
| 67 | if (isSchemeRelativeUrl(input)) { | |
| 68 | const url = parseAbsoluteUrl('http:' + input); | |
| 69 | url.scheme = ''; | |
| 70 | url.type = 6 /* SchemeRelative */; | |
| 71 | return url; | |
| 72 | } | |
| 73 | if (isAbsolutePath(input)) { | |
| 74 | const url = parseAbsoluteUrl('http://foo.com' + input); | |
| 75 | url.scheme = ''; | |
| 76 | url.host = ''; | |
| 77 | url.type = 5 /* AbsolutePath */; | |
| 78 | return url; | |
| 79 | } | |
| 80 | if (isFileUrl(input)) | |
| 81 | return parseFileUrl(input); | |
| 82 | if (isAbsoluteUrl(input)) | |
| 83 | return parseAbsoluteUrl(input); | |
| 84 | const url = parseAbsoluteUrl('http://foo.com/' + input); | |
| 85 | url.scheme = ''; | |
| 86 | url.host = ''; | |
| 87 | url.type = input | |
| 88 | ? input.startsWith('?') | |
| 89 | ? 3 /* Query */ | |
| 90 | : input.startsWith('#') | |
| 91 | ? 2 /* Hash */ | |
| 92 | : 4 /* RelativePath */ | |
| 93 | : 1 /* Empty */; | |
| 94 | return url; | |
| 95 | } | |
| 96 | function stripPathFilename(path) { | |
| 97 | // If a path ends with a parent directory "..", then it's a relative path with excess parent | |
| 98 | // paths. It's not a file, so we can't strip it. | |
| 99 | if (path.endsWith('/..')) | |
| 100 | return path; | |
| 101 | const index = path.lastIndexOf('/'); | |
| 102 | return path.slice(0, index + 1); | |
| 103 | } | |
| 104 | function mergePaths(url, base) { | |
| 105 | normalizePath(base, base.type); | |
| 106 | // If the path is just a "/", then it was an empty path to begin with (remember, we're a relative | |
| 107 | // path). | |
| 108 | if (url.path === '/') { | |
| 109 | url.path = base.path; | |
| 110 | } | |
| 111 | else { | |
| 112 | // Resolution happens relative to the base path's directory, not the file. | |
| 113 | url.path = stripPathFilename(base.path) + url.path; | |
| 114 | } | |
| 115 | } | |
| 116 | /** | |
| 117 | * The path can have empty directories "//", unneeded parents "foo/..", or current directory | |
| 118 | * "foo/.". We need to normalize to a standard representation. | |
| 119 | */ | |
| 120 | function normalizePath(url, type) { | |
| 121 | const rel = type <= 4 /* RelativePath */; | |
| 122 | const pieces = url.path.split('/'); | |
| 123 | // We need to preserve the first piece always, so that we output a leading slash. The item at | |
| 124 | // pieces[0] is an empty string. | |
| 125 | let pointer = 1; | |
| 126 | // Positive is the number of real directories we've output, used for popping a parent directory. | |
| 127 | // Eg, "foo/bar/.." will have a positive 2, and we can decrement to be left with just "foo". | |
| 128 | let positive = 0; | |
| 129 | // We need to keep a trailing slash if we encounter an empty directory (eg, splitting "foo/" will | |
| 130 | // generate `["foo", ""]` pieces). And, if we pop a parent directory. But once we encounter a | |
| 131 | // real directory, we won't need to append, unless the other conditions happen again. | |
| 132 | let addTrailingSlash = false; | |
| 133 | for (let i = 1; i < pieces.length; i++) { | |
| 134 | const piece = pieces[i]; | |
| 135 | // An empty directory, could be a trailing slash, or just a double "//" in the path. | |
| 136 | if (!piece) { | |
| 137 | addTrailingSlash = true; | |
| 138 | continue; | |
| 139 | } | |
| 140 | // If we encounter a real directory, then we don't need to append anymore. | |
| 141 | addTrailingSlash = false; | |
| 142 | // A current directory, which we can always drop. | |
| 143 | if (piece === '.') | |
| 144 | continue; | |
| 145 | // A parent directory, we need to see if there are any real directories we can pop. Else, we | |
| 146 | // have an excess of parents, and we'll need to keep the "..". | |
| 147 | if (piece === '..') { | |
| 148 | if (positive) { | |
| 149 | addTrailingSlash = true; | |
| 150 | positive--; | |
| 151 | pointer--; | |
| 152 | } | |
| 153 | else if (rel) { | |
| 154 | // If we're in a relativePath, then we need to keep the excess parents. Else, in an absolute | |
| 155 | // URL, protocol relative URL, or an absolute path, we don't need to keep excess. | |
| 156 | pieces[pointer++] = piece; | |
| 157 | } | |
| 158 | continue; | |
| 159 | } | |
| 160 | // We've encountered a real directory. Move it to the next insertion pointer, which accounts for | |
| 161 | // any popped or dropped directories. | |
| 162 | pieces[pointer++] = piece; | |
| 163 | positive++; | |
| 164 | } | |
| 165 | let path = ''; | |
| 166 | for (let i = 1; i < pointer; i++) { | |
| 167 | path += '/' + pieces[i]; | |
| 168 | } | |
| 169 | if (!path || (addTrailingSlash && !path.endsWith('/..'))) { | |
| 170 | path += '/'; | |
| 171 | } | |
| 172 | url.path = path; | |
| 173 | } | |
| 174 | /** | |
| 175 | * Attempts to resolve `input` URL/path relative to `base`. | |
| 176 | */ | |
| 177 | function resolve(input, base) { | |
| 178 | if (!input && !base) | |
| 179 | return ''; | |
| 180 | const url = parseUrl(input); | |
| 181 | let inputType = url.type; | |
| 182 | if (base && inputType !== 7 /* Absolute */) { | |
| 183 | const baseUrl = parseUrl(base); | |
| 184 | const baseType = baseUrl.type; | |
| 185 | switch (inputType) { | |
| 186 | case 1 /* Empty */: | |
| 187 | url.hash = baseUrl.hash; | |
| 188 | // fall through | |
| 189 | case 2 /* Hash */: | |
| 190 | url.query = baseUrl.query; | |
| 191 | // fall through | |
| 192 | case 3 /* Query */: | |
| 193 | case 4 /* RelativePath */: | |
| 194 | mergePaths(url, baseUrl); | |
| 195 | // fall through | |
| 196 | case 5 /* AbsolutePath */: | |
| 197 | // The host, user, and port are joined, you can't copy one without the others. | |
| 198 | url.user = baseUrl.user; | |
| 199 | url.host = baseUrl.host; | |
| 200 | url.port = baseUrl.port; | |
| 201 | // fall through | |
| 202 | case 6 /* SchemeRelative */: | |
| 203 | // The input doesn't have a schema at least, so we need to copy at least that over. | |
| 204 | url.scheme = baseUrl.scheme; | |
| 205 | } | |
| 206 | if (baseType > inputType) | |
| 207 | inputType = baseType; | |
| 208 | } | |
| 209 | normalizePath(url, inputType); | |
| 210 | const queryHash = url.query + url.hash; | |
| 211 | switch (inputType) { | |
| 212 | // This is impossible, because of the empty checks at the start of the function. | |
| 213 | // case UrlType.Empty: | |
| 214 | case 2 /* Hash */: | |
| 215 | case 3 /* Query */: | |
| 216 | return queryHash; | |
| 217 | case 4 /* RelativePath */: { | |
| 218 | // The first char is always a "/", and we need it to be relative. | |
| 219 | const path = url.path.slice(1); | |
| 220 | if (!path) | |
| 221 | return queryHash || '.'; | |
| 222 | if (isRelative(base || input) && !isRelative(path)) { | |
| 223 | // If base started with a leading ".", or there is no base and input started with a ".", | |
| 224 | // then we need to ensure that the relative path starts with a ".". We don't know if | |
| 225 | // relative starts with a "..", though, so check before prepending. | |
| 226 | return './' + path + queryHash; | |
| 227 | } | |
| 228 | return path + queryHash; | |
| 229 | } | |
| 230 | case 5 /* AbsolutePath */: | |
| 231 | return url.path + queryHash; | |
| 232 | default: | |
| 233 | return url.scheme + '//' + url.user + url.host + url.port + url.path + queryHash; | |
| 234 | } | |
| 235 | } | |
| 236 | ||
| 237 | return resolve; | |
| 238 | ||
| 239 | })); | |
| 240 | //# sourceMappingURL=resolve-uri.umd.js.map |
| … | … | @@ -0,0 +1 @@ |
| 1 | {"version":3,"file":"resolve-uri.umd.js","sources":["../src/resolve-uri.ts"],"sourcesContent":["// Matches the scheme of a URL, eg \"http://\"\nconst schemeRegex = /^[\\w+.-]+:\\/\\//;\n\n/**\n * Matches the parts of a URL:\n * 1. Scheme, including \":\", guaranteed.\n * 2. User/password, including \"@\", optional.\n * 3. Host, guaranteed.\n * 4. Port, including \":\", optional.\n * 5. Path, including \"/\", optional.\n * 6. Query, including \"?\", optional.\n * 7. Hash, including \"#\", optional.\n */\nconst urlRegex = /^([\\w+.-]+:)\\/\\/([^@/#?]*@)?([^:/#?]*)(:\\d+)?(\\/[^#?]*)?(\\?[^#]*)?(#.*)?/;\n\n/**\n * File URLs are weird. They dont' need the regular `//` in the scheme, they may or may not start\n * with a leading `/`, they can have a domain (but only if they don't start with a Windows drive).\n *\n * 1. Host, optional.\n * 2. Path, which may include \"/\", guaranteed.\n * 3. Query, including \"?\", optional.\n * 4. Hash, including \"#\", optional.\n */\nconst fileRegex = /^file:(?:\\/\\/((?![a-z]:)[^/#?]*)?)?(\\/?[^#?]*)(\\?[^#]*)?(#.*)?/i;\n\ntype Url = {\n scheme: string;\n user: string;\n host: string;\n port: string;\n path: string;\n query: string;\n hash: string;\n type: UrlType;\n};\n\nconst enum UrlType {\n Empty = 1,\n Hash = 2,\n Query = 3,\n RelativePath = 4,\n AbsolutePath = 5,\n SchemeRelative = 6,\n Absolute = 7,\n}\n\nfunction isAbsoluteUrl(input: string): boolean {\n return schemeRegex.test(input);\n}\n\nfunction isSchemeRelativeUrl(input: string): boolean {\n return input.startsWith('//');\n}\n\nfunction isAbsolutePath(input: string): boolean {\n return input.startsWith('/');\n}\n\nfunction isFileUrl(input: string): boolean {\n return input.startsWith('file:');\n}\n\nfunction isRelative(input: string): boolean {\n return /^[.?#]/.test(input);\n}\n\nfunction parseAbsoluteUrl(input: string): Url {\n const match = urlRegex.exec(input)!;\n return makeUrl(\n match[1],\n match[2] || '',\n match[3],\n match[4] || '',\n match[5] || '/',\n match[6] || '',\n match[7] || '',\n );\n}\n\nfunction parseFileUrl(input: string): Url {\n const match = fileRegex.exec(input)!;\n const path = match[2];\n return makeUrl(\n 'file:',\n '',\n match[1] || '',\n '',\n isAbsolutePath(path) ? path : '/' + path,\n match[3] || '',\n match[4] || '',\n );\n}\n\nfunction makeUrl(\n scheme: string,\n user: string,\n host: string,\n port: string,\n path: string,\n query: string,\n hash: string,\n): Url {\n return {\n scheme,\n user,\n host,\n port,\n path,\n query,\n hash,\n type: UrlType.Absolute,\n };\n}\n\nfunction parseUrl(input: string): Url {\n if (isSchemeRelativeUrl(input)) {\n const url = parseAbsoluteUrl('http:' + input);\n url.scheme = '';\n url.type = UrlType.SchemeRelative;\n return url;\n }\n\n if (isAbsolutePath(input)) {\n const url = parseAbsoluteUrl('http://foo.com' + input);\n url.scheme = '';\n url.host = '';\n url.type = UrlType.AbsolutePath;\n return url;\n }\n\n if (isFileUrl(input)) return parseFileUrl(input);\n\n if (isAbsoluteUrl(input)) return parseAbsoluteUrl(input);\n\n const url = parseAbsoluteUrl('http://foo.com/' + input);\n url.scheme = '';\n url.host = '';\n url.type = input\n ? input.startsWith('?')\n ? UrlType.Query\n : input.startsWith('#')\n ? UrlType.Hash\n : UrlType.RelativePath\n : UrlType.Empty;\n return url;\n}\n\nfunction stripPathFilename(path: string): string {\n // If a path ends with a parent directory \"..\", then it's a relative path with excess parent\n // paths. It's not a file, so we can't strip it.\n if (path.endsWith('/..')) return path;\n const index = path.lastIndexOf('/');\n return path.slice(0, index + 1);\n}\n\nfunction mergePaths(url: Url, base: Url) {\n normalizePath(base, base.type);\n\n // If the path is just a \"/\", then it was an empty path to begin with (remember, we're a relative\n // path).\n if (url.path === '/') {\n url.path = base.path;\n } else {\n // Resolution happens relative to the base path's directory, not the file.\n url.path = stripPathFilename(base.path) + url.path;\n }\n}\n\n/**\n * The path can have empty directories \"//\", unneeded parents \"foo/..\", or current directory\n * \"foo/.\". We need to normalize to a standard representation.\n */\nfunction normalizePath(url: Url, type: UrlType) {\n const rel = type <= UrlType.RelativePath;\n const pieces = url.path.split('/');\n\n // We need to preserve the first piece always, so that we output a leading slash. The item at\n // pieces[0] is an empty string.\n let pointer = 1;\n\n // Positive is the number of real directories we've output, used for popping a parent directory.\n // Eg, \"foo/bar/..\" will have a positive 2, and we can decrement to be left with just \"foo\".\n let positive = 0;\n\n // We need to keep a trailing slash if we encounter an empty directory (eg, splitting \"foo/\" will\n // generate `[\"foo\", \"\"]` pieces). And, if we pop a parent directory. But once we encounter a\n // real directory, we won't need to append, unless the other conditions happen again.\n let addTrailingSlash = false;\n\n for (let i = 1; i < pieces.length; i++) {\n const piece = pieces[i];\n\n // An empty directory, could be a trailing slash, or just a double \"//\" in the path.\n if (!piece) {\n addTrailingSlash = true;\n continue;\n }\n\n // If we encounter a real directory, then we don't need to append anymore.\n addTrailingSlash = false;\n\n // A current directory, which we can always drop.\n if (piece === '.') continue;\n\n // A parent directory, we need to see if there are any real directories we can pop. Else, we\n // have an excess of parents, and we'll need to keep the \"..\".\n if (piece === '..') {\n if (positive) {\n addTrailingSlash = true;\n positive--;\n pointer--;\n } else if (rel) {\n // If we're in a relativePath, then we need to keep the excess parents. Else, in an absolute\n // URL, protocol relative URL, or an absolute path, we don't need to keep excess.\n pieces[pointer++] = piece;\n }\n continue;\n }\n\n // We've encountered a real directory. Move it to the next insertion pointer, which accounts for\n // any popped or dropped directories.\n pieces[pointer++] = piece;\n positive++;\n }\n\n let path = '';\n for (let i = 1; i < pointer; i++) {\n path += '/' + pieces[i];\n }\n if (!path || (addTrailingSlash && !path.endsWith('/..'))) {\n path += '/';\n }\n url.path = path;\n}\n\n/**\n * Attempts to resolve `input` URL/path relative to `base`.\n */\nexport default function resolve(input: string, base: string | undefined): string {\n if (!input && !base) return '';\n\n const url = parseUrl(input);\n let inputType = url.type;\n\n if (base && inputType !== UrlType.Absolute) {\n const baseUrl = parseUrl(base);\n const baseType = baseUrl.type;\n\n switch (inputType) {\n case UrlType.Empty:\n url.hash = baseUrl.hash;\n // fall through\n\n case UrlType.Hash:\n url.query = baseUrl.query;\n // fall through\n\n case UrlType.Query:\n case UrlType.RelativePath:\n mergePaths(url, baseUrl);\n // fall through\n\n case UrlType.AbsolutePath:\n // The host, user, and port are joined, you can't copy one without the others.\n url.user = baseUrl.user;\n url.host = baseUrl.host;\n url.port = baseUrl.port;\n // fall through\n\n case UrlType.SchemeRelative:\n // The input doesn't have a schema at least, so we need to copy at least that over.\n url.scheme = baseUrl.scheme;\n }\n if (baseType > inputType) inputType = baseType;\n }\n\n normalizePath(url, inputType);\n\n const queryHash = url.query + url.hash;\n switch (inputType) {\n // This is impossible, because of the empty checks at the start of the function.\n // case UrlType.Empty:\n\n case UrlType.Hash:\n case UrlType.Query:\n return queryHash;\n\n case UrlType.RelativePath: {\n // The first char is always a \"/\", and we need it to be relative.\n const path = url.path.slice(1);\n\n if (!path) return queryHash || '.';\n\n if (isRelative(base || input) && !isRelative(path)) {\n // If base started with a leading \".\", or there is no base and input started with a \".\",\n // then we need to ensure that the relative path starts with a \".\". We don't know if\n // relative starts with a \"..\", though, so check before prepending.\n return './' + path + queryHash;\n }\n\n return path + queryHash;\n }\n\n case UrlType.AbsolutePath:\n return url.path + queryHash;\n\n default:\n return url.scheme + '//' + url.user + url.host + url.port + url.path + queryHash;\n }\n}\n"],"names":[],"mappings":";;;;;;IAAA;IACA,MAAM,WAAW,GAAG,gBAAgB,CAAC;IAErC;;;;;;;;;;IAUA,MAAM,QAAQ,GAAG,0EAA0E,CAAC;IAE5F;;;;;;;;;IASA,MAAM,SAAS,GAAG,iEAAiE,CAAC;IAuBpF,SAAS,aAAa,CAAC,KAAa;QAClC,OAAO,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACjC,CAAC;IAED,SAAS,mBAAmB,CAAC,KAAa;QACxC,OAAO,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;IAChC,CAAC;IAED,SAAS,cAAc,CAAC,KAAa;QACnC,OAAO,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;IAC/B,CAAC;IAED,SAAS,SAAS,CAAC,KAAa;QAC9B,OAAO,KAAK,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;IACnC,CAAC;IAED,SAAS,UAAU,CAAC,KAAa;QAC/B,OAAO,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC9B,CAAC;IAED,SAAS,gBAAgB,CAAC,KAAa;QACrC,MAAM,KAAK,GAAG,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAE,CAAC;QACpC,OAAO,OAAO,CACZ,KAAK,CAAC,CAAC,CAAC,EACR,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,EACd,KAAK,CAAC,CAAC,CAAC,EACR,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,EACd,KAAK,CAAC,CAAC,CAAC,IAAI,GAAG,EACf,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,EACd,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CACf,CAAC;IACJ,CAAC;IAED,SAAS,YAAY,CAAC,KAAa;QACjC,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,CAAC,KAAK,CAAE,CAAC;QACrC,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACtB,OAAO,OAAO,CACZ,OAAO,EACP,EAAE,EACF,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,EACd,EAAE,EACF,cAAc,CAAC,IAAI,CAAC,GAAG,IAAI,GAAG,GAAG,GAAG,IAAI,EACxC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,EACd,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CACf,CAAC;IACJ,CAAC;IAED,SAAS,OAAO,CACd,MAAc,EACd,IAAY,EACZ,IAAY,EACZ,IAAY,EACZ,IAAY,EACZ,KAAa,EACb,IAAY;QAEZ,OAAO;YACL,MAAM;YACN,IAAI;YACJ,IAAI;YACJ,IAAI;YACJ,IAAI;YACJ,KAAK;YACL,IAAI;YACJ,IAAI;SACL,CAAC;IACJ,CAAC;IAED,SAAS,QAAQ,CAAC,KAAa;QAC7B,IAAI,mBAAmB,CAAC,KAAK,CAAC,EAAE;YAC9B,MAAM,GAAG,GAAG,gBAAgB,CAAC,OAAO,GAAG,KAAK,CAAC,CAAC;YAC9C,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC;YAChB,GAAG,CAAC,IAAI,0BAA0B;YAClC,OAAO,GAAG,CAAC;SACZ;QAED,IAAI,cAAc,CAAC,KAAK,CAAC,EAAE;YACzB,MAAM,GAAG,GAAG,gBAAgB,CAAC,gBAAgB,GAAG,KAAK,CAAC,CAAC;YACvD,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC;YAChB,GAAG,CAAC,IAAI,GAAG,EAAE,CAAC;YACd,GAAG,CAAC,IAAI,wBAAwB;YAChC,OAAO,GAAG,CAAC;SACZ;QAED,IAAI,SAAS,CAAC,KAAK,CAAC;YAAE,OAAO,YAAY,CAAC,KAAK,CAAC,CAAC;QAEjD,IAAI,aAAa,CAAC,KAAK,CAAC;YAAE,OAAO,gBAAgB,CAAC,KAAK,CAAC,CAAC;QAEzD,MAAM,GAAG,GAAG,gBAAgB,CAAC,iBAAiB,GAAG,KAAK,CAAC,CAAC;QACxD,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC;QAChB,GAAG,CAAC,IAAI,GAAG,EAAE,CAAC;QACd,GAAG,CAAC,IAAI,GAAG,KAAK;cACZ,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC;;kBAEnB,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC;;;4BAGT;QAClB,OAAO,GAAG,CAAC;IACb,CAAC;IAED,SAAS,iBAAiB,CAAC,IAAY;;;QAGrC,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QACtC,MAAM,KAAK,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;QACpC,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC;IAClC,CAAC;IAED,SAAS,UAAU,CAAC,GAAQ,EAAE,IAAS;QACrC,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;;;QAI/B,IAAI,GAAG,CAAC,IAAI,KAAK,GAAG,EAAE;YACpB,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;SACtB;aAAM;;YAEL,GAAG,CAAC,IAAI,GAAG,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC;SACpD;IACH,CAAC;IAED;;;;IAIA,SAAS,aAAa,CAAC,GAAQ,EAAE,IAAa;QAC5C,MAAM,GAAG,GAAG,IAAI,yBAAyB;QACzC,MAAM,MAAM,GAAG,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;;;QAInC,IAAI,OAAO,GAAG,CAAC,CAAC;;;QAIhB,IAAI,QAAQ,GAAG,CAAC,CAAC;;;;QAKjB,IAAI,gBAAgB,GAAG,KAAK,CAAC;QAE7B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;YACtC,MAAM,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;;YAGxB,IAAI,CAAC,KAAK,EAAE;gBACV,gBAAgB,GAAG,IAAI,CAAC;gBACxB,SAAS;aACV;;YAGD,gBAAgB,GAAG,KAAK,CAAC;;YAGzB,IAAI,KAAK,KAAK,GAAG;gBAAE,SAAS;;;YAI5B,IAAI,KAAK,KAAK,IAAI,EAAE;gBAClB,IAAI,QAAQ,EAAE;oBACZ,gBAAgB,GAAG,IAAI,CAAC;oBACxB,QAAQ,EAAE,CAAC;oBACX,OAAO,EAAE,CAAC;iBACX;qBAAM,IAAI,GAAG,EAAE;;;oBAGd,MAAM,CAAC,OAAO,EAAE,CAAC,GAAG,KAAK,CAAC;iBAC3B;gBACD,SAAS;aACV;;;YAID,MAAM,CAAC,OAAO,EAAE,CAAC,GAAG,KAAK,CAAC;YAC1B,QAAQ,EAAE,CAAC;SACZ;QAED,IAAI,IAAI,GAAG,EAAE,CAAC;QACd,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,EAAE,CAAC,EAAE,EAAE;YAChC,IAAI,IAAI,GAAG,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;SACzB;QACD,IAAI,CAAC,IAAI,KAAK,gBAAgB,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE;YACxD,IAAI,IAAI,GAAG,CAAC;SACb;QACD,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC;IAClB,CAAC;IAED;;;aAGwB,OAAO,CAAC,KAAa,EAAE,IAAwB;QACrE,IAAI,CAAC,KAAK,IAAI,CAAC,IAAI;YAAE,OAAO,EAAE,CAAC;QAE/B,MAAM,GAAG,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;QAC5B,IAAI,SAAS,GAAG,GAAG,CAAC,IAAI,CAAC;QAEzB,IAAI,IAAI,IAAI,SAAS,uBAAuB;YAC1C,MAAM,OAAO,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;YAC/B,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;YAE9B,QAAQ,SAAS;gBACf;oBACE,GAAG,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;;gBAG1B;oBACE,GAAG,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;;gBAG5B,mBAAmB;gBACnB;oBACE,UAAU,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;;gBAG3B;;oBAEE,GAAG,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;oBACxB,GAAG,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;oBACxB,GAAG,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;;gBAG1B;;oBAEE,GAAG,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;aAC/B;YACD,IAAI,QAAQ,GAAG,SAAS;gBAAE,SAAS,GAAG,QAAQ,CAAC;SAChD;QAED,aAAa,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;QAE9B,MAAM,SAAS,GAAG,GAAG,CAAC,KAAK,GAAG,GAAG,CAAC,IAAI,CAAC;QACvC,QAAQ,SAAS;;;YAIf,kBAAkB;YAClB;gBACE,OAAO,SAAS,CAAC;YAEnB,2BAA2B;;gBAEzB,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;gBAE/B,IAAI,CAAC,IAAI;oBAAE,OAAO,SAAS,IAAI,GAAG,CAAC;gBAEnC,IAAI,UAAU,CAAC,IAAI,IAAI,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE;;;;oBAIlD,OAAO,IAAI,GAAG,IAAI,GAAG,SAAS,CAAC;iBAChC;gBAED,OAAO,IAAI,GAAG,SAAS,CAAC;aACzB;YAED;gBACE,OAAO,GAAG,CAAC,IAAI,GAAG,SAAS,CAAC;YAE9B;gBACE,OAAO,GAAG,CAAC,MAAM,GAAG,IAAI,GAAG,GAAG,CAAC,IAAI,GAAG,GAAG,CAAC,IAAI,GAAG,GAAG,CAAC,IAAI,GAAG,GAAG,CAAC,IAAI,GAAG,SAAS,CAAC;SACpF;IACH;;;;;;;;"} |
| … | … | @@ -0,0 +1,4 @@ |
| 1 | /** | |
| 2 | * Attempts to resolve `input` URL/path relative to `base`. | |
| 3 | */ | |
| 4 | export default function resolve(input: string, base: string | undefined): string; |
| … | … | @@ -0,0 +1,69 @@ |
| 1 | { | |
| 2 | "name": "@jridgewell/resolve-uri", | |
| 3 | "version": "3.1.2", | |
| 4 | "description": "Resolve a URI relative to an optional base URI", | |
| 5 | "keywords": [ | |
| 6 | "resolve", | |
| 7 | "uri", | |
| 8 | "url", | |
| 9 | "path" | |
| 10 | ], | |
| 11 | "author": "Justin Ridgewell <justin@ridgewell.name>", | |
| 12 | "license": "MIT", | |
| 13 | "repository": "https://github.com/jridgewell/resolve-uri", | |
| 14 | "main": "dist/resolve-uri.umd.js", | |
| 15 | "module": "dist/resolve-uri.mjs", | |
| 16 | "types": "dist/types/resolve-uri.d.ts", | |
| 17 | "exports": { | |
| 18 | ".": [ | |
| 19 | { | |
| 20 | "types": "./dist/types/resolve-uri.d.ts", | |
| 21 | "browser": "./dist/resolve-uri.umd.js", | |
| 22 | "require": "./dist/resolve-uri.umd.js", | |
| 23 | "import": "./dist/resolve-uri.mjs" | |
| 24 | }, | |
| 25 | "./dist/resolve-uri.umd.js" | |
| 26 | ], | |
| 27 | "./package.json": "./package.json" | |
| 28 | }, | |
| 29 | "files": [ | |
| 30 | "dist" | |
| 31 | ], | |
| 32 | "engines": { | |
| 33 | "node": ">=6.0.0" | |
| 34 | }, | |
| 35 | "scripts": { | |
| 36 | "prebuild": "rm -rf dist", | |
| 37 | "build": "run-s -n build:*", | |
| 38 | "build:rollup": "rollup -c rollup.config.js", | |
| 39 | "build:ts": "tsc --project tsconfig.build.json", | |
| 40 | "lint": "run-s -n lint:*", | |
| 41 | "lint:prettier": "npm run test:lint:prettier -- --write", | |
| 42 | "lint:ts": "npm run test:lint:ts -- --fix", | |
| 43 | "pretest": "run-s build:rollup", | |
| 44 | "test": "run-s -n test:lint test:only", | |
| 45 | "test:debug": "mocha --inspect-brk", | |
| 46 | "test:lint": "run-s -n test:lint:*", | |
| 47 | "test:lint:prettier": "prettier --check '{src,test}/**/*.ts'", | |
| 48 | "test:lint:ts": "eslint '{src,test}/**/*.ts'", | |
| 49 | "test:only": "mocha", | |
| 50 | "test:coverage": "c8 mocha", | |
| 51 | "test:watch": "mocha --watch", | |
| 52 | "prepublishOnly": "npm run preversion", | |
| 53 | "preversion": "run-s test build" | |
| 54 | }, | |
| 55 | "devDependencies": { | |
| 56 | "@jridgewell/resolve-uri-latest": "npm:@jridgewell/resolve-uri@*", | |
| 57 | "@rollup/plugin-typescript": "8.3.0", | |
| 58 | "@typescript-eslint/eslint-plugin": "5.10.0", | |
| 59 | "@typescript-eslint/parser": "5.10.0", | |
| 60 | "c8": "7.11.0", | |
| 61 | "eslint": "8.7.0", | |
| 62 | "eslint-config-prettier": "8.3.0", | |
| 63 | "mocha": "9.2.0", | |
| 64 | "npm-run-all": "4.1.5", | |
| 65 | "prettier": "2.5.1", | |
| 66 | "rollup": "2.66.0", | |
| 67 | "typescript": "4.5.5" | |
| 68 | } | |
| 69 | } |
| … | … | @@ -0,0 +1,19 @@ |
| 1 | Copyright 2024 Justin Ridgewell <justin@ridgewell.name> | |
| 2 | ||
| 3 | Permission is hereby granted, free of charge, to any person obtaining a copy | |
| 4 | of this software and associated documentation files (the "Software"), to deal | |
| 5 | in the Software without restriction, including without limitation the rights | |
| 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| 7 | copies of the Software, and to permit persons to whom the Software is | |
| 8 | furnished to do so, subject to the following conditions: | |
| 9 | ||
| 10 | The above copyright notice and this permission notice shall be included in | |
| 11 | all copies or substantial portions of the Software. | |
| 12 | ||
| 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
| 19 | SOFTWARE. |
| … | … | @@ -0,0 +1,264 @@ |
| 1 | # @jridgewell/sourcemap-codec | |
| 2 | ||
| 3 | Encode/decode the `mappings` property of a [sourcemap](https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit). | |
| 4 | ||
| 5 | ||
| 6 | ## Why? | |
| 7 | ||
| 8 | Sourcemaps are difficult to generate and manipulate, because the `mappings` property – the part that actually links the generated code back to the original source – is encoded using an obscure method called [Variable-length quantity](https://en.wikipedia.org/wiki/Variable-length_quantity). On top of that, each segment in the mapping contains offsets rather than absolute indices, which means that you can't look at a segment in isolation – you have to understand the whole sourcemap. | |
| 9 | ||
| 10 | This package makes the process slightly easier. | |
| 11 | ||
| 12 | ||
| 13 | ## Installation | |
| 14 | ||
| 15 | ```bash | |
| 16 | npm install @jridgewell/sourcemap-codec | |
| 17 | ``` | |
| 18 | ||
| 19 | ||
| 20 | ## Usage | |
| 21 | ||
| 22 | ```js | |
| 23 | import { encode, decode } from '@jridgewell/sourcemap-codec'; | |
| 24 | ||
| 25 | var decoded = decode( ';EAEEA,EAAE,EAAC,CAAE;ECQY,UACC' ); | |
| 26 | ||
| 27 | assert.deepEqual( decoded, [ | |
| 28 | // the first line (of the generated code) has no mappings, | |
| 29 | // as shown by the starting semi-colon (which separates lines) | |
| 30 | [], | |
| 31 | ||
| 32 | // the second line contains four (comma-separated) segments | |
| 33 | [ | |
| 34 | // segments are encoded as you'd expect: | |
| 35 | // [ generatedCodeColumn, sourceIndex, sourceCodeLine, sourceCodeColumn, nameIndex ] | |
| 36 | ||
| 37 | // i.e. the first segment begins at column 2, and maps back to the second column | |
| 38 | // of the second line (both zero-based) of the 0th source, and uses the 0th | |
| 39 | // name in the `map.names` array | |
| 40 | [ 2, 0, 2, 2, 0 ], | |
| 41 | ||
| 42 | // the remaining segments are 4-length rather than 5-length, | |
| 43 | // because they don't map a name | |
| 44 | [ 4, 0, 2, 4 ], | |
| 45 | [ 6, 0, 2, 5 ], | |
| 46 | [ 7, 0, 2, 7 ] | |
| 47 | ], | |
| 48 | ||
| 49 | // the final line contains two segments | |
| 50 | [ | |
| 51 | [ 2, 1, 10, 19 ], | |
| 52 | [ 12, 1, 11, 20 ] | |
| 53 | ] | |
| 54 | ]); | |
| 55 | ||
| 56 | var encoded = encode( decoded ); | |
| 57 | assert.equal( encoded, ';EAEEA,EAAE,EAAC,CAAE;ECQY,UACC' ); | |
| 58 | ``` | |
| 59 | ||
| 60 | ## Benchmarks | |
| 61 | ||
| 62 | ``` | |
| 63 | node v20.10.0 | |
| 64 | ||
| 65 | amp.js.map - 45120 segments | |
| 66 | ||
| 67 | Decode Memory Usage: | |
| 68 | local code 5815135 bytes | |
| 69 | @jridgewell/sourcemap-codec 1.4.15 5868160 bytes | |
| 70 | sourcemap-codec 5492584 bytes | |
| 71 | source-map-0.6.1 13569984 bytes | |
| 72 | source-map-0.8.0 6390584 bytes | |
| 73 | chrome dev tools 8011136 bytes | |
| 74 | Smallest memory usage is sourcemap-codec | |
| 75 | ||
| 76 | Decode speed: | |
| 77 | decode: local code x 492 ops/sec ±1.22% (90 runs sampled) | |
| 78 | decode: @jridgewell/sourcemap-codec 1.4.15 x 499 ops/sec ±1.16% (89 runs sampled) | |
| 79 | decode: sourcemap-codec x 376 ops/sec ±1.66% (89 runs sampled) | |
| 80 | decode: source-map-0.6.1 x 34.99 ops/sec ±0.94% (48 runs sampled) | |
| 81 | decode: source-map-0.8.0 x 351 ops/sec ±0.07% (95 runs sampled) | |
| 82 | chrome dev tools x 165 ops/sec ±0.91% (86 runs sampled) | |
| 83 | Fastest is decode: @jridgewell/sourcemap-codec 1.4.15 | |
| 84 | ||
| 85 | Encode Memory Usage: | |
| 86 | local code 444248 bytes | |
| 87 | @jridgewell/sourcemap-codec 1.4.15 623024 bytes | |
| 88 | sourcemap-codec 8696280 bytes | |
| 89 | source-map-0.6.1 8745176 bytes | |
| 90 | source-map-0.8.0 8736624 bytes | |
| 91 | Smallest memory usage is local code | |
| 92 | ||
| 93 | Encode speed: | |
| 94 | encode: local code x 796 ops/sec ±0.11% (97 runs sampled) | |
| 95 | encode: @jridgewell/sourcemap-codec 1.4.15 x 795 ops/sec ±0.25% (98 runs sampled) | |
| 96 | encode: sourcemap-codec x 231 ops/sec ±0.83% (86 runs sampled) | |
| 97 | encode: source-map-0.6.1 x 166 ops/sec ±0.57% (86 runs sampled) | |
| 98 | encode: source-map-0.8.0 x 203 ops/sec ±0.45% (88 runs sampled) | |
| 99 | Fastest is encode: local code,encode: @jridgewell/sourcemap-codec 1.4.15 | |
| 100 | ||
| 101 | ||
| 102 | *** | |
| 103 | ||
| 104 | ||
| 105 | babel.min.js.map - 347793 segments | |
| 106 | ||
| 107 | Decode Memory Usage: | |
| 108 | local code 35424960 bytes | |
| 109 | @jridgewell/sourcemap-codec 1.4.15 35424696 bytes | |
| 110 | sourcemap-codec 36033464 bytes | |
| 111 | source-map-0.6.1 62253704 bytes | |
| 112 | source-map-0.8.0 43843920 bytes | |
| 113 | chrome dev tools 45111400 bytes | |
| 114 | Smallest memory usage is @jridgewell/sourcemap-codec 1.4.15 | |
| 115 | ||
| 116 | Decode speed: | |
| 117 | decode: local code x 38.18 ops/sec ±5.44% (52 runs sampled) | |
| 118 | decode: @jridgewell/sourcemap-codec 1.4.15 x 38.36 ops/sec ±5.02% (52 runs sampled) | |
| 119 | decode: sourcemap-codec x 34.05 ops/sec ±4.45% (47 runs sampled) | |
| 120 | decode: source-map-0.6.1 x 4.31 ops/sec ±2.76% (15 runs sampled) | |
| 121 | decode: source-map-0.8.0 x 55.60 ops/sec ±0.13% (73 runs sampled) | |
| 122 | chrome dev tools x 16.94 ops/sec ±3.78% (46 runs sampled) | |
| 123 | Fastest is decode: source-map-0.8.0 | |
| 124 | ||
| 125 | Encode Memory Usage: | |
| 126 | local code 2606016 bytes | |
| 127 | @jridgewell/sourcemap-codec 1.4.15 2626440 bytes | |
| 128 | sourcemap-codec 21152576 bytes | |
| 129 | source-map-0.6.1 25023928 bytes | |
| 130 | source-map-0.8.0 25256448 bytes | |
| 131 | Smallest memory usage is local code | |
| 132 | ||
| 133 | Encode speed: | |
| 134 | encode: local code x 127 ops/sec ±0.18% (83 runs sampled) | |
| 135 | encode: @jridgewell/sourcemap-codec 1.4.15 x 128 ops/sec ±0.26% (83 runs sampled) | |
| 136 | encode: sourcemap-codec x 29.31 ops/sec ±2.55% (53 runs sampled) | |
| 137 | encode: source-map-0.6.1 x 18.85 ops/sec ±3.19% (36 runs sampled) | |
| 138 | encode: source-map-0.8.0 x 19.34 ops/sec ±1.97% (36 runs sampled) | |
| 139 | Fastest is encode: @jridgewell/sourcemap-codec 1.4.15 | |
| 140 | ||
| 141 | ||
| 142 | *** | |
| 143 | ||
| 144 | ||
| 145 | preact.js.map - 1992 segments | |
| 146 | ||
| 147 | Decode Memory Usage: | |
| 148 | local code 261696 bytes | |
| 149 | @jridgewell/sourcemap-codec 1.4.15 244296 bytes | |
| 150 | sourcemap-codec 302816 bytes | |
| 151 | source-map-0.6.1 939176 bytes | |
| 152 | source-map-0.8.0 336 bytes | |
| 153 | chrome dev tools 587368 bytes | |
| 154 | Smallest memory usage is source-map-0.8.0 | |
| 155 | ||
| 156 | Decode speed: | |
| 157 | decode: local code x 17,782 ops/sec ±0.32% (97 runs sampled) | |
| 158 | decode: @jridgewell/sourcemap-codec 1.4.15 x 17,863 ops/sec ±0.40% (100 runs sampled) | |
| 159 | decode: sourcemap-codec x 12,453 ops/sec ±0.27% (101 runs sampled) | |
| 160 | decode: source-map-0.6.1 x 1,288 ops/sec ±1.05% (96 runs sampled) | |
| 161 | decode: source-map-0.8.0 x 9,289 ops/sec ±0.27% (101 runs sampled) | |
| 162 | chrome dev tools x 4,769 ops/sec ±0.18% (100 runs sampled) | |
| 163 | Fastest is decode: @jridgewell/sourcemap-codec 1.4.15 | |
| 164 | ||
| 165 | Encode Memory Usage: | |
| 166 | local code 262944 bytes | |
| 167 | @jridgewell/sourcemap-codec 1.4.15 25544 bytes | |
| 168 | sourcemap-codec 323048 bytes | |
| 169 | source-map-0.6.1 507808 bytes | |
| 170 | source-map-0.8.0 507480 bytes | |
| 171 | Smallest memory usage is @jridgewell/sourcemap-codec 1.4.15 | |
| 172 | ||
| 173 | Encode speed: | |
| 174 | encode: local code x 24,207 ops/sec ±0.79% (95 runs sampled) | |
| 175 | encode: @jridgewell/sourcemap-codec 1.4.15 x 24,288 ops/sec ±0.48% (96 runs sampled) | |
| 176 | encode: sourcemap-codec x 6,761 ops/sec ±0.21% (100 runs sampled) | |
| 177 | encode: source-map-0.6.1 x 5,374 ops/sec ±0.17% (99 runs sampled) | |
| 178 | encode: source-map-0.8.0 x 5,633 ops/sec ±0.32% (99 runs sampled) | |
| 179 | Fastest is encode: @jridgewell/sourcemap-codec 1.4.15,encode: local code | |
| 180 | ||
| 181 | ||
| 182 | *** | |
| 183 | ||
| 184 | ||
| 185 | react.js.map - 5726 segments | |
| 186 | ||
| 187 | Decode Memory Usage: | |
| 188 | local code 678816 bytes | |
| 189 | @jridgewell/sourcemap-codec 1.4.15 678816 bytes | |
| 190 | sourcemap-codec 816400 bytes | |
| 191 | source-map-0.6.1 2288864 bytes | |
| 192 | source-map-0.8.0 721360 bytes | |
| 193 | chrome dev tools 1012512 bytes | |
| 194 | Smallest memory usage is local code | |
| 195 | ||
| 196 | Decode speed: | |
| 197 | decode: local code x 6,178 ops/sec ±0.19% (98 runs sampled) | |
| 198 | decode: @jridgewell/sourcemap-codec 1.4.15 x 6,261 ops/sec ±0.22% (100 runs sampled) | |
| 199 | decode: sourcemap-codec x 4,472 ops/sec ±0.90% (99 runs sampled) | |
| 200 | decode: source-map-0.6.1 x 449 ops/sec ±0.31% (95 runs sampled) | |
| 201 | decode: source-map-0.8.0 x 3,219 ops/sec ±0.13% (100 runs sampled) | |
| 202 | chrome dev tools x 1,743 ops/sec ±0.20% (99 runs sampled) | |
| 203 | Fastest is decode: @jridgewell/sourcemap-codec 1.4.15 | |
| 204 | ||
| 205 | Encode Memory Usage: | |
| 206 | local code 140960 bytes | |
| 207 | @jridgewell/sourcemap-codec 1.4.15 159808 bytes | |
| 208 | sourcemap-codec 969304 bytes | |
| 209 | source-map-0.6.1 930520 bytes | |
| 210 | source-map-0.8.0 930248 bytes | |
| 211 | Smallest memory usage is local code | |
| 212 | ||
| 213 | Encode speed: | |
| 214 | encode: local code x 8,013 ops/sec ±0.19% (100 runs sampled) | |
| 215 | encode: @jridgewell/sourcemap-codec 1.4.15 x 7,989 ops/sec ±0.20% (101 runs sampled) | |
| 216 | encode: sourcemap-codec x 2,472 ops/sec ±0.21% (99 runs sampled) | |
| 217 | encode: source-map-0.6.1 x 2,200 ops/sec ±0.17% (99 runs sampled) | |
| 218 | encode: source-map-0.8.0 x 2,220 ops/sec ±0.37% (99 runs sampled) | |
| 219 | Fastest is encode: local code | |
| 220 | ||
| 221 | ||
| 222 | *** | |
| 223 | ||
| 224 | ||
| 225 | vscode.map - 2141001 segments | |
| 226 | ||
| 227 | Decode Memory Usage: | |
| 228 | local code 198955264 bytes | |
| 229 | @jridgewell/sourcemap-codec 1.4.15 199175352 bytes | |
| 230 | sourcemap-codec 199102688 bytes | |
| 231 | source-map-0.6.1 386323432 bytes | |
| 232 | source-map-0.8.0 244116432 bytes | |
| 233 | chrome dev tools 293734280 bytes | |
| 234 | Smallest memory usage is local code | |
| 235 | ||
| 236 | Decode speed: | |
| 237 | decode: local code x 3.90 ops/sec ±22.21% (15 runs sampled) | |
| 238 | decode: @jridgewell/sourcemap-codec 1.4.15 x 3.95 ops/sec ±23.53% (15 runs sampled) | |
| 239 | decode: sourcemap-codec x 3.82 ops/sec ±17.94% (14 runs sampled) | |
| 240 | decode: source-map-0.6.1 x 0.61 ops/sec ±7.81% (6 runs sampled) | |
| 241 | decode: source-map-0.8.0 x 9.54 ops/sec ±0.28% (28 runs sampled) | |
| 242 | chrome dev tools x 2.18 ops/sec ±10.58% (10 runs sampled) | |
| 243 | Fastest is decode: source-map-0.8.0 | |
| 244 | ||
| 245 | Encode Memory Usage: | |
| 246 | local code 13509880 bytes | |
| 247 | @jridgewell/sourcemap-codec 1.4.15 13537648 bytes | |
| 248 | sourcemap-codec 32540104 bytes | |
| 249 | source-map-0.6.1 127531040 bytes | |
| 250 | source-map-0.8.0 127535312 bytes | |
| 251 | Smallest memory usage is local code | |
| 252 | ||
| 253 | Encode speed: | |
| 254 | encode: local code x 20.10 ops/sec ±0.19% (38 runs sampled) | |
| 255 | encode: @jridgewell/sourcemap-codec 1.4.15 x 20.26 ops/sec ±0.32% (38 runs sampled) | |
| 256 | encode: sourcemap-codec x 5.44 ops/sec ±1.64% (18 runs sampled) | |
| 257 | encode: source-map-0.6.1 x 2.30 ops/sec ±4.79% (10 runs sampled) | |
| 258 | encode: source-map-0.8.0 x 2.46 ops/sec ±6.53% (10 runs sampled) | |
| 259 | Fastest is encode: @jridgewell/sourcemap-codec 1.4.15 | |
| 260 | ``` | |
| 261 | ||
| 262 | # License | |
| 263 | ||
| 264 | MIT |
| … | … | @@ -0,0 +1,423 @@ |
| 1 | // src/vlq.ts | |
| 2 | var comma = ",".charCodeAt(0); | |
| 3 | var semicolon = ";".charCodeAt(0); | |
| 4 | var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; | |
| 5 | var intToChar = new Uint8Array(64); | |
| 6 | var charToInt = new Uint8Array(128); | |
| 7 | for (let i = 0; i < chars.length; i++) { | |
| 8 | const c = chars.charCodeAt(i); | |
| 9 | intToChar[i] = c; | |
| 10 | charToInt[c] = i; | |
| 11 | } | |
| 12 | function decodeInteger(reader, relative) { | |
| 13 | let value = 0; | |
| 14 | let shift = 0; | |
| 15 | let integer = 0; | |
| 16 | do { | |
| 17 | const c = reader.next(); | |
| 18 | integer = charToInt[c]; | |
| 19 | value |= (integer & 31) << shift; | |
| 20 | shift += 5; | |
| 21 | } while (integer & 32); | |
| 22 | const shouldNegate = value & 1; | |
| 23 | value >>>= 1; | |
| 24 | if (shouldNegate) { | |
| 25 | value = -2147483648 | -value; | |
| 26 | } | |
| 27 | return relative + value; | |
| 28 | } | |
| 29 | function encodeInteger(builder, num, relative) { | |
| 30 | let delta = num - relative; | |
| 31 | delta = delta < 0 ? -delta << 1 | 1 : delta << 1; | |
| 32 | do { | |
| 33 | let clamped = delta & 31; | |
| 34 | delta >>>= 5; | |
| 35 | if (delta > 0) clamped |= 32; | |
| 36 | builder.write(intToChar[clamped]); | |
| 37 | } while (delta > 0); | |
| 38 | return num; | |
| 39 | } | |
| 40 | function hasMoreVlq(reader, max) { | |
| 41 | if (reader.pos >= max) return false; | |
| 42 | return reader.peek() !== comma; | |
| 43 | } | |
| 44 | ||
| 45 | // src/strings.ts | |
| 46 | var bufLength = 1024 * 16; | |
| 47 | var td = typeof TextDecoder !== "undefined" ? /* @__PURE__ */ new TextDecoder() : typeof Buffer !== "undefined" ? { | |
| 48 | decode(buf) { | |
| 49 | const out = Buffer.from(buf.buffer, buf.byteOffset, buf.byteLength); | |
| 50 | return out.toString(); | |
| 51 | } | |
| 52 | } : { | |
| 53 | decode(buf) { | |
| 54 | let out = ""; | |
| 55 | for (let i = 0; i < buf.length; i++) { | |
| 56 | out += String.fromCharCode(buf[i]); | |
| 57 | } | |
| 58 | return out; | |
| 59 | } | |
| 60 | }; | |
| 61 | var StringWriter = class { | |
| 62 | constructor() { | |
| 63 | this.pos = 0; | |
| 64 | this.out = ""; | |
| 65 | this.buffer = new Uint8Array(bufLength); | |
| 66 | } | |
| 67 | write(v) { | |
| 68 | const { buffer } = this; | |
| 69 | buffer[this.pos++] = v; | |
| 70 | if (this.pos === bufLength) { | |
| 71 | this.out += td.decode(buffer); | |
| 72 | this.pos = 0; | |
| 73 | } | |
| 74 | } | |
| 75 | flush() { | |
| 76 | const { buffer, out, pos } = this; | |
| 77 | return pos > 0 ? out + td.decode(buffer.subarray(0, pos)) : out; | |
| 78 | } | |
| 79 | }; | |
| 80 | var StringReader = class { | |
| 81 | constructor(buffer) { | |
| 82 | this.pos = 0; | |
| 83 | this.buffer = buffer; | |
| 84 | } | |
| 85 | next() { | |
| 86 | return this.buffer.charCodeAt(this.pos++); | |
| 87 | } | |
| 88 | peek() { | |
| 89 | return this.buffer.charCodeAt(this.pos); | |
| 90 | } | |
| 91 | indexOf(char) { | |
| 92 | const { buffer, pos } = this; | |
| 93 | const idx = buffer.indexOf(char, pos); | |
| 94 | return idx === -1 ? buffer.length : idx; | |
| 95 | } | |
| 96 | }; | |
| 97 | ||
| 98 | // src/scopes.ts | |
| 99 | var EMPTY = []; | |
| 100 | function decodeOriginalScopes(input) { | |
| 101 | const { length } = input; | |
| 102 | const reader = new StringReader(input); | |
| 103 | const scopes = []; | |
| 104 | const stack = []; | |
| 105 | let line = 0; | |
| 106 | for (; reader.pos < length; reader.pos++) { | |
| 107 | line = decodeInteger(reader, line); | |
| 108 | const column = decodeInteger(reader, 0); | |
| 109 | if (!hasMoreVlq(reader, length)) { | |
| 110 | const last = stack.pop(); | |
| 111 | last[2] = line; | |
| 112 | last[3] = column; | |
| 113 | continue; | |
| 114 | } | |
| 115 | const kind = decodeInteger(reader, 0); | |
| 116 | const fields = decodeInteger(reader, 0); | |
| 117 | const hasName = fields & 1; | |
| 118 | const scope = hasName ? [line, column, 0, 0, kind, decodeInteger(reader, 0)] : [line, column, 0, 0, kind]; | |
| 119 | let vars = EMPTY; | |
| 120 | if (hasMoreVlq(reader, length)) { | |
| 121 | vars = []; | |
| 122 | do { | |
| 123 | const varsIndex = decodeInteger(reader, 0); | |
| 124 | vars.push(varsIndex); | |
| 125 | } while (hasMoreVlq(reader, length)); | |
| 126 | } | |
| 127 | scope.vars = vars; | |
| 128 | scopes.push(scope); | |
| 129 | stack.push(scope); | |
| 130 | } | |
| 131 | return scopes; | |
| 132 | } | |
| 133 | function encodeOriginalScopes(scopes) { | |
| 134 | const writer = new StringWriter(); | |
| 135 | for (let i = 0; i < scopes.length; ) { | |
| 136 | i = _encodeOriginalScopes(scopes, i, writer, [0]); | |
| 137 | } | |
| 138 | return writer.flush(); | |
| 139 | } | |
| 140 | function _encodeOriginalScopes(scopes, index, writer, state) { | |
| 141 | const scope = scopes[index]; | |
| 142 | const { 0: startLine, 1: startColumn, 2: endLine, 3: endColumn, 4: kind, vars } = scope; | |
| 143 | if (index > 0) writer.write(comma); | |
| 144 | state[0] = encodeInteger(writer, startLine, state[0]); | |
| 145 | encodeInteger(writer, startColumn, 0); | |
| 146 | encodeInteger(writer, kind, 0); | |
| 147 | const fields = scope.length === 6 ? 1 : 0; | |
| 148 | encodeInteger(writer, fields, 0); | |
| 149 | if (scope.length === 6) encodeInteger(writer, scope[5], 0); | |
| 150 | for (const v of vars) { | |
| 151 | encodeInteger(writer, v, 0); | |
| 152 | } | |
| 153 | for (index++; index < scopes.length; ) { | |
| 154 | const next = scopes[index]; | |
| 155 | const { 0: l, 1: c } = next; | |
| 156 | if (l > endLine || l === endLine && c >= endColumn) { | |
| 157 | break; | |
| 158 | } | |
| 159 | index = _encodeOriginalScopes(scopes, index, writer, state); | |
| 160 | } | |
| 161 | writer.write(comma); | |
| 162 | state[0] = encodeInteger(writer, endLine, state[0]); | |
| 163 | encodeInteger(writer, endColumn, 0); | |
| 164 | return index; | |
| 165 | } | |
| 166 | function decodeGeneratedRanges(input) { | |
| 167 | const { length } = input; | |
| 168 | const reader = new StringReader(input); | |
| 169 | const ranges = []; | |
| 170 | const stack = []; | |
| 171 | let genLine = 0; | |
| 172 | let definitionSourcesIndex = 0; | |
| 173 | let definitionScopeIndex = 0; | |
| 174 | let callsiteSourcesIndex = 0; | |
| 175 | let callsiteLine = 0; | |
| 176 | let callsiteColumn = 0; | |
| 177 | let bindingLine = 0; | |
| 178 | let bindingColumn = 0; | |
| 179 | do { | |
| 180 | const semi = reader.indexOf(";"); | |
| 181 | let genColumn = 0; | |
| 182 | for (; reader.pos < semi; reader.pos++) { | |
| 183 | genColumn = decodeInteger(reader, genColumn); | |
| 184 | if (!hasMoreVlq(reader, semi)) { | |
| 185 | const last = stack.pop(); | |
| 186 | last[2] = genLine; | |
| 187 | last[3] = genColumn; | |
| 188 | continue; | |
| 189 | } | |
| 190 | const fields = decodeInteger(reader, 0); | |
| 191 | const hasDefinition = fields & 1; | |
| 192 | const hasCallsite = fields & 2; | |
| 193 | const hasScope = fields & 4; | |
| 194 | let callsite = null; | |
| 195 | let bindings = EMPTY; | |
| 196 | let range; | |
| 197 | if (hasDefinition) { | |
| 198 | const defSourcesIndex = decodeInteger(reader, definitionSourcesIndex); | |
| 199 | definitionScopeIndex = decodeInteger( | |
| 200 | reader, | |
| 201 | definitionSourcesIndex === defSourcesIndex ? definitionScopeIndex : 0 | |
| 202 | ); | |
| 203 | definitionSourcesIndex = defSourcesIndex; | |
| 204 | range = [genLine, genColumn, 0, 0, defSourcesIndex, definitionScopeIndex]; | |
| 205 | } else { | |
| 206 | range = [genLine, genColumn, 0, 0]; | |
| 207 | } | |
| 208 | range.isScope = !!hasScope; | |
| 209 | if (hasCallsite) { | |
| 210 | const prevCsi = callsiteSourcesIndex; | |
| 211 | const prevLine = callsiteLine; | |
| 212 | callsiteSourcesIndex = decodeInteger(reader, callsiteSourcesIndex); | |
| 213 | const sameSource = prevCsi === callsiteSourcesIndex; | |
| 214 | callsiteLine = decodeInteger(reader, sameSource ? callsiteLine : 0); | |
| 215 | callsiteColumn = decodeInteger( | |
| 216 | reader, | |
| 217 | sameSource && prevLine === callsiteLine ? callsiteColumn : 0 | |
| 218 | ); | |
| 219 | callsite = [callsiteSourcesIndex, callsiteLine, callsiteColumn]; | |
| 220 | } | |
| 221 | range.callsite = callsite; | |
| 222 | if (hasMoreVlq(reader, semi)) { | |
| 223 | bindings = []; | |
| 224 | do { | |
| 225 | bindingLine = genLine; | |
| 226 | bindingColumn = genColumn; | |
| 227 | const expressionsCount = decodeInteger(reader, 0); | |
| 228 | let expressionRanges; | |
| 229 | if (expressionsCount < -1) { | |
| 230 | expressionRanges = [[decodeInteger(reader, 0)]]; | |
| 231 | for (let i = -1; i > expressionsCount; i--) { | |
| 232 | const prevBl = bindingLine; | |
| 233 | bindingLine = decodeInteger(reader, bindingLine); | |
| 234 | bindingColumn = decodeInteger(reader, bindingLine === prevBl ? bindingColumn : 0); | |
| 235 | const expression = decodeInteger(reader, 0); | |
| 236 | expressionRanges.push([expression, bindingLine, bindingColumn]); | |
| 237 | } | |
| 238 | } else { | |
| 239 | expressionRanges = [[expressionsCount]]; | |
| 240 | } | |
| 241 | bindings.push(expressionRanges); | |
| 242 | } while (hasMoreVlq(reader, semi)); | |
| 243 | } | |
| 244 | range.bindings = bindings; | |
| 245 | ranges.push(range); | |
| 246 | stack.push(range); | |
| 247 | } | |
| 248 | genLine++; | |
| 249 | reader.pos = semi + 1; | |
| 250 | } while (reader.pos < length); | |
| 251 | return ranges; | |
| 252 | } | |
| 253 | function encodeGeneratedRanges(ranges) { | |
| 254 | if (ranges.length === 0) return ""; | |
| 255 | const writer = new StringWriter(); | |
| 256 | for (let i = 0; i < ranges.length; ) { | |
| 257 | i = _encodeGeneratedRanges(ranges, i, writer, [0, 0, 0, 0, 0, 0, 0]); | |
| 258 | } | |
| 259 | return writer.flush(); | |
| 260 | } | |
| 261 | function _encodeGeneratedRanges(ranges, index, writer, state) { | |
| 262 | const range = ranges[index]; | |
| 263 | const { | |
| 264 | 0: startLine, | |
| 265 | 1: startColumn, | |
| 266 | 2: endLine, | |
| 267 | 3: endColumn, | |
| 268 | isScope, | |
| 269 | callsite, | |
| 270 | bindings | |
| 271 | } = range; | |
| 272 | if (state[0] < startLine) { | |
| 273 | catchupLine(writer, state[0], startLine); | |
| 274 | state[0] = startLine; | |
| 275 | state[1] = 0; | |
| 276 | } else if (index > 0) { | |
| 277 | writer.write(comma); | |
| 278 | } | |
| 279 | state[1] = encodeInteger(writer, range[1], state[1]); | |
| 280 | const fields = (range.length === 6 ? 1 : 0) | (callsite ? 2 : 0) | (isScope ? 4 : 0); | |
| 281 | encodeInteger(writer, fields, 0); | |
| 282 | if (range.length === 6) { | |
| 283 | const { 4: sourcesIndex, 5: scopesIndex } = range; | |
| 284 | if (sourcesIndex !== state[2]) { | |
| 285 | state[3] = 0; | |
| 286 | } | |
| 287 | state[2] = encodeInteger(writer, sourcesIndex, state[2]); | |
| 288 | state[3] = encodeInteger(writer, scopesIndex, state[3]); | |
| 289 | } | |
| 290 | if (callsite) { | |
| 291 | const { 0: sourcesIndex, 1: callLine, 2: callColumn } = range.callsite; | |
| 292 | if (sourcesIndex !== state[4]) { | |
| 293 | state[5] = 0; | |
| 294 | state[6] = 0; | |
| 295 | } else if (callLine !== state[5]) { | |
| 296 | state[6] = 0; | |
| 297 | } | |
| 298 | state[4] = encodeInteger(writer, sourcesIndex, state[4]); | |
| 299 | state[5] = encodeInteger(writer, callLine, state[5]); | |
| 300 | state[6] = encodeInteger(writer, callColumn, state[6]); | |
| 301 | } | |
| 302 | if (bindings) { | |
| 303 | for (const binding of bindings) { | |
| 304 | if (binding.length > 1) encodeInteger(writer, -binding.length, 0); | |
| 305 | const expression = binding[0][0]; | |
| 306 | encodeInteger(writer, expression, 0); | |
| 307 | let bindingStartLine = startLine; | |
| 308 | let bindingStartColumn = startColumn; | |
| 309 | for (let i = 1; i < binding.length; i++) { | |
| 310 | const expRange = binding[i]; | |
| 311 | bindingStartLine = encodeInteger(writer, expRange[1], bindingStartLine); | |
| 312 | bindingStartColumn = encodeInteger(writer, expRange[2], bindingStartColumn); | |
| 313 | encodeInteger(writer, expRange[0], 0); | |
| 314 | } | |
| 315 | } | |
| 316 | } | |
| 317 | for (index++; index < ranges.length; ) { | |
| 318 | const next = ranges[index]; | |
| 319 | const { 0: l, 1: c } = next; | |
| 320 | if (l > endLine || l === endLine && c >= endColumn) { | |
| 321 | break; | |
| 322 | } | |
| 323 | index = _encodeGeneratedRanges(ranges, index, writer, state); | |
| 324 | } | |
| 325 | if (state[0] < endLine) { | |
| 326 | catchupLine(writer, state[0], endLine); | |
| 327 | state[0] = endLine; | |
| 328 | state[1] = 0; | |
| 329 | } else { | |
| 330 | writer.write(comma); | |
| 331 | } | |
| 332 | state[1] = encodeInteger(writer, endColumn, state[1]); | |
| 333 | return index; | |
| 334 | } | |
| 335 | function catchupLine(writer, lastLine, line) { | |
| 336 | do { | |
| 337 | writer.write(semicolon); | |
| 338 | } while (++lastLine < line); | |
| 339 | } | |
| 340 | ||
| 341 | // src/sourcemap-codec.ts | |
| 342 | function decode(mappings) { | |
| 343 | const { length } = mappings; | |
| 344 | const reader = new StringReader(mappings); | |
| 345 | const decoded = []; | |
| 346 | let genColumn = 0; | |
| 347 | let sourcesIndex = 0; | |
| 348 | let sourceLine = 0; | |
| 349 | let sourceColumn = 0; | |
| 350 | let namesIndex = 0; | |
| 351 | do { | |
| 352 | const semi = reader.indexOf(";"); | |
| 353 | const line = []; | |
| 354 | let sorted = true; | |
| 355 | let lastCol = 0; | |
| 356 | genColumn = 0; | |
| 357 | while (reader.pos < semi) { | |
| 358 | let seg; | |
| 359 | genColumn = decodeInteger(reader, genColumn); | |
| 360 | if (genColumn < lastCol) sorted = false; | |
| 361 | lastCol = genColumn; | |
| 362 | if (hasMoreVlq(reader, semi)) { | |
| 363 | sourcesIndex = decodeInteger(reader, sourcesIndex); | |
| 364 | sourceLine = decodeInteger(reader, sourceLine); | |
| 365 | sourceColumn = decodeInteger(reader, sourceColumn); | |
| 366 | if (hasMoreVlq(reader, semi)) { | |
| 367 | namesIndex = decodeInteger(reader, namesIndex); | |
| 368 | seg = [genColumn, sourcesIndex, sourceLine, sourceColumn, namesIndex]; | |
| 369 | } else { | |
| 370 | seg = [genColumn, sourcesIndex, sourceLine, sourceColumn]; | |
| 371 | } | |
| 372 | } else { | |
| 373 | seg = [genColumn]; | |
| 374 | } | |
| 375 | line.push(seg); | |
| 376 | reader.pos++; | |
| 377 | } | |
| 378 | if (!sorted) sort(line); | |
| 379 | decoded.push(line); | |
| 380 | reader.pos = semi + 1; | |
| 381 | } while (reader.pos <= length); | |
| 382 | return decoded; | |
| 383 | } | |
| 384 | function sort(line) { | |
| 385 | line.sort(sortComparator); | |
| 386 | } | |
| 387 | function sortComparator(a, b) { | |
| 388 | return a[0] - b[0]; | |
| 389 | } | |
| 390 | function encode(decoded) { | |
| 391 | const writer = new StringWriter(); | |
| 392 | let sourcesIndex = 0; | |
| 393 | let sourceLine = 0; | |
| 394 | let sourceColumn = 0; | |
| 395 | let namesIndex = 0; | |
| 396 | for (let i = 0; i < decoded.length; i++) { | |
| 397 | const line = decoded[i]; | |
| 398 | if (i > 0) writer.write(semicolon); | |
| 399 | if (line.length === 0) continue; | |
| 400 | let genColumn = 0; | |
| 401 | for (let j = 0; j < line.length; j++) { | |
| 402 | const segment = line[j]; | |
| 403 | if (j > 0) writer.write(comma); | |
| 404 | genColumn = encodeInteger(writer, segment[0], genColumn); | |
| 405 | if (segment.length === 1) continue; | |
| 406 | sourcesIndex = encodeInteger(writer, segment[1], sourcesIndex); | |
| 407 | sourceLine = encodeInteger(writer, segment[2], sourceLine); | |
| 408 | sourceColumn = encodeInteger(writer, segment[3], sourceColumn); | |
| 409 | if (segment.length === 4) continue; | |
| 410 | namesIndex = encodeInteger(writer, segment[4], namesIndex); | |
| 411 | } | |
| 412 | } | |
| 413 | return writer.flush(); | |
| 414 | } | |
| 415 | export { | |
| 416 | decode, | |
| 417 | decodeGeneratedRanges, | |
| 418 | decodeOriginalScopes, | |
| 419 | encode, | |
| 420 | encodeGeneratedRanges, | |
| 421 | encodeOriginalScopes | |
| 422 | }; | |
| 423 | //# sourceMappingURL=sourcemap-codec.mjs.map |
| … | … | @@ -0,0 +1,6 @@ |
| 1 | { | |
| 2 | "version": 3, | |
| 3 | "sources": ["../src/vlq.ts", "../src/strings.ts", "../src/scopes.ts", "../src/sourcemap-codec.ts"], | |
| 4 | "mappings": ";AAEO,IAAM,QAAQ,IAAI,WAAW,CAAC;AAC9B,IAAM,YAAY,IAAI,WAAW,CAAC;AAEzC,IAAM,QAAQ;AACd,IAAM,YAAY,IAAI,WAAW,EAAE;AACnC,IAAM,YAAY,IAAI,WAAW,GAAG;AAEpC,SAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,QAAM,IAAI,MAAM,WAAW,CAAC;AAC5B,YAAU,CAAC,IAAI;AACf,YAAU,CAAC,IAAI;AACjB;AAEO,SAAS,cAAc,QAAsB,UAA0B;AAC5E,MAAI,QAAQ;AACZ,MAAI,QAAQ;AACZ,MAAI,UAAU;AAEd,KAAG;AACD,UAAM,IAAI,OAAO,KAAK;AACtB,cAAU,UAAU,CAAC;AACrB,cAAU,UAAU,OAAO;AAC3B,aAAS;AAAA,EACX,SAAS,UAAU;AAEnB,QAAM,eAAe,QAAQ;AAC7B,aAAW;AAEX,MAAI,cAAc;AAChB,YAAQ,cAAc,CAAC;AAAA,EACzB;AAEA,SAAO,WAAW;AACpB;AAEO,SAAS,cAAc,SAAuB,KAAa,UAA0B;AAC1F,MAAI,QAAQ,MAAM;AAElB,UAAQ,QAAQ,IAAK,CAAC,SAAS,IAAK,IAAI,SAAS;AACjD,KAAG;AACD,QAAI,UAAU,QAAQ;AACtB,eAAW;AACX,QAAI,QAAQ,EAAG,YAAW;AAC1B,YAAQ,MAAM,UAAU,OAAO,CAAC;AAAA,EAClC,SAAS,QAAQ;AAEjB,SAAO;AACT;AAEO,SAAS,WAAW,QAAsB,KAAa;AAC5D,MAAI,OAAO,OAAO,IAAK,QAAO;AAC9B,SAAO,OAAO,KAAK,MAAM;AAC3B;;;ACtDA,IAAM,YAAY,OAAO;AAGzB,IAAM,KACJ,OAAO,gBAAgB,cACH,oBAAI,YAAY,IAChC,OAAO,WAAW,cAChB;AAAA,EACE,OAAO,KAAyB;AAC9B,UAAM,MAAM,OAAO,KAAK,IAAI,QAAQ,IAAI,YAAY,IAAI,UAAU;AAClE,WAAO,IAAI,SAAS;AAAA,EACtB;AACF,IACA;AAAA,EACE,OAAO,KAAyB;AAC9B,QAAI,MAAM;AACV,aAAS,IAAI,GAAG,IAAI,IAAI,QAAQ,KAAK;AACnC,aAAO,OAAO,aAAa,IAAI,CAAC,CAAC;AAAA,IACnC;AACA,WAAO;AAAA,EACT;AACF;AAED,IAAM,eAAN,MAAmB;AAAA,EAAnB;AACL,eAAM;AACN,SAAQ,MAAM;AACd,SAAQ,SAAS,IAAI,WAAW,SAAS;AAAA;AAAA,EAEzC,MAAM,GAAiB;AACrB,UAAM,EAAE,OAAO,IAAI;AACnB,WAAO,KAAK,KAAK,IAAI;AACrB,QAAI,KAAK,QAAQ,WAAW;AAC1B,WAAK,OAAO,GAAG,OAAO,MAAM;AAC5B,WAAK,MAAM;AAAA,IACb;AAAA,EACF;AAAA,EAEA,QAAgB;AACd,UAAM,EAAE,QAAQ,KAAK,IAAI,IAAI;AAC7B,WAAO,MAAM,IAAI,MAAM,GAAG,OAAO,OAAO,SAAS,GAAG,GAAG,CAAC,IAAI;AAAA,EAC9D;AACF;AAEO,IAAM,eAAN,MAAmB;AAAA,EAIxB,YAAY,QAAgB;AAH5B,eAAM;AAIJ,SAAK,SAAS;AAAA,EAChB;AAAA,EAEA,OAAe;AACb,WAAO,KAAK,OAAO,WAAW,KAAK,KAAK;AAAA,EAC1C;AAAA,EAEA,OAAe;AACb,WAAO,KAAK,OAAO,WAAW,KAAK,GAAG;AAAA,EACxC;AAAA,EAEA,QAAQ,MAAsB;AAC5B,UAAM,EAAE,QAAQ,IAAI,IAAI;AACxB,UAAM,MAAM,OAAO,QAAQ,MAAM,GAAG;AACpC,WAAO,QAAQ,KAAK,OAAO,SAAS;AAAA,EACtC;AACF;;;AC7DA,IAAM,QAAe,CAAC;AA+Bf,SAAS,qBAAqB,OAAgC;AACnE,QAAM,EAAE,OAAO,IAAI;AACnB,QAAM,SAAS,IAAI,aAAa,KAAK;AACrC,QAAM,SAA0B,CAAC;AACjC,QAAM,QAAyB,CAAC;AAChC,MAAI,OAAO;AAEX,SAAO,OAAO,MAAM,QAAQ,OAAO,OAAO;AACxC,WAAO,cAAc,QAAQ,IAAI;AACjC,UAAM,SAAS,cAAc,QAAQ,CAAC;AAEtC,QAAI,CAAC,WAAW,QAAQ,MAAM,GAAG;AAC/B,YAAM,OAAO,MAAM,IAAI;AACvB,WAAK,CAAC,IAAI;AACV,WAAK,CAAC,IAAI;AACV;AAAA,IACF;AAEA,UAAM,OAAO,cAAc,QAAQ,CAAC;AACpC,UAAM,SAAS,cAAc,QAAQ,CAAC;AACtC,UAAM,UAAU,SAAS;AAEzB,UAAM,QACJ,UAAU,CAAC,MAAM,QAAQ,GAAG,GAAG,MAAM,cAAc,QAAQ,CAAC,CAAC,IAAI,CAAC,MAAM,QAAQ,GAAG,GAAG,IAAI;AAG5F,QAAI,OAAc;AAClB,QAAI,WAAW,QAAQ,MAAM,GAAG;AAC9B,aAAO,CAAC;AACR,SAAG;AACD,cAAM,YAAY,cAAc,QAAQ,CAAC;AACzC,aAAK,KAAK,SAAS;AAAA,MACrB,SAAS,WAAW,QAAQ,MAAM;AAAA,IACpC;AACA,UAAM,OAAO;AAEb,WAAO,KAAK,KAAK;AACjB,UAAM,KAAK,KAAK;AAAA,EAClB;AAEA,SAAO;AACT;AAEO,SAAS,qBAAqB,QAAiC;AACpE,QAAM,SAAS,IAAI,aAAa;AAEhC,WAAS,IAAI,GAAG,IAAI,OAAO,UAAU;AACnC,QAAI,sBAAsB,QAAQ,GAAG,QAAQ,CAAC,CAAC,CAAC;AAAA,EAClD;AAEA,SAAO,OAAO,MAAM;AACtB;AAEA,SAAS,sBACP,QACA,OACA,QACA,OAGQ;AACR,QAAM,QAAQ,OAAO,KAAK;AAC1B,QAAM,EAAE,GAAG,WAAW,GAAG,aAAa,GAAG,SAAS,GAAG,WAAW,GAAG,MAAM,KAAK,IAAI;AAElF,MAAI,QAAQ,EAAG,QAAO,MAAM,KAAK;AAEjC,QAAM,CAAC,IAAI,cAAc,QAAQ,WAAW,MAAM,CAAC,CAAC;AACpD,gBAAc,QAAQ,aAAa,CAAC;AACpC,gBAAc,QAAQ,MAAM,CAAC;AAE7B,QAAM,SAAS,MAAM,WAAW,IAAI,IAAS;AAC7C,gBAAc,QAAQ,QAAQ,CAAC;AAC/B,MAAI,MAAM,WAAW,EAAG,eAAc,QAAQ,MAAM,CAAC,GAAG,CAAC;AAEzD,aAAW,KAAK,MAAM;AACpB,kBAAc,QAAQ,GAAG,CAAC;AAAA,EAC5B;AAEA,OAAK,SAAS,QAAQ,OAAO,UAAU;AACrC,UAAM,OAAO,OAAO,KAAK;AACzB,UAAM,EAAE,GAAG,GAAG,GAAG,EAAE,IAAI;AACvB,QAAI,IAAI,WAAY,MAAM,WAAW,KAAK,WAAY;AACpD;AAAA,IACF;AACA,YAAQ,sBAAsB,QAAQ,OAAO,QAAQ,KAAK;AAAA,EAC5D;AAEA,SAAO,MAAM,KAAK;AAClB,QAAM,CAAC,IAAI,cAAc,QAAQ,SAAS,MAAM,CAAC,CAAC;AAClD,gBAAc,QAAQ,WAAW,CAAC;AAElC,SAAO;AACT;AAEO,SAAS,sBAAsB,OAAiC;AACrE,QAAM,EAAE,OAAO,IAAI;AACnB,QAAM,SAAS,IAAI,aAAa,KAAK;AACrC,QAAM,SAA2B,CAAC;AAClC,QAAM,QAA0B,CAAC;AAEjC,MAAI,UAAU;AACd,MAAI,yBAAyB;AAC7B,MAAI,uBAAuB;AAC3B,MAAI,uBAAuB;AAC3B,MAAI,eAAe;AACnB,MAAI,iBAAiB;AACrB,MAAI,cAAc;AAClB,MAAI,gBAAgB;AAEpB,KAAG;AACD,UAAM,OAAO,OAAO,QAAQ,GAAG;AAC/B,QAAI,YAAY;AAEhB,WAAO,OAAO,MAAM,MAAM,OAAO,OAAO;AACtC,kBAAY,cAAc,QAAQ,SAAS;AAE3C,UAAI,CAAC,WAAW,QAAQ,IAAI,GAAG;AAC7B,cAAM,OAAO,MAAM,IAAI;AACvB,aAAK,CAAC,IAAI;AACV,aAAK,CAAC,IAAI;AACV;AAAA,MACF;AAEA,YAAM,SAAS,cAAc,QAAQ,CAAC;AACtC,YAAM,gBAAgB,SAAS;AAC/B,YAAM,cAAc,SAAS;AAC7B,YAAM,WAAW,SAAS;AAE1B,UAAI,WAA4B;AAChC,UAAI,WAAsB;AAC1B,UAAI;AACJ,UAAI,eAAe;AACjB,cAAM,kBAAkB,cAAc,QAAQ,sBAAsB;AACpE,+BAAuB;AAAA,UACrB;AAAA,UACA,2BAA2B,kBAAkB,uBAAuB;AAAA,QACtE;AAEA,iCAAyB;AACzB,gBAAQ,CAAC,SAAS,WAAW,GAAG,GAAG,iBAAiB,oBAAoB;AAAA,MAC1E,OAAO;AACL,gBAAQ,CAAC,SAAS,WAAW,GAAG,CAAC;AAAA,MACnC;AAEA,YAAM,UAAU,CAAC,CAAC;AAElB,UAAI,aAAa;AACf,cAAM,UAAU;AAChB,cAAM,WAAW;AACjB,+BAAuB,cAAc,QAAQ,oBAAoB;AACjE,cAAM,aAAa,YAAY;AAC/B,uBAAe,cAAc,QAAQ,aAAa,eAAe,CAAC;AAClE,yBAAiB;AAAA,UACf;AAAA,UACA,cAAc,aAAa,eAAe,iBAAiB;AAAA,QAC7D;AAEA,mBAAW,CAAC,sBAAsB,cAAc,cAAc;AAAA,MAChE;AACA,YAAM,WAAW;AAEjB,UAAI,WAAW,QAAQ,IAAI,GAAG;AAC5B,mBAAW,CAAC;AACZ,WAAG;AACD,wBAAc;AACd,0BAAgB;AAChB,gBAAM,mBAAmB,cAAc,QAAQ,CAAC;AAChD,cAAI;AACJ,cAAI,mBAAmB,IAAI;AACzB,+BAAmB,CAAC,CAAC,cAAc,QAAQ,CAAC,CAAC,CAAC;AAC9C,qBAAS,IAAI,IAAI,IAAI,kBAAkB,KAAK;AAC1C,oBAAM,SAAS;AACf,4BAAc,cAAc,QAAQ,WAAW;AAC/C,8BAAgB,cAAc,QAAQ,gBAAgB,SAAS,gBAAgB,CAAC;AAChF,oBAAM,aAAa,cAAc,QAAQ,CAAC;AAC1C,+BAAiB,KAAK,CAAC,YAAY,aAAa,aAAa,CAAC;AAAA,YAChE;AAAA,UACF,OAAO;AACL,+BAAmB,CAAC,CAAC,gBAAgB,CAAC;AAAA,UACxC;AACA,mBAAS,KAAK,gBAAgB;AAAA,QAChC,SAAS,WAAW,QAAQ,IAAI;AAAA,MAClC;AACA,YAAM,WAAW;AAEjB,aAAO,KAAK,KAAK;AACjB,YAAM,KAAK,KAAK;AAAA,IAClB;AAEA;AACA,WAAO,MAAM,OAAO;AAAA,EACtB,SAAS,OAAO,MAAM;AAEtB,SAAO;AACT;AAEO,SAAS,sBAAsB,QAAkC;AACtE,MAAI,OAAO,WAAW,EAAG,QAAO;AAEhC,QAAM,SAAS,IAAI,aAAa;AAEhC,WAAS,IAAI,GAAG,IAAI,OAAO,UAAU;AACnC,QAAI,uBAAuB,QAAQ,GAAG,QAAQ,CAAC,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC,CAAC;AAAA,EACrE;AAEA,SAAO,OAAO,MAAM;AACtB;AAEA,SAAS,uBACP,QACA,OACA,QACA,OASQ;AACR,QAAM,QAAQ,OAAO,KAAK;AAC1B,QAAM;AAAA,IACJ,GAAG;AAAA,IACH,GAAG;AAAA,IACH,GAAG;AAAA,IACH,GAAG;AAAA,IACH;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI;AAEJ,MAAI,MAAM,CAAC,IAAI,WAAW;AACxB,gBAAY,QAAQ,MAAM,CAAC,GAAG,SAAS;AACvC,UAAM,CAAC,IAAI;AACX,UAAM,CAAC,IAAI;AAAA,EACb,WAAW,QAAQ,GAAG;AACpB,WAAO,MAAM,KAAK;AAAA,EACpB;AAEA,QAAM,CAAC,IAAI,cAAc,QAAQ,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC;AAEnD,QAAM,UACH,MAAM,WAAW,IAAI,IAAS,MAAM,WAAW,IAAS,MAAM,UAAU,IAAS;AACpF,gBAAc,QAAQ,QAAQ,CAAC;AAE/B,MAAI,MAAM,WAAW,GAAG;AACtB,UAAM,EAAE,GAAG,cAAc,GAAG,YAAY,IAAI;AAC5C,QAAI,iBAAiB,MAAM,CAAC,GAAG;AAC7B,YAAM,CAAC,IAAI;AAAA,IACb;AACA,UAAM,CAAC,IAAI,cAAc,QAAQ,cAAc,MAAM,CAAC,CAAC;AACvD,UAAM,CAAC,IAAI,cAAc,QAAQ,aAAa,MAAM,CAAC,CAAC;AAAA,EACxD;AAEA,MAAI,UAAU;AACZ,UAAM,EAAE,GAAG,cAAc,GAAG,UAAU,GAAG,WAAW,IAAI,MAAM;AAC9D,QAAI,iBAAiB,MAAM,CAAC,GAAG;AAC7B,YAAM,CAAC,IAAI;AACX,YAAM,CAAC,IAAI;AAAA,IACb,WAAW,aAAa,MAAM,CAAC,GAAG;AAChC,YAAM,CAAC,IAAI;AAAA,IACb;AACA,UAAM,CAAC,IAAI,cAAc,QAAQ,cAAc,MAAM,CAAC,CAAC;AACvD,UAAM,CAAC,IAAI,cAAc,QAAQ,UAAU,MAAM,CAAC,CAAC;AACnD,UAAM,CAAC,IAAI,cAAc,QAAQ,YAAY,MAAM,CAAC,CAAC;AAAA,EACvD;AAEA,MAAI,UAAU;AACZ,eAAW,WAAW,UAAU;AAC9B,UAAI,QAAQ,SAAS,EAAG,eAAc,QAAQ,CAAC,QAAQ,QAAQ,CAAC;AAChE,YAAM,aAAa,QAAQ,CAAC,EAAE,CAAC;AAC/B,oBAAc,QAAQ,YAAY,CAAC;AACnC,UAAI,mBAAmB;AACvB,UAAI,qBAAqB;AACzB,eAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;AACvC,cAAM,WAAW,QAAQ,CAAC;AAC1B,2BAAmB,cAAc,QAAQ,SAAS,CAAC,GAAI,gBAAgB;AACvE,6BAAqB,cAAc,QAAQ,SAAS,CAAC,GAAI,kBAAkB;AAC3E,sBAAc,QAAQ,SAAS,CAAC,GAAI,CAAC;AAAA,MACvC;AAAA,IACF;AAAA,EACF;AAEA,OAAK,SAAS,QAAQ,OAAO,UAAU;AACrC,UAAM,OAAO,OAAO,KAAK;AACzB,UAAM,EAAE,GAAG,GAAG,GAAG,EAAE,IAAI;AACvB,QAAI,IAAI,WAAY,MAAM,WAAW,KAAK,WAAY;AACpD;AAAA,IACF;AACA,YAAQ,uBAAuB,QAAQ,OAAO,QAAQ,KAAK;AAAA,EAC7D;AAEA,MAAI,MAAM,CAAC,IAAI,SAAS;AACtB,gBAAY,QAAQ,MAAM,CAAC,GAAG,OAAO;AACrC,UAAM,CAAC,IAAI;AACX,UAAM,CAAC,IAAI;AAAA,EACb,OAAO;AACL,WAAO,MAAM,KAAK;AAAA,EACpB;AACA,QAAM,CAAC,IAAI,cAAc,QAAQ,WAAW,MAAM,CAAC,CAAC;AAEpD,SAAO;AACT;AAEA,SAAS,YAAY,QAAsB,UAAkB,MAAc;AACzE,KAAG;AACD,WAAO,MAAM,SAAS;AAAA,EACxB,SAAS,EAAE,WAAW;AACxB;;;ACtUO,SAAS,OAAO,UAAqC;AAC1D,QAAM,EAAE,OAAO,IAAI;AACnB,QAAM,SAAS,IAAI,aAAa,QAAQ;AACxC,QAAM,UAA6B,CAAC;AACpC,MAAI,YAAY;AAChB,MAAI,eAAe;AACnB,MAAI,aAAa;AACjB,MAAI,eAAe;AACnB,MAAI,aAAa;AAEjB,KAAG;AACD,UAAM,OAAO,OAAO,QAAQ,GAAG;AAC/B,UAAM,OAAsB,CAAC;AAC7B,QAAI,SAAS;AACb,QAAI,UAAU;AACd,gBAAY;AAEZ,WAAO,OAAO,MAAM,MAAM;AACxB,UAAI;AAEJ,kBAAY,cAAc,QAAQ,SAAS;AAC3C,UAAI,YAAY,QAAS,UAAS;AAClC,gBAAU;AAEV,UAAI,WAAW,QAAQ,IAAI,GAAG;AAC5B,uBAAe,cAAc,QAAQ,YAAY;AACjD,qBAAa,cAAc,QAAQ,UAAU;AAC7C,uBAAe,cAAc,QAAQ,YAAY;AAEjD,YAAI,WAAW,QAAQ,IAAI,GAAG;AAC5B,uBAAa,cAAc,QAAQ,UAAU;AAC7C,gBAAM,CAAC,WAAW,cAAc,YAAY,cAAc,UAAU;AAAA,QACtE,OAAO;AACL,gBAAM,CAAC,WAAW,cAAc,YAAY,YAAY;AAAA,QAC1D;AAAA,MACF,OAAO;AACL,cAAM,CAAC,SAAS;AAAA,MAClB;AAEA,WAAK,KAAK,GAAG;AACb,aAAO;AAAA,IACT;AAEA,QAAI,CAAC,OAAQ,MAAK,IAAI;AACtB,YAAQ,KAAK,IAAI;AACjB,WAAO,MAAM,OAAO;AAAA,EACtB,SAAS,OAAO,OAAO;AAEvB,SAAO;AACT;AAEA,SAAS,KAAK,MAA0B;AACtC,OAAK,KAAK,cAAc;AAC1B;AAEA,SAAS,eAAe,GAAqB,GAA6B;AACxE,SAAO,EAAE,CAAC,IAAI,EAAE,CAAC;AACnB;AAIO,SAAS,OAAO,SAA8C;AACnE,QAAM,SAAS,IAAI,aAAa;AAChC,MAAI,eAAe;AACnB,MAAI,aAAa;AACjB,MAAI,eAAe;AACnB,MAAI,aAAa;AAEjB,WAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;AACvC,UAAM,OAAO,QAAQ,CAAC;AACtB,QAAI,IAAI,EAAG,QAAO,MAAM,SAAS;AACjC,QAAI,KAAK,WAAW,EAAG;AAEvB,QAAI,YAAY;AAEhB,aAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,YAAM,UAAU,KAAK,CAAC;AACtB,UAAI,IAAI,EAAG,QAAO,MAAM,KAAK;AAE7B,kBAAY,cAAc,QAAQ,QAAQ,CAAC,GAAG,SAAS;AAEvD,UAAI,QAAQ,WAAW,EAAG;AAC1B,qBAAe,cAAc,QAAQ,QAAQ,CAAC,GAAG,YAAY;AAC7D,mBAAa,cAAc,QAAQ,QAAQ,CAAC,GAAG,UAAU;AACzD,qBAAe,cAAc,QAAQ,QAAQ,CAAC,GAAG,YAAY;AAE7D,UAAI,QAAQ,WAAW,EAAG;AAC1B,mBAAa,cAAc,QAAQ,QAAQ,CAAC,GAAG,UAAU;AAAA,IAC3D;AAAA,EACF;AAEA,SAAO,OAAO,MAAM;AACtB;", | |
| 5 | "names": [] | |
| 6 | } |
| … | … | @@ -0,0 +1,464 @@ |
| 1 | (function (global, factory) { | |
| 2 | if (typeof exports === 'object' && typeof module !== 'undefined') { | |
| 3 | factory(module); | |
| 4 | module.exports = def(module); | |
| 5 | } else if (typeof define === 'function' && define.amd) { | |
| 6 | define(['module'], function(mod) { | |
| 7 | factory.apply(this, arguments); | |
| 8 | mod.exports = def(mod); | |
| 9 | }); | |
| 10 | } else { | |
| 11 | const mod = { exports: {} }; | |
| 12 | factory(mod); | |
| 13 | global = typeof globalThis !== 'undefined' ? globalThis : global || self; | |
| 14 | global.sourcemapCodec = def(mod); | |
| 15 | } | |
| 16 | function def(m) { return 'default' in m.exports ? m.exports.default : m.exports; } | |
| 17 | })(this, (function (module) { | |
| 18 | "use strict"; | |
| 19 | var __defProp = Object.defineProperty; | |
| 20 | var __getOwnPropDesc = Object.getOwnPropertyDescriptor; | |
| 21 | var __getOwnPropNames = Object.getOwnPropertyNames; | |
| 22 | var __hasOwnProp = Object.prototype.hasOwnProperty; | |
| 23 | var __export = (target, all) => { | |
| 24 | for (var name in all) | |
| 25 | __defProp(target, name, { get: all[name], enumerable: true }); | |
| 26 | }; | |
| 27 | var __copyProps = (to, from, except, desc) => { | |
| 28 | if (from && typeof from === "object" || typeof from === "function") { | |
| 29 | for (let key of __getOwnPropNames(from)) | |
| 30 | if (!__hasOwnProp.call(to, key) && key !== except) | |
| 31 | __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); | |
| 32 | } | |
| 33 | return to; | |
| 34 | }; | |
| 35 | var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); | |
| 36 | ||
| 37 | // src/sourcemap-codec.ts | |
| 38 | var sourcemap_codec_exports = {}; | |
| 39 | __export(sourcemap_codec_exports, { | |
| 40 | decode: () => decode, | |
| 41 | decodeGeneratedRanges: () => decodeGeneratedRanges, | |
| 42 | decodeOriginalScopes: () => decodeOriginalScopes, | |
| 43 | encode: () => encode, | |
| 44 | encodeGeneratedRanges: () => encodeGeneratedRanges, | |
| 45 | encodeOriginalScopes: () => encodeOriginalScopes | |
| 46 | }); | |
| 47 | module.exports = __toCommonJS(sourcemap_codec_exports); | |
| 48 | ||
| 49 | // src/vlq.ts | |
| 50 | var comma = ",".charCodeAt(0); | |
| 51 | var semicolon = ";".charCodeAt(0); | |
| 52 | var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; | |
| 53 | var intToChar = new Uint8Array(64); | |
| 54 | var charToInt = new Uint8Array(128); | |
| 55 | for (let i = 0; i < chars.length; i++) { | |
| 56 | const c = chars.charCodeAt(i); | |
| 57 | intToChar[i] = c; | |
| 58 | charToInt[c] = i; | |
| 59 | } | |
| 60 | function decodeInteger(reader, relative) { | |
| 61 | let value = 0; | |
| 62 | let shift = 0; | |
| 63 | let integer = 0; | |
| 64 | do { | |
| 65 | const c = reader.next(); | |
| 66 | integer = charToInt[c]; | |
| 67 | value |= (integer & 31) << shift; | |
| 68 | shift += 5; | |
| 69 | } while (integer & 32); | |
| 70 | const shouldNegate = value & 1; | |
| 71 | value >>>= 1; | |
| 72 | if (shouldNegate) { | |
| 73 | value = -2147483648 | -value; | |
| 74 | } | |
| 75 | return relative + value; | |
| 76 | } | |
| 77 | function encodeInteger(builder, num, relative) { | |
| 78 | let delta = num - relative; | |
| 79 | delta = delta < 0 ? -delta << 1 | 1 : delta << 1; | |
| 80 | do { | |
| 81 | let clamped = delta & 31; | |
| 82 | delta >>>= 5; | |
| 83 | if (delta > 0) clamped |= 32; | |
| 84 | builder.write(intToChar[clamped]); | |
| 85 | } while (delta > 0); | |
| 86 | return num; | |
| 87 | } | |
| 88 | function hasMoreVlq(reader, max) { | |
| 89 | if (reader.pos >= max) return false; | |
| 90 | return reader.peek() !== comma; | |
| 91 | } | |
| 92 | ||
| 93 | // src/strings.ts | |
| 94 | var bufLength = 1024 * 16; | |
| 95 | var td = typeof TextDecoder !== "undefined" ? /* @__PURE__ */ new TextDecoder() : typeof Buffer !== "undefined" ? { | |
| 96 | decode(buf) { | |
| 97 | const out = Buffer.from(buf.buffer, buf.byteOffset, buf.byteLength); | |
| 98 | return out.toString(); | |
| 99 | } | |
| 100 | } : { | |
| 101 | decode(buf) { | |
| 102 | let out = ""; | |
| 103 | for (let i = 0; i < buf.length; i++) { | |
| 104 | out += String.fromCharCode(buf[i]); | |
| 105 | } | |
| 106 | return out; | |
| 107 | } | |
| 108 | }; | |
| 109 | var StringWriter = class { | |
| 110 | constructor() { | |
| 111 | this.pos = 0; | |
| 112 | this.out = ""; | |
| 113 | this.buffer = new Uint8Array(bufLength); | |
| 114 | } | |
| 115 | write(v) { | |
| 116 | const { buffer } = this; | |
| 117 | buffer[this.pos++] = v; | |
| 118 | if (this.pos === bufLength) { | |
| 119 | this.out += td.decode(buffer); | |
| 120 | this.pos = 0; | |
| 121 | } | |
| 122 | } | |
| 123 | flush() { | |
| 124 | const { buffer, out, pos } = this; | |
| 125 | return pos > 0 ? out + td.decode(buffer.subarray(0, pos)) : out; | |
| 126 | } | |
| 127 | }; | |
| 128 | var StringReader = class { | |
| 129 | constructor(buffer) { | |
| 130 | this.pos = 0; | |
| 131 | this.buffer = buffer; | |
| 132 | } | |
| 133 | next() { | |
| 134 | return this.buffer.charCodeAt(this.pos++); | |
| 135 | } | |
| 136 | peek() { | |
| 137 | return this.buffer.charCodeAt(this.pos); | |
| 138 | } | |
| 139 | indexOf(char) { | |
| 140 | const { buffer, pos } = this; | |
| 141 | const idx = buffer.indexOf(char, pos); | |
| 142 | return idx === -1 ? buffer.length : idx; | |
| 143 | } | |
| 144 | }; | |
| 145 | ||
| 146 | // src/scopes.ts | |
| 147 | var EMPTY = []; | |
| 148 | function decodeOriginalScopes(input) { | |
| 149 | const { length } = input; | |
| 150 | const reader = new StringReader(input); | |
| 151 | const scopes = []; | |
| 152 | const stack = []; | |
| 153 | let line = 0; | |
| 154 | for (; reader.pos < length; reader.pos++) { | |
| 155 | line = decodeInteger(reader, line); | |
| 156 | const column = decodeInteger(reader, 0); | |
| 157 | if (!hasMoreVlq(reader, length)) { | |
| 158 | const last = stack.pop(); | |
| 159 | last[2] = line; | |
| 160 | last[3] = column; | |
| 161 | continue; | |
| 162 | } | |
| 163 | const kind = decodeInteger(reader, 0); | |
| 164 | const fields = decodeInteger(reader, 0); | |
| 165 | const hasName = fields & 1; | |
| 166 | const scope = hasName ? [line, column, 0, 0, kind, decodeInteger(reader, 0)] : [line, column, 0, 0, kind]; | |
| 167 | let vars = EMPTY; | |
| 168 | if (hasMoreVlq(reader, length)) { | |
| 169 | vars = []; | |
| 170 | do { | |
| 171 | const varsIndex = decodeInteger(reader, 0); | |
| 172 | vars.push(varsIndex); | |
| 173 | } while (hasMoreVlq(reader, length)); | |
| 174 | } | |
| 175 | scope.vars = vars; | |
| 176 | scopes.push(scope); | |
| 177 | stack.push(scope); | |
| 178 | } | |
| 179 | return scopes; | |
| 180 | } | |
| 181 | function encodeOriginalScopes(scopes) { | |
| 182 | const writer = new StringWriter(); | |
| 183 | for (let i = 0; i < scopes.length; ) { | |
| 184 | i = _encodeOriginalScopes(scopes, i, writer, [0]); | |
| 185 | } | |
| 186 | return writer.flush(); | |
| 187 | } | |
| 188 | function _encodeOriginalScopes(scopes, index, writer, state) { | |
| 189 | const scope = scopes[index]; | |
| 190 | const { 0: startLine, 1: startColumn, 2: endLine, 3: endColumn, 4: kind, vars } = scope; | |
| 191 | if (index > 0) writer.write(comma); | |
| 192 | state[0] = encodeInteger(writer, startLine, state[0]); | |
| 193 | encodeInteger(writer, startColumn, 0); | |
| 194 | encodeInteger(writer, kind, 0); | |
| 195 | const fields = scope.length === 6 ? 1 : 0; | |
| 196 | encodeInteger(writer, fields, 0); | |
| 197 | if (scope.length === 6) encodeInteger(writer, scope[5], 0); | |
| 198 | for (const v of vars) { | |
| 199 | encodeInteger(writer, v, 0); | |
| 200 | } | |
| 201 | for (index++; index < scopes.length; ) { | |
| 202 | const next = scopes[index]; | |
| 203 | const { 0: l, 1: c } = next; | |
| 204 | if (l > endLine || l === endLine && c >= endColumn) { | |
| 205 | break; | |
| 206 | } | |
| 207 | index = _encodeOriginalScopes(scopes, index, writer, state); | |
| 208 | } | |
| 209 | writer.write(comma); | |
| 210 | state[0] = encodeInteger(writer, endLine, state[0]); | |
| 211 | encodeInteger(writer, endColumn, 0); | |
| 212 | return index; | |
| 213 | } | |
| 214 | function decodeGeneratedRanges(input) { | |
| 215 | const { length } = input; | |
| 216 | const reader = new StringReader(input); | |
| 217 | const ranges = []; | |
| 218 | const stack = []; | |
| 219 | let genLine = 0; | |
| 220 | let definitionSourcesIndex = 0; | |
| 221 | let definitionScopeIndex = 0; | |
| 222 | let callsiteSourcesIndex = 0; | |
| 223 | let callsiteLine = 0; | |
| 224 | let callsiteColumn = 0; | |
| 225 | let bindingLine = 0; | |
| 226 | let bindingColumn = 0; | |
| 227 | do { | |
| 228 | const semi = reader.indexOf(";"); | |
| 229 | let genColumn = 0; | |
| 230 | for (; reader.pos < semi; reader.pos++) { | |
| 231 | genColumn = decodeInteger(reader, genColumn); | |
| 232 | if (!hasMoreVlq(reader, semi)) { | |
| 233 | const last = stack.pop(); | |
| 234 | last[2] = genLine; | |
| 235 | last[3] = genColumn; | |
| 236 | continue; | |
| 237 | } | |
| 238 | const fields = decodeInteger(reader, 0); | |
| 239 | const hasDefinition = fields & 1; | |
| 240 | const hasCallsite = fields & 2; | |
| 241 | const hasScope = fields & 4; | |
| 242 | let callsite = null; | |
| 243 | let bindings = EMPTY; | |
| 244 | let range; | |
| 245 | if (hasDefinition) { | |
| 246 | const defSourcesIndex = decodeInteger(reader, definitionSourcesIndex); | |
| 247 | definitionScopeIndex = decodeInteger( | |
| 248 | reader, | |
| 249 | definitionSourcesIndex === defSourcesIndex ? definitionScopeIndex : 0 | |
| 250 | ); | |
| 251 | definitionSourcesIndex = defSourcesIndex; | |
| 252 | range = [genLine, genColumn, 0, 0, defSourcesIndex, definitionScopeIndex]; | |
| 253 | } else { | |
| 254 | range = [genLine, genColumn, 0, 0]; | |
| 255 | } | |
| 256 | range.isScope = !!hasScope; | |
| 257 | if (hasCallsite) { | |
| 258 | const prevCsi = callsiteSourcesIndex; | |
| 259 | const prevLine = callsiteLine; | |
| 260 | callsiteSourcesIndex = decodeInteger(reader, callsiteSourcesIndex); | |
| 261 | const sameSource = prevCsi === callsiteSourcesIndex; | |
| 262 | callsiteLine = decodeInteger(reader, sameSource ? callsiteLine : 0); | |
| 263 | callsiteColumn = decodeInteger( | |
| 264 | reader, | |
| 265 | sameSource && prevLine === callsiteLine ? callsiteColumn : 0 | |
| 266 | ); | |
| 267 | callsite = [callsiteSourcesIndex, callsiteLine, callsiteColumn]; | |
| 268 | } | |
| 269 | range.callsite = callsite; | |
| 270 | if (hasMoreVlq(reader, semi)) { | |
| 271 | bindings = []; | |
| 272 | do { | |
| 273 | bindingLine = genLine; | |
| 274 | bindingColumn = genColumn; | |
| 275 | const expressionsCount = decodeInteger(reader, 0); | |
| 276 | let expressionRanges; | |
| 277 | if (expressionsCount < -1) { | |
| 278 | expressionRanges = [[decodeInteger(reader, 0)]]; | |
| 279 | for (let i = -1; i > expressionsCount; i--) { | |
| 280 | const prevBl = bindingLine; | |
| 281 | bindingLine = decodeInteger(reader, bindingLine); | |
| 282 | bindingColumn = decodeInteger(reader, bindingLine === prevBl ? bindingColumn : 0); | |
| 283 | const expression = decodeInteger(reader, 0); | |
| 284 | expressionRanges.push([expression, bindingLine, bindingColumn]); | |
| 285 | } | |
| 286 | } else { | |
| 287 | expressionRanges = [[expressionsCount]]; | |
| 288 | } | |
| 289 | bindings.push(expressionRanges); | |
| 290 | } while (hasMoreVlq(reader, semi)); | |
| 291 | } | |
| 292 | range.bindings = bindings; | |
| 293 | ranges.push(range); | |
| 294 | stack.push(range); | |
| 295 | } | |
| 296 | genLine++; | |
| 297 | reader.pos = semi + 1; | |
| 298 | } while (reader.pos < length); | |
| 299 | return ranges; | |
| 300 | } | |
| 301 | function encodeGeneratedRanges(ranges) { | |
| 302 | if (ranges.length === 0) return ""; | |
| 303 | const writer = new StringWriter(); | |
| 304 | for (let i = 0; i < ranges.length; ) { | |
| 305 | i = _encodeGeneratedRanges(ranges, i, writer, [0, 0, 0, 0, 0, 0, 0]); | |
| 306 | } | |
| 307 | return writer.flush(); | |
| 308 | } | |
| 309 | function _encodeGeneratedRanges(ranges, index, writer, state) { | |
| 310 | const range = ranges[index]; | |
| 311 | const { | |
| 312 | 0: startLine, | |
| 313 | 1: startColumn, | |
| 314 | 2: endLine, | |
| 315 | 3: endColumn, | |
| 316 | isScope, | |
| 317 | callsite, | |
| 318 | bindings | |
| 319 | } = range; | |
| 320 | if (state[0] < startLine) { | |
| 321 | catchupLine(writer, state[0], startLine); | |
| 322 | state[0] = startLine; | |
| 323 | state[1] = 0; | |
| 324 | } else if (index > 0) { | |
| 325 | writer.write(comma); | |
| 326 | } | |
| 327 | state[1] = encodeInteger(writer, range[1], state[1]); | |
| 328 | const fields = (range.length === 6 ? 1 : 0) | (callsite ? 2 : 0) | (isScope ? 4 : 0); | |
| 329 | encodeInteger(writer, fields, 0); | |
| 330 | if (range.length === 6) { | |
| 331 | const { 4: sourcesIndex, 5: scopesIndex } = range; | |
| 332 | if (sourcesIndex !== state[2]) { | |
| 333 | state[3] = 0; | |
| 334 | } | |
| 335 | state[2] = encodeInteger(writer, sourcesIndex, state[2]); | |
| 336 | state[3] = encodeInteger(writer, scopesIndex, state[3]); | |
| 337 | } | |
| 338 | if (callsite) { | |
| 339 | const { 0: sourcesIndex, 1: callLine, 2: callColumn } = range.callsite; | |
| 340 | if (sourcesIndex !== state[4]) { | |
| 341 | state[5] = 0; | |
| 342 | state[6] = 0; | |
| 343 | } else if (callLine !== state[5]) { | |
| 344 | state[6] = 0; | |
| 345 | } | |
| 346 | state[4] = encodeInteger(writer, sourcesIndex, state[4]); | |
| 347 | state[5] = encodeInteger(writer, callLine, state[5]); | |
| 348 | state[6] = encodeInteger(writer, callColumn, state[6]); | |
| 349 | } | |
| 350 | if (bindings) { | |
| 351 | for (const binding of bindings) { | |
| 352 | if (binding.length > 1) encodeInteger(writer, -binding.length, 0); | |
| 353 | const expression = binding[0][0]; | |
| 354 | encodeInteger(writer, expression, 0); | |
| 355 | let bindingStartLine = startLine; | |
| 356 | let bindingStartColumn = startColumn; | |
| 357 | for (let i = 1; i < binding.length; i++) { | |
| 358 | const expRange = binding[i]; | |
| 359 | bindingStartLine = encodeInteger(writer, expRange[1], bindingStartLine); | |
| 360 | bindingStartColumn = encodeInteger(writer, expRange[2], bindingStartColumn); | |
| 361 | encodeInteger(writer, expRange[0], 0); | |
| 362 | } | |
| 363 | } | |
| 364 | } | |
| 365 | for (index++; index < ranges.length; ) { | |
| 366 | const next = ranges[index]; | |
| 367 | const { 0: l, 1: c } = next; | |
| 368 | if (l > endLine || l === endLine && c >= endColumn) { | |
| 369 | break; | |
| 370 | } | |
| 371 | index = _encodeGeneratedRanges(ranges, index, writer, state); | |
| 372 | } | |
| 373 | if (state[0] < endLine) { | |
| 374 | catchupLine(writer, state[0], endLine); | |
| 375 | state[0] = endLine; | |
| 376 | state[1] = 0; | |
| 377 | } else { | |
| 378 | writer.write(comma); | |
| 379 | } | |
| 380 | state[1] = encodeInteger(writer, endColumn, state[1]); | |
| 381 | return index; | |
| 382 | } | |
| 383 | function catchupLine(writer, lastLine, line) { | |
| 384 | do { | |
| 385 | writer.write(semicolon); | |
| 386 | } while (++lastLine < line); | |
| 387 | } | |
| 388 | ||
| 389 | // src/sourcemap-codec.ts | |
| 390 | function decode(mappings) { | |
| 391 | const { length } = mappings; | |
| 392 | const reader = new StringReader(mappings); | |
| 393 | const decoded = []; | |
| 394 | let genColumn = 0; | |
| 395 | let sourcesIndex = 0; | |
| 396 | let sourceLine = 0; | |
| 397 | let sourceColumn = 0; | |
| 398 | let namesIndex = 0; | |
| 399 | do { | |
| 400 | const semi = reader.indexOf(";"); | |
| 401 | const line = []; | |
| 402 | let sorted = true; | |
| 403 | let lastCol = 0; | |
| 404 | genColumn = 0; | |
| 405 | while (reader.pos < semi) { | |
| 406 | let seg; | |
| 407 | genColumn = decodeInteger(reader, genColumn); | |
| 408 | if (genColumn < lastCol) sorted = false; | |
| 409 | lastCol = genColumn; | |
| 410 | if (hasMoreVlq(reader, semi)) { | |
| 411 | sourcesIndex = decodeInteger(reader, sourcesIndex); | |
| 412 | sourceLine = decodeInteger(reader, sourceLine); | |
| 413 | sourceColumn = decodeInteger(reader, sourceColumn); | |
| 414 | if (hasMoreVlq(reader, semi)) { | |
| 415 | namesIndex = decodeInteger(reader, namesIndex); | |
| 416 | seg = [genColumn, sourcesIndex, sourceLine, sourceColumn, namesIndex]; | |
| 417 | } else { | |
| 418 | seg = [genColumn, sourcesIndex, sourceLine, sourceColumn]; | |
| 419 | } | |
| 420 | } else { | |
| 421 | seg = [genColumn]; | |
| 422 | } | |
| 423 | line.push(seg); | |
| 424 | reader.pos++; | |
| 425 | } | |
| 426 | if (!sorted) sort(line); | |
| 427 | decoded.push(line); | |
| 428 | reader.pos = semi + 1; | |
| 429 | } while (reader.pos <= length); | |
| 430 | return decoded; | |
| 431 | } | |
| 432 | function sort(line) { | |
| 433 | line.sort(sortComparator); | |
| 434 | } | |
| 435 | function sortComparator(a, b) { | |
| 436 | return a[0] - b[0]; | |
| 437 | } | |
| 438 | function encode(decoded) { | |
| 439 | const writer = new StringWriter(); | |
| 440 | let sourcesIndex = 0; | |
| 441 | let sourceLine = 0; | |
| 442 | let sourceColumn = 0; | |
| 443 | let namesIndex = 0; | |
| 444 | for (let i = 0; i < decoded.length; i++) { | |
| 445 | const line = decoded[i]; | |
| 446 | if (i > 0) writer.write(semicolon); | |
| 447 | if (line.length === 0) continue; | |
| 448 | let genColumn = 0; | |
| 449 | for (let j = 0; j < line.length; j++) { | |
| 450 | const segment = line[j]; | |
| 451 | if (j > 0) writer.write(comma); | |
| 452 | genColumn = encodeInteger(writer, segment[0], genColumn); | |
| 453 | if (segment.length === 1) continue; | |
| 454 | sourcesIndex = encodeInteger(writer, segment[1], sourcesIndex); | |
| 455 | sourceLine = encodeInteger(writer, segment[2], sourceLine); | |
| 456 | sourceColumn = encodeInteger(writer, segment[3], sourceColumn); | |
| 457 | if (segment.length === 4) continue; | |
| 458 | namesIndex = encodeInteger(writer, segment[4], namesIndex); | |
| 459 | } | |
| 460 | } | |
| 461 | return writer.flush(); | |
| 462 | } | |
| 463 | })); | |
| 464 | //# sourceMappingURL=sourcemap-codec.umd.js.map |
| … | … | @@ -0,0 +1,6 @@ |
| 1 | { | |
| 2 | "version": 3, | |
| 3 | "sources": ["../src/sourcemap-codec.ts", "../src/vlq.ts", "../src/strings.ts", "../src/scopes.ts"], | |
| 4 | "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACEO,IAAM,QAAQ,IAAI,WAAW,CAAC;AAC9B,IAAM,YAAY,IAAI,WAAW,CAAC;AAEzC,IAAM,QAAQ;AACd,IAAM,YAAY,IAAI,WAAW,EAAE;AACnC,IAAM,YAAY,IAAI,WAAW,GAAG;AAEpC,SAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,QAAM,IAAI,MAAM,WAAW,CAAC;AAC5B,YAAU,CAAC,IAAI;AACf,YAAU,CAAC,IAAI;AACjB;AAEO,SAAS,cAAc,QAAsB,UAA0B;AAC5E,MAAI,QAAQ;AACZ,MAAI,QAAQ;AACZ,MAAI,UAAU;AAEd,KAAG;AACD,UAAM,IAAI,OAAO,KAAK;AACtB,cAAU,UAAU,CAAC;AACrB,cAAU,UAAU,OAAO;AAC3B,aAAS;AAAA,EACX,SAAS,UAAU;AAEnB,QAAM,eAAe,QAAQ;AAC7B,aAAW;AAEX,MAAI,cAAc;AAChB,YAAQ,cAAc,CAAC;AAAA,EACzB;AAEA,SAAO,WAAW;AACpB;AAEO,SAAS,cAAc,SAAuB,KAAa,UAA0B;AAC1F,MAAI,QAAQ,MAAM;AAElB,UAAQ,QAAQ,IAAK,CAAC,SAAS,IAAK,IAAI,SAAS;AACjD,KAAG;AACD,QAAI,UAAU,QAAQ;AACtB,eAAW;AACX,QAAI,QAAQ,EAAG,YAAW;AAC1B,YAAQ,MAAM,UAAU,OAAO,CAAC;AAAA,EAClC,SAAS,QAAQ;AAEjB,SAAO;AACT;AAEO,SAAS,WAAW,QAAsB,KAAa;AAC5D,MAAI,OAAO,OAAO,IAAK,QAAO;AAC9B,SAAO,OAAO,KAAK,MAAM;AAC3B;;;ACtDA,IAAM,YAAY,OAAO;AAGzB,IAAM,KACJ,OAAO,gBAAgB,cACH,oBAAI,YAAY,IAChC,OAAO,WAAW,cAChB;AAAA,EACE,OAAO,KAAyB;AAC9B,UAAM,MAAM,OAAO,KAAK,IAAI,QAAQ,IAAI,YAAY,IAAI,UAAU;AAClE,WAAO,IAAI,SAAS;AAAA,EACtB;AACF,IACA;AAAA,EACE,OAAO,KAAyB;AAC9B,QAAI,MAAM;AACV,aAAS,IAAI,GAAG,IAAI,IAAI,QAAQ,KAAK;AACnC,aAAO,OAAO,aAAa,IAAI,CAAC,CAAC;AAAA,IACnC;AACA,WAAO;AAAA,EACT;AACF;AAED,IAAM,eAAN,MAAmB;AAAA,EAAnB;AACL,eAAM;AACN,SAAQ,MAAM;AACd,SAAQ,SAAS,IAAI,WAAW,SAAS;AAAA;AAAA,EAEzC,MAAM,GAAiB;AACrB,UAAM,EAAE,OAAO,IAAI;AACnB,WAAO,KAAK,KAAK,IAAI;AACrB,QAAI,KAAK,QAAQ,WAAW;AAC1B,WAAK,OAAO,GAAG,OAAO,MAAM;AAC5B,WAAK,MAAM;AAAA,IACb;AAAA,EACF;AAAA,EAEA,QAAgB;AACd,UAAM,EAAE,QAAQ,KAAK,IAAI,IAAI;AAC7B,WAAO,MAAM,IAAI,MAAM,GAAG,OAAO,OAAO,SAAS,GAAG,GAAG,CAAC,IAAI;AAAA,EAC9D;AACF;AAEO,IAAM,eAAN,MAAmB;AAAA,EAIxB,YAAY,QAAgB;AAH5B,eAAM;AAIJ,SAAK,SAAS;AAAA,EAChB;AAAA,EAEA,OAAe;AACb,WAAO,KAAK,OAAO,WAAW,KAAK,KAAK;AAAA,EAC1C;AAAA,EAEA,OAAe;AACb,WAAO,KAAK,OAAO,WAAW,KAAK,GAAG;AAAA,EACxC;AAAA,EAEA,QAAQ,MAAsB;AAC5B,UAAM,EAAE,QAAQ,IAAI,IAAI;AACxB,UAAM,MAAM,OAAO,QAAQ,MAAM,GAAG;AACpC,WAAO,QAAQ,KAAK,OAAO,SAAS;AAAA,EACtC;AACF;;;AC7DA,IAAM,QAAe,CAAC;AA+Bf,SAAS,qBAAqB,OAAgC;AACnE,QAAM,EAAE,OAAO,IAAI;AACnB,QAAM,SAAS,IAAI,aAAa,KAAK;AACrC,QAAM,SAA0B,CAAC;AACjC,QAAM,QAAyB,CAAC;AAChC,MAAI,OAAO;AAEX,SAAO,OAAO,MAAM,QAAQ,OAAO,OAAO;AACxC,WAAO,cAAc,QAAQ,IAAI;AACjC,UAAM,SAAS,cAAc,QAAQ,CAAC;AAEtC,QAAI,CAAC,WAAW,QAAQ,MAAM,GAAG;AAC/B,YAAM,OAAO,MAAM,IAAI;AACvB,WAAK,CAAC,IAAI;AACV,WAAK,CAAC,IAAI;AACV;AAAA,IACF;AAEA,UAAM,OAAO,cAAc,QAAQ,CAAC;AACpC,UAAM,SAAS,cAAc,QAAQ,CAAC;AACtC,UAAM,UAAU,SAAS;AAEzB,UAAM,QACJ,UAAU,CAAC,MAAM,QAAQ,GAAG,GAAG,MAAM,cAAc,QAAQ,CAAC,CAAC,IAAI,CAAC,MAAM,QAAQ,GAAG,GAAG,IAAI;AAG5F,QAAI,OAAc;AAClB,QAAI,WAAW,QAAQ,MAAM,GAAG;AAC9B,aAAO,CAAC;AACR,SAAG;AACD,cAAM,YAAY,cAAc,QAAQ,CAAC;AACzC,aAAK,KAAK,SAAS;AAAA,MACrB,SAAS,WAAW,QAAQ,MAAM;AAAA,IACpC;AACA,UAAM,OAAO;AAEb,WAAO,KAAK,KAAK;AACjB,UAAM,KAAK,KAAK;AAAA,EAClB;AAEA,SAAO;AACT;AAEO,SAAS,qBAAqB,QAAiC;AACpE,QAAM,SAAS,IAAI,aAAa;AAEhC,WAAS,IAAI,GAAG,IAAI,OAAO,UAAU;AACnC,QAAI,sBAAsB,QAAQ,GAAG,QAAQ,CAAC,CAAC,CAAC;AAAA,EAClD;AAEA,SAAO,OAAO,MAAM;AACtB;AAEA,SAAS,sBACP,QACA,OACA,QACA,OAGQ;AACR,QAAM,QAAQ,OAAO,KAAK;AAC1B,QAAM,EAAE,GAAG,WAAW,GAAG,aAAa,GAAG,SAAS,GAAG,WAAW,GAAG,MAAM,KAAK,IAAI;AAElF,MAAI,QAAQ,EAAG,QAAO,MAAM,KAAK;AAEjC,QAAM,CAAC,IAAI,cAAc,QAAQ,WAAW,MAAM,CAAC,CAAC;AACpD,gBAAc,QAAQ,aAAa,CAAC;AACpC,gBAAc,QAAQ,MAAM,CAAC;AAE7B,QAAM,SAAS,MAAM,WAAW,IAAI,IAAS;AAC7C,gBAAc,QAAQ,QAAQ,CAAC;AAC/B,MAAI,MAAM,WAAW,EAAG,eAAc,QAAQ,MAAM,CAAC,GAAG,CAAC;AAEzD,aAAW,KAAK,MAAM;AACpB,kBAAc,QAAQ,GAAG,CAAC;AAAA,EAC5B;AAEA,OAAK,SAAS,QAAQ,OAAO,UAAU;AACrC,UAAM,OAAO,OAAO,KAAK;AACzB,UAAM,EAAE,GAAG,GAAG,GAAG,EAAE,IAAI;AACvB,QAAI,IAAI,WAAY,MAAM,WAAW,KAAK,WAAY;AACpD;AAAA,IACF;AACA,YAAQ,sBAAsB,QAAQ,OAAO,QAAQ,KAAK;AAAA,EAC5D;AAEA,SAAO,MAAM,KAAK;AAClB,QAAM,CAAC,IAAI,cAAc,QAAQ,SAAS,MAAM,CAAC,CAAC;AAClD,gBAAc,QAAQ,WAAW,CAAC;AAElC,SAAO;AACT;AAEO,SAAS,sBAAsB,OAAiC;AACrE,QAAM,EAAE,OAAO,IAAI;AACnB,QAAM,SAAS,IAAI,aAAa,KAAK;AACrC,QAAM,SAA2B,CAAC;AAClC,QAAM,QAA0B,CAAC;AAEjC,MAAI,UAAU;AACd,MAAI,yBAAyB;AAC7B,MAAI,uBAAuB;AAC3B,MAAI,uBAAuB;AAC3B,MAAI,eAAe;AACnB,MAAI,iBAAiB;AACrB,MAAI,cAAc;AAClB,MAAI,gBAAgB;AAEpB,KAAG;AACD,UAAM,OAAO,OAAO,QAAQ,GAAG;AAC/B,QAAI,YAAY;AAEhB,WAAO,OAAO,MAAM,MAAM,OAAO,OAAO;AACtC,kBAAY,cAAc,QAAQ,SAAS;AAE3C,UAAI,CAAC,WAAW,QAAQ,IAAI,GAAG;AAC7B,cAAM,OAAO,MAAM,IAAI;AACvB,aAAK,CAAC,IAAI;AACV,aAAK,CAAC,IAAI;AACV;AAAA,MACF;AAEA,YAAM,SAAS,cAAc,QAAQ,CAAC;AACtC,YAAM,gBAAgB,SAAS;AAC/B,YAAM,cAAc,SAAS;AAC7B,YAAM,WAAW,SAAS;AAE1B,UAAI,WAA4B;AAChC,UAAI,WAAsB;AAC1B,UAAI;AACJ,UAAI,eAAe;AACjB,cAAM,kBAAkB,cAAc,QAAQ,sBAAsB;AACpE,+BAAuB;AAAA,UACrB;AAAA,UACA,2BAA2B,kBAAkB,uBAAuB;AAAA,QACtE;AAEA,iCAAyB;AACzB,gBAAQ,CAAC,SAAS,WAAW,GAAG,GAAG,iBAAiB,oBAAoB;AAAA,MAC1E,OAAO;AACL,gBAAQ,CAAC,SAAS,WAAW,GAAG,CAAC;AAAA,MACnC;AAEA,YAAM,UAAU,CAAC,CAAC;AAElB,UAAI,aAAa;AACf,cAAM,UAAU;AAChB,cAAM,WAAW;AACjB,+BAAuB,cAAc,QAAQ,oBAAoB;AACjE,cAAM,aAAa,YAAY;AAC/B,uBAAe,cAAc,QAAQ,aAAa,eAAe,CAAC;AAClE,yBAAiB;AAAA,UACf;AAAA,UACA,cAAc,aAAa,eAAe,iBAAiB;AAAA,QAC7D;AAEA,mBAAW,CAAC,sBAAsB,cAAc,cAAc;AAAA,MAChE;AACA,YAAM,WAAW;AAEjB,UAAI,WAAW,QAAQ,IAAI,GAAG;AAC5B,mBAAW,CAAC;AACZ,WAAG;AACD,wBAAc;AACd,0BAAgB;AAChB,gBAAM,mBAAmB,cAAc,QAAQ,CAAC;AAChD,cAAI;AACJ,cAAI,mBAAmB,IAAI;AACzB,+BAAmB,CAAC,CAAC,cAAc,QAAQ,CAAC,CAAC,CAAC;AAC9C,qBAAS,IAAI,IAAI,IAAI,kBAAkB,KAAK;AAC1C,oBAAM,SAAS;AACf,4BAAc,cAAc,QAAQ,WAAW;AAC/C,8BAAgB,cAAc,QAAQ,gBAAgB,SAAS,gBAAgB,CAAC;AAChF,oBAAM,aAAa,cAAc,QAAQ,CAAC;AAC1C,+BAAiB,KAAK,CAAC,YAAY,aAAa,aAAa,CAAC;AAAA,YAChE;AAAA,UACF,OAAO;AACL,+BAAmB,CAAC,CAAC,gBAAgB,CAAC;AAAA,UACxC;AACA,mBAAS,KAAK,gBAAgB;AAAA,QAChC,SAAS,WAAW,QAAQ,IAAI;AAAA,MAClC;AACA,YAAM,WAAW;AAEjB,aAAO,KAAK,KAAK;AACjB,YAAM,KAAK,KAAK;AAAA,IAClB;AAEA;AACA,WAAO,MAAM,OAAO;AAAA,EACtB,SAAS,OAAO,MAAM;AAEtB,SAAO;AACT;AAEO,SAAS,sBAAsB,QAAkC;AACtE,MAAI,OAAO,WAAW,EAAG,QAAO;AAEhC,QAAM,SAAS,IAAI,aAAa;AAEhC,WAAS,IAAI,GAAG,IAAI,OAAO,UAAU;AACnC,QAAI,uBAAuB,QAAQ,GAAG,QAAQ,CAAC,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC,CAAC;AAAA,EACrE;AAEA,SAAO,OAAO,MAAM;AACtB;AAEA,SAAS,uBACP,QACA,OACA,QACA,OASQ;AACR,QAAM,QAAQ,OAAO,KAAK;AAC1B,QAAM;AAAA,IACJ,GAAG;AAAA,IACH,GAAG;AAAA,IACH,GAAG;AAAA,IACH,GAAG;AAAA,IACH;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI;AAEJ,MAAI,MAAM,CAAC,IAAI,WAAW;AACxB,gBAAY,QAAQ,MAAM,CAAC,GAAG,SAAS;AACvC,UAAM,CAAC,IAAI;AACX,UAAM,CAAC,IAAI;AAAA,EACb,WAAW,QAAQ,GAAG;AACpB,WAAO,MAAM,KAAK;AAAA,EACpB;AAEA,QAAM,CAAC,IAAI,cAAc,QAAQ,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC;AAEnD,QAAM,UACH,MAAM,WAAW,IAAI,IAAS,MAAM,WAAW,IAAS,MAAM,UAAU,IAAS;AACpF,gBAAc,QAAQ,QAAQ,CAAC;AAE/B,MAAI,MAAM,WAAW,GAAG;AACtB,UAAM,EAAE,GAAG,cAAc,GAAG,YAAY,IAAI;AAC5C,QAAI,iBAAiB,MAAM,CAAC,GAAG;AAC7B,YAAM,CAAC,IAAI;AAAA,IACb;AACA,UAAM,CAAC,IAAI,cAAc,QAAQ,cAAc,MAAM,CAAC,CAAC;AACvD,UAAM,CAAC,IAAI,cAAc,QAAQ,aAAa,MAAM,CAAC,CAAC;AAAA,EACxD;AAEA,MAAI,UAAU;AACZ,UAAM,EAAE,GAAG,cAAc,GAAG,UAAU,GAAG,WAAW,IAAI,MAAM;AAC9D,QAAI,iBAAiB,MAAM,CAAC,GAAG;AAC7B,YAAM,CAAC,IAAI;AACX,YAAM,CAAC,IAAI;AAAA,IACb,WAAW,aAAa,MAAM,CAAC,GAAG;AAChC,YAAM,CAAC,IAAI;AAAA,IACb;AACA,UAAM,CAAC,IAAI,cAAc,QAAQ,cAAc,MAAM,CAAC,CAAC;AACvD,UAAM,CAAC,IAAI,cAAc,QAAQ,UAAU,MAAM,CAAC,CAAC;AACnD,UAAM,CAAC,IAAI,cAAc,QAAQ,YAAY,MAAM,CAAC,CAAC;AAAA,EACvD;AAEA,MAAI,UAAU;AACZ,eAAW,WAAW,UAAU;AAC9B,UAAI,QAAQ,SAAS,EAAG,eAAc,QAAQ,CAAC,QAAQ,QAAQ,CAAC;AAChE,YAAM,aAAa,QAAQ,CAAC,EAAE,CAAC;AAC/B,oBAAc,QAAQ,YAAY,CAAC;AACnC,UAAI,mBAAmB;AACvB,UAAI,qBAAqB;AACzB,eAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;AACvC,cAAM,WAAW,QAAQ,CAAC;AAC1B,2BAAmB,cAAc,QAAQ,SAAS,CAAC,GAAI,gBAAgB;AACvE,6BAAqB,cAAc,QAAQ,SAAS,CAAC,GAAI,kBAAkB;AAC3E,sBAAc,QAAQ,SAAS,CAAC,GAAI,CAAC;AAAA,MACvC;AAAA,IACF;AAAA,EACF;AAEA,OAAK,SAAS,QAAQ,OAAO,UAAU;AACrC,UAAM,OAAO,OAAO,KAAK;AACzB,UAAM,EAAE,GAAG,GAAG,GAAG,EAAE,IAAI;AACvB,QAAI,IAAI,WAAY,MAAM,WAAW,KAAK,WAAY;AACpD;AAAA,IACF;AACA,YAAQ,uBAAuB,QAAQ,OAAO,QAAQ,KAAK;AAAA,EAC7D;AAEA,MAAI,MAAM,CAAC,IAAI,SAAS;AACtB,gBAAY,QAAQ,MAAM,CAAC,GAAG,OAAO;AACrC,UAAM,CAAC,IAAI;AACX,UAAM,CAAC,IAAI;AAAA,EACb,OAAO;AACL,WAAO,MAAM,KAAK;AAAA,EACpB;AACA,QAAM,CAAC,IAAI,cAAc,QAAQ,WAAW,MAAM,CAAC,CAAC;AAEpD,SAAO;AACT;AAEA,SAAS,YAAY,QAAsB,UAAkB,MAAc;AACzE,KAAG;AACD,WAAO,MAAM,SAAS;AAAA,EACxB,SAAS,EAAE,WAAW;AACxB;;;AHtUO,SAAS,OAAO,UAAqC;AAC1D,QAAM,EAAE,OAAO,IAAI;AACnB,QAAM,SAAS,IAAI,aAAa,QAAQ;AACxC,QAAM,UAA6B,CAAC;AACpC,MAAI,YAAY;AAChB,MAAI,eAAe;AACnB,MAAI,aAAa;AACjB,MAAI,eAAe;AACnB,MAAI,aAAa;AAEjB,KAAG;AACD,UAAM,OAAO,OAAO,QAAQ,GAAG;AAC/B,UAAM,OAAsB,CAAC;AAC7B,QAAI,SAAS;AACb,QAAI,UAAU;AACd,gBAAY;AAEZ,WAAO,OAAO,MAAM,MAAM;AACxB,UAAI;AAEJ,kBAAY,cAAc,QAAQ,SAAS;AAC3C,UAAI,YAAY,QAAS,UAAS;AAClC,gBAAU;AAEV,UAAI,WAAW,QAAQ,IAAI,GAAG;AAC5B,uBAAe,cAAc,QAAQ,YAAY;AACjD,qBAAa,cAAc,QAAQ,UAAU;AAC7C,uBAAe,cAAc,QAAQ,YAAY;AAEjD,YAAI,WAAW,QAAQ,IAAI,GAAG;AAC5B,uBAAa,cAAc,QAAQ,UAAU;AAC7C,gBAAM,CAAC,WAAW,cAAc,YAAY,cAAc,UAAU;AAAA,QACtE,OAAO;AACL,gBAAM,CAAC,WAAW,cAAc,YAAY,YAAY;AAAA,QAC1D;AAAA,MACF,OAAO;AACL,cAAM,CAAC,SAAS;AAAA,MAClB;AAEA,WAAK,KAAK,GAAG;AACb,aAAO;AAAA,IACT;AAEA,QAAI,CAAC,OAAQ,MAAK,IAAI;AACtB,YAAQ,KAAK,IAAI;AACjB,WAAO,MAAM,OAAO;AAAA,EACtB,SAAS,OAAO,OAAO;AAEvB,SAAO;AACT;AAEA,SAAS,KAAK,MAA0B;AACtC,OAAK,KAAK,cAAc;AAC1B;AAEA,SAAS,eAAe,GAAqB,GAA6B;AACxE,SAAO,EAAE,CAAC,IAAI,EAAE,CAAC;AACnB;AAIO,SAAS,OAAO,SAA8C;AACnE,QAAM,SAAS,IAAI,aAAa;AAChC,MAAI,eAAe;AACnB,MAAI,aAAa;AACjB,MAAI,eAAe;AACnB,MAAI,aAAa;AAEjB,WAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;AACvC,UAAM,OAAO,QAAQ,CAAC;AACtB,QAAI,IAAI,EAAG,QAAO,MAAM,SAAS;AACjC,QAAI,KAAK,WAAW,EAAG;AAEvB,QAAI,YAAY;AAEhB,aAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,YAAM,UAAU,KAAK,CAAC;AACtB,UAAI,IAAI,EAAG,QAAO,MAAM,KAAK;AAE7B,kBAAY,cAAc,QAAQ,QAAQ,CAAC,GAAG,SAAS;AAEvD,UAAI,QAAQ,WAAW,EAAG;AAC1B,qBAAe,cAAc,QAAQ,QAAQ,CAAC,GAAG,YAAY;AAC7D,mBAAa,cAAc,QAAQ,QAAQ,CAAC,GAAG,UAAU;AACzD,qBAAe,cAAc,QAAQ,QAAQ,CAAC,GAAG,YAAY;AAE7D,UAAI,QAAQ,WAAW,EAAG;AAC1B,mBAAa,cAAc,QAAQ,QAAQ,CAAC,GAAG,UAAU;AAAA,IAC3D;AAAA,EACF;AAEA,SAAO,OAAO,MAAM;AACtB;", | |
| 5 | "names": [] | |
| 6 | } |
| … | … | @@ -0,0 +1,63 @@ |
| 1 | { | |
| 2 | "name": "@jridgewell/sourcemap-codec", | |
| 3 | "version": "1.5.5", | |
| 4 | "description": "Encode/decode sourcemap mappings", | |
| 5 | "keywords": [ | |
| 6 | "sourcemap", | |
| 7 | "vlq" | |
| 8 | ], | |
| 9 | "main": "dist/sourcemap-codec.umd.js", | |
| 10 | "module": "dist/sourcemap-codec.mjs", | |
| 11 | "types": "types/sourcemap-codec.d.cts", | |
| 12 | "files": [ | |
| 13 | "dist", | |
| 14 | "src", | |
| 15 | "types" | |
| 16 | ], | |
| 17 | "exports": { | |
| 18 | ".": [ | |
| 19 | { | |
| 20 | "import": { | |
| 21 | "types": "./types/sourcemap-codec.d.mts", | |
| 22 | "default": "./dist/sourcemap-codec.mjs" | |
| 23 | }, | |
| 24 | "default": { | |
| 25 | "types": "./types/sourcemap-codec.d.cts", | |
| 26 | "default": "./dist/sourcemap-codec.umd.js" | |
| 27 | } | |
| 28 | }, | |
| 29 | "./dist/sourcemap-codec.umd.js" | |
| 30 | ], | |
| 31 | "./package.json": "./package.json" | |
| 32 | }, | |
| 33 | "scripts": { | |
| 34 | "benchmark": "run-s build:code benchmark:*", | |
| 35 | "benchmark:install": "cd benchmark && npm install", | |
| 36 | "benchmark:only": "node --expose-gc benchmark/index.js", | |
| 37 | "build": "run-s -n build:code build:types", | |
| 38 | "build:code": "node ../../esbuild.mjs sourcemap-codec.ts", | |
| 39 | "build:types": "run-s build:types:force build:types:emit build:types:mts", | |
| 40 | "build:types:force": "rimraf tsconfig.build.tsbuildinfo", | |
| 41 | "build:types:emit": "tsc --project tsconfig.build.json", | |
| 42 | "build:types:mts": "node ../../mts-types.mjs", | |
| 43 | "clean": "run-s -n clean:code clean:types", | |
| 44 | "clean:code": "tsc --build --clean tsconfig.build.json", | |
| 45 | "clean:types": "rimraf dist types", | |
| 46 | "test": "run-s -n test:types test:only test:format", | |
| 47 | "test:format": "prettier --check '{src,test}/**/*.ts'", | |
| 48 | "test:only": "mocha", | |
| 49 | "test:types": "eslint '{src,test}/**/*.ts'", | |
| 50 | "lint": "run-s -n lint:types lint:format", | |
| 51 | "lint:format": "npm run test:format -- --write", | |
| 52 | "lint:types": "npm run test:types -- --fix", | |
| 53 | "prepublishOnly": "npm run-s -n build test" | |
| 54 | }, | |
| 55 | "homepage": "https://github.com/jridgewell/sourcemaps/tree/main/packages/sourcemap-codec", | |
| 56 | "repository": { | |
| 57 | "type": "git", | |
| 58 | "url": "git+https://github.com/jridgewell/sourcemaps.git", | |
| 59 | "directory": "packages/sourcemap-codec" | |
| 60 | }, | |
| 61 | "author": "Justin Ridgewell <justin@ridgewell.name>", | |
| 62 | "license": "MIT" | |
| 63 | } |
| … | … | @@ -0,0 +1,345 @@ |
| 1 | import { StringReader, StringWriter } from './strings'; | |
| 2 | import { comma, decodeInteger, encodeInteger, hasMoreVlq, semicolon } from './vlq'; | |
| 3 | ||
| 4 | const EMPTY: any[] = []; | |
| 5 | ||
| 6 | type Line = number; | |
| 7 | type Column = number; | |
| 8 | type Kind = number; | |
| 9 | type Name = number; | |
| 10 | type Var = number; | |
| 11 | type SourcesIndex = number; | |
| 12 | type ScopesIndex = number; | |
| 13 | ||
| 14 | type Mix<A, B, O> = (A & O) | (B & O); | |
| 15 | ||
| 16 | export type OriginalScope = Mix< | |
| 17 | [Line, Column, Line, Column, Kind], | |
| 18 | [Line, Column, Line, Column, Kind, Name], | |
| 19 | { vars: Var[] } | |
| 20 | >; | |
| 21 | ||
| 22 | export type GeneratedRange = Mix< | |
| 23 | [Line, Column, Line, Column], | |
| 24 | [Line, Column, Line, Column, SourcesIndex, ScopesIndex], | |
| 25 | { | |
| 26 | callsite: CallSite | null; | |
| 27 | bindings: Binding[]; | |
| 28 | isScope: boolean; | |
| 29 | } | |
| 30 | >; | |
| 31 | export type CallSite = [SourcesIndex, Line, Column]; | |
| 32 | type Binding = BindingExpressionRange[]; | |
| 33 | export type BindingExpressionRange = [Name] | [Name, Line, Column]; | |
| 34 | ||
| 35 | export function decodeOriginalScopes(input: string): OriginalScope[] { | |
| 36 | const { length } = input; | |
| 37 | const reader = new StringReader(input); | |
| 38 | const scopes: OriginalScope[] = []; | |
| 39 | const stack: OriginalScope[] = []; | |
| 40 | let line = 0; | |
| 41 | ||
| 42 | for (; reader.pos < length; reader.pos++) { | |
| 43 | line = decodeInteger(reader, line); | |
| 44 | const column = decodeInteger(reader, 0); | |
| 45 | ||
| 46 | if (!hasMoreVlq(reader, length)) { | |
| 47 | const last = stack.pop()!; | |
| 48 | last[2] = line; | |
| 49 | last[3] = column; | |
| 50 | continue; | |
| 51 | } | |
| 52 | ||
| 53 | const kind = decodeInteger(reader, 0); | |
| 54 | const fields = decodeInteger(reader, 0); | |
| 55 | const hasName = fields & 0b0001; | |
| 56 | ||
| 57 | const scope: OriginalScope = ( | |
| 58 | hasName ? [line, column, 0, 0, kind, decodeInteger(reader, 0)] : [line, column, 0, 0, kind] | |
| 59 | ) as OriginalScope; | |
| 60 | ||
| 61 | let vars: Var[] = EMPTY; | |
| 62 | if (hasMoreVlq(reader, length)) { | |
| 63 | vars = []; | |
| 64 | do { | |
| 65 | const varsIndex = decodeInteger(reader, 0); | |
| 66 | vars.push(varsIndex); | |
| 67 | } while (hasMoreVlq(reader, length)); | |
| 68 | } | |
| 69 | scope.vars = vars; | |
| 70 | ||
| 71 | scopes.push(scope); | |
| 72 | stack.push(scope); | |
| 73 | } | |
| 74 | ||
| 75 | return scopes; | |
| 76 | } | |
| 77 | ||
| 78 | export function encodeOriginalScopes(scopes: OriginalScope[]): string { | |
| 79 | const writer = new StringWriter(); | |
| 80 | ||
| 81 | for (let i = 0; i < scopes.length; ) { | |
| 82 | i = _encodeOriginalScopes(scopes, i, writer, [0]); | |
| 83 | } | |
| 84 | ||
| 85 | return writer.flush(); | |
| 86 | } | |
| 87 | ||
| 88 | function _encodeOriginalScopes( | |
| 89 | scopes: OriginalScope[], | |
| 90 | index: number, | |
| 91 | writer: StringWriter, | |
| 92 | state: [ | |
| 93 | number, // GenColumn | |
| 94 | ], | |
| 95 | ): number { | |
| 96 | const scope = scopes[index]; | |
| 97 | const { 0: startLine, 1: startColumn, 2: endLine, 3: endColumn, 4: kind, vars } = scope; | |
| 98 | ||
| 99 | if (index > 0) writer.write(comma); | |
| 100 | ||
| 101 | state[0] = encodeInteger(writer, startLine, state[0]); | |
| 102 | encodeInteger(writer, startColumn, 0); | |
| 103 | encodeInteger(writer, kind, 0); | |
| 104 | ||
| 105 | const fields = scope.length === 6 ? 0b0001 : 0; | |
| 106 | encodeInteger(writer, fields, 0); | |
| 107 | if (scope.length === 6) encodeInteger(writer, scope[5], 0); | |
| 108 | ||
| 109 | for (const v of vars) { | |
| 110 | encodeInteger(writer, v, 0); | |
| 111 | } | |
| 112 | ||
| 113 | for (index++; index < scopes.length; ) { | |
| 114 | const next = scopes[index]; | |
| 115 | const { 0: l, 1: c } = next; | |
| 116 | if (l > endLine || (l === endLine && c >= endColumn)) { | |
| 117 | break; | |
| 118 | } | |
| 119 | index = _encodeOriginalScopes(scopes, index, writer, state); | |
| 120 | } | |
| 121 | ||
| 122 | writer.write(comma); | |
| 123 | state[0] = encodeInteger(writer, endLine, state[0]); | |
| 124 | encodeInteger(writer, endColumn, 0); | |
| 125 | ||
| 126 | return index; | |
| 127 | } | |
| 128 | ||
| 129 | export function decodeGeneratedRanges(input: string): GeneratedRange[] { | |
| 130 | const { length } = input; | |
| 131 | const reader = new StringReader(input); | |
| 132 | const ranges: GeneratedRange[] = []; | |
| 133 | const stack: GeneratedRange[] = []; | |
| 134 | ||
| 135 | let genLine = 0; | |
| 136 | let definitionSourcesIndex = 0; | |
| 137 | let definitionScopeIndex = 0; | |
| 138 | let callsiteSourcesIndex = 0; | |
| 139 | let callsiteLine = 0; | |
| 140 | let callsiteColumn = 0; | |
| 141 | let bindingLine = 0; | |
| 142 | let bindingColumn = 0; | |
| 143 | ||
| 144 | do { | |
| 145 | const semi = reader.indexOf(';'); | |
| 146 | let genColumn = 0; | |
| 147 | ||
| 148 | for (; reader.pos < semi; reader.pos++) { | |
| 149 | genColumn = decodeInteger(reader, genColumn); | |
| 150 | ||
| 151 | if (!hasMoreVlq(reader, semi)) { | |
| 152 | const last = stack.pop()!; | |
| 153 | last[2] = genLine; | |
| 154 | last[3] = genColumn; | |
| 155 | continue; | |
| 156 | } | |
| 157 | ||
| 158 | const fields = decodeInteger(reader, 0); | |
| 159 | const hasDefinition = fields & 0b0001; | |
| 160 | const hasCallsite = fields & 0b0010; | |
| 161 | const hasScope = fields & 0b0100; | |
| 162 | ||
| 163 | let callsite: CallSite | null = null; | |
| 164 | let bindings: Binding[] = EMPTY; | |
| 165 | let range: GeneratedRange; | |
| 166 | if (hasDefinition) { | |
| 167 | const defSourcesIndex = decodeInteger(reader, definitionSourcesIndex); | |
| 168 | definitionScopeIndex = decodeInteger( | |
| 169 | reader, | |
| 170 | definitionSourcesIndex === defSourcesIndex ? definitionScopeIndex : 0, | |
| 171 | ); | |
| 172 | ||
| 173 | definitionSourcesIndex = defSourcesIndex; | |
| 174 | range = [genLine, genColumn, 0, 0, defSourcesIndex, definitionScopeIndex] as GeneratedRange; | |
| 175 | } else { | |
| 176 | range = [genLine, genColumn, 0, 0] as GeneratedRange; | |
| 177 | } | |
| 178 | ||
| 179 | range.isScope = !!hasScope; | |
| 180 | ||
| 181 | if (hasCallsite) { | |
| 182 | const prevCsi = callsiteSourcesIndex; | |
| 183 | const prevLine = callsiteLine; | |
| 184 | callsiteSourcesIndex = decodeInteger(reader, callsiteSourcesIndex); | |
| 185 | const sameSource = prevCsi === callsiteSourcesIndex; | |
| 186 | callsiteLine = decodeInteger(reader, sameSource ? callsiteLine : 0); | |
| 187 | callsiteColumn = decodeInteger( | |
| 188 | reader, | |
| 189 | sameSource && prevLine === callsiteLine ? callsiteColumn : 0, | |
| 190 | ); | |
| 191 | ||
| 192 | callsite = [callsiteSourcesIndex, callsiteLine, callsiteColumn]; | |
| 193 | } | |
| 194 | range.callsite = callsite; | |
| 195 | ||
| 196 | if (hasMoreVlq(reader, semi)) { | |
| 197 | bindings = []; | |
| 198 | do { | |
| 199 | bindingLine = genLine; | |
| 200 | bindingColumn = genColumn; | |
| 201 | const expressionsCount = decodeInteger(reader, 0); | |
| 202 | let expressionRanges: BindingExpressionRange[]; | |
| 203 | if (expressionsCount < -1) { | |
| 204 | expressionRanges = [[decodeInteger(reader, 0)]]; | |
| 205 | for (let i = -1; i > expressionsCount; i--) { | |
| 206 | const prevBl = bindingLine; | |
| 207 | bindingLine = decodeInteger(reader, bindingLine); | |
| 208 | bindingColumn = decodeInteger(reader, bindingLine === prevBl ? bindingColumn : 0); | |
| 209 | const expression = decodeInteger(reader, 0); | |
| 210 | expressionRanges.push([expression, bindingLine, bindingColumn]); | |
| 211 | } | |
| 212 | } else { | |
| 213 | expressionRanges = [[expressionsCount]]; | |
| 214 | } | |
| 215 | bindings.push(expressionRanges); | |
| 216 | } while (hasMoreVlq(reader, semi)); | |
| 217 | } | |
| 218 | range.bindings = bindings; | |
| 219 | ||
| 220 | ranges.push(range); | |
| 221 | stack.push(range); | |
| 222 | } | |
| 223 | ||
| 224 | genLine++; | |
| 225 | reader.pos = semi + 1; | |
| 226 | } while (reader.pos < length); | |
| 227 | ||
| 228 | return ranges; | |
| 229 | } | |
| 230 | ||
| 231 | export function encodeGeneratedRanges(ranges: GeneratedRange[]): string { | |
| 232 | if (ranges.length === 0) return ''; | |
| 233 | ||
| 234 | const writer = new StringWriter(); | |
| 235 | ||
| 236 | for (let i = 0; i < ranges.length; ) { | |
| 237 | i = _encodeGeneratedRanges(ranges, i, writer, [0, 0, 0, 0, 0, 0, 0]); | |
| 238 | } | |
| 239 | ||
| 240 | return writer.flush(); | |
| 241 | } | |
| 242 | ||
| 243 | function _encodeGeneratedRanges( | |
| 244 | ranges: GeneratedRange[], | |
| 245 | index: number, | |
| 246 | writer: StringWriter, | |
| 247 | state: [ | |
| 248 | number, // GenLine | |
| 249 | number, // GenColumn | |
| 250 | number, // DefSourcesIndex | |
| 251 | number, // DefScopesIndex | |
| 252 | number, // CallSourcesIndex | |
| 253 | number, // CallLine | |
| 254 | number, // CallColumn | |
| 255 | ], | |
| 256 | ): number { | |
| 257 | const range = ranges[index]; | |
| 258 | const { | |
| 259 | 0: startLine, | |
| 260 | 1: startColumn, | |
| 261 | 2: endLine, | |
| 262 | 3: endColumn, | |
| 263 | isScope, | |
| 264 | callsite, | |
| 265 | bindings, | |
| 266 | } = range; | |
| 267 | ||
| 268 | if (state[0] < startLine) { | |
| 269 | catchupLine(writer, state[0], startLine); | |
| 270 | state[0] = startLine; | |
| 271 | state[1] = 0; | |
| 272 | } else if (index > 0) { | |
| 273 | writer.write(comma); | |
| 274 | } | |
| 275 | ||
| 276 | state[1] = encodeInteger(writer, range[1], state[1]); | |
| 277 | ||
| 278 | const fields = | |
| 279 | (range.length === 6 ? 0b0001 : 0) | (callsite ? 0b0010 : 0) | (isScope ? 0b0100 : 0); | |
| 280 | encodeInteger(writer, fields, 0); | |
| 281 | ||
| 282 | if (range.length === 6) { | |
| 283 | const { 4: sourcesIndex, 5: scopesIndex } = range; | |
| 284 | if (sourcesIndex !== state[2]) { | |
| 285 | state[3] = 0; | |
| 286 | } | |
| 287 | state[2] = encodeInteger(writer, sourcesIndex, state[2]); | |
| 288 | state[3] = encodeInteger(writer, scopesIndex, state[3]); | |
| 289 | } | |
| 290 | ||
| 291 | if (callsite) { | |
| 292 | const { 0: sourcesIndex, 1: callLine, 2: callColumn } = range.callsite!; | |
| 293 | if (sourcesIndex !== state[4]) { | |
| 294 | state[5] = 0; | |
| 295 | state[6] = 0; | |
| 296 | } else if (callLine !== state[5]) { | |
| 297 | state[6] = 0; | |
| 298 | } | |
| 299 | state[4] = encodeInteger(writer, sourcesIndex, state[4]); | |
| 300 | state[5] = encodeInteger(writer, callLine, state[5]); | |
| 301 | state[6] = encodeInteger(writer, callColumn, state[6]); | |
| 302 | } | |
| 303 | ||
| 304 | if (bindings) { | |
| 305 | for (const binding of bindings) { | |
| 306 | if (binding.length > 1) encodeInteger(writer, -binding.length, 0); | |
| 307 | const expression = binding[0][0]; | |
| 308 | encodeInteger(writer, expression, 0); | |
| 309 | let bindingStartLine = startLine; | |
| 310 | let bindingStartColumn = startColumn; | |
| 311 | for (let i = 1; i < binding.length; i++) { | |
| 312 | const expRange = binding[i]; | |
| 313 | bindingStartLine = encodeInteger(writer, expRange[1]!, bindingStartLine); | |
| 314 | bindingStartColumn = encodeInteger(writer, expRange[2]!, bindingStartColumn); | |
| 315 | encodeInteger(writer, expRange[0]!, 0); | |
| 316 | } | |
| 317 | } | |
| 318 | } | |
| 319 | ||
| 320 | for (index++; index < ranges.length; ) { | |
| 321 | const next = ranges[index]; | |
| 322 | const { 0: l, 1: c } = next; | |
| 323 | if (l > endLine || (l === endLine && c >= endColumn)) { | |
| 324 | break; | |
| 325 | } | |
| 326 | index = _encodeGeneratedRanges(ranges, index, writer, state); | |
| 327 | } | |
| 328 | ||
| 329 | if (state[0] < endLine) { | |
| 330 | catchupLine(writer, state[0], endLine); | |
| 331 | state[0] = endLine; | |
| 332 | state[1] = 0; | |
| 333 | } else { | |
| 334 | writer.write(comma); | |
| 335 | } | |
| 336 | state[1] = encodeInteger(writer, endColumn, state[1]); | |
| 337 | ||
| 338 | return index; | |
| 339 | } | |
| 340 | ||
| 341 | function catchupLine(writer: StringWriter, lastLine: number, line: number) { | |
| 342 | do { | |
| 343 | writer.write(semicolon); | |
| 344 | } while (++lastLine < line); | |
| 345 | } |
| … | … | @@ -0,0 +1,111 @@ |
| 1 | import { comma, decodeInteger, encodeInteger, hasMoreVlq, semicolon } from './vlq'; | |
| 2 | import { StringWriter, StringReader } from './strings'; | |
| 3 | ||
| 4 | export { | |
| 5 | decodeOriginalScopes, | |
| 6 | encodeOriginalScopes, | |
| 7 | decodeGeneratedRanges, | |
| 8 | encodeGeneratedRanges, | |
| 9 | } from './scopes'; | |
| 10 | export type { OriginalScope, GeneratedRange, CallSite, BindingExpressionRange } from './scopes'; | |
| 11 | ||
| 12 | export type SourceMapSegment = | |
| 13 | | [number] | |
| 14 | | [number, number, number, number] | |
| 15 | | [number, number, number, number, number]; | |
| 16 | export type SourceMapLine = SourceMapSegment[]; | |
| 17 | export type SourceMapMappings = SourceMapLine[]; | |
| 18 | ||
| 19 | export function decode(mappings: string): SourceMapMappings { | |
| 20 | const { length } = mappings; | |
| 21 | const reader = new StringReader(mappings); | |
| 22 | const decoded: SourceMapMappings = []; | |
| 23 | let genColumn = 0; | |
| 24 | let sourcesIndex = 0; | |
| 25 | let sourceLine = 0; | |
| 26 | let sourceColumn = 0; | |
| 27 | let namesIndex = 0; | |
| 28 | ||
| 29 | do { | |
| 30 | const semi = reader.indexOf(';'); | |
| 31 | const line: SourceMapLine = []; | |
| 32 | let sorted = true; | |
| 33 | let lastCol = 0; | |
| 34 | genColumn = 0; | |
| 35 | ||
| 36 | while (reader.pos < semi) { | |
| 37 | let seg: SourceMapSegment; | |
| 38 | ||
| 39 | genColumn = decodeInteger(reader, genColumn); | |
| 40 | if (genColumn < lastCol) sorted = false; | |
| 41 | lastCol = genColumn; | |
| 42 | ||
| 43 | if (hasMoreVlq(reader, semi)) { | |
| 44 | sourcesIndex = decodeInteger(reader, sourcesIndex); | |
| 45 | sourceLine = decodeInteger(reader, sourceLine); | |
| 46 | sourceColumn = decodeInteger(reader, sourceColumn); | |
| 47 | ||
| 48 | if (hasMoreVlq(reader, semi)) { | |
| 49 | namesIndex = decodeInteger(reader, namesIndex); | |
| 50 | seg = [genColumn, sourcesIndex, sourceLine, sourceColumn, namesIndex]; | |
| 51 | } else { | |
| 52 | seg = [genColumn, sourcesIndex, sourceLine, sourceColumn]; | |
| 53 | } | |
| 54 | } else { | |
| 55 | seg = [genColumn]; | |
| 56 | } | |
| 57 | ||
| 58 | line.push(seg); | |
| 59 | reader.pos++; | |
| 60 | } | |
| 61 | ||
| 62 | if (!sorted) sort(line); | |
| 63 | decoded.push(line); | |
| 64 | reader.pos = semi + 1; | |
| 65 | } while (reader.pos <= length); | |
| 66 | ||
| 67 | return decoded; | |
| 68 | } | |
| 69 | ||
| 70 | function sort(line: SourceMapSegment[]) { | |
| 71 | line.sort(sortComparator); | |
| 72 | } | |
| 73 | ||
| 74 | function sortComparator(a: SourceMapSegment, b: SourceMapSegment): number { | |
| 75 | return a[0] - b[0]; | |
| 76 | } | |
| 77 | ||
| 78 | export function encode(decoded: SourceMapMappings): string; | |
| 79 | export function encode(decoded: Readonly<SourceMapMappings>): string; | |
| 80 | export function encode(decoded: Readonly<SourceMapMappings>): string { | |
| 81 | const writer = new StringWriter(); | |
| 82 | let sourcesIndex = 0; | |
| 83 | let sourceLine = 0; | |
| 84 | let sourceColumn = 0; | |
| 85 | let namesIndex = 0; | |
| 86 | ||
| 87 | for (let i = 0; i < decoded.length; i++) { | |
| 88 | const line = decoded[i]; | |
| 89 | if (i > 0) writer.write(semicolon); | |
| 90 | if (line.length === 0) continue; | |
| 91 | ||
| 92 | let genColumn = 0; | |
| 93 | ||
| 94 | for (let j = 0; j < line.length; j++) { | |
| 95 | const segment = line[j]; | |
| 96 | if (j > 0) writer.write(comma); | |
| 97 | ||
| 98 | genColumn = encodeInteger(writer, segment[0], genColumn); | |
| 99 | ||
| 100 | if (segment.length === 1) continue; | |
| 101 | sourcesIndex = encodeInteger(writer, segment[1], sourcesIndex); | |
| 102 | sourceLine = encodeInteger(writer, segment[2], sourceLine); | |
| 103 | sourceColumn = encodeInteger(writer, segment[3], sourceColumn); | |
| 104 | ||
| 105 | if (segment.length === 4) continue; | |
| 106 | namesIndex = encodeInteger(writer, segment[4], namesIndex); | |
| 107 | } | |
| 108 | } | |
| 109 | ||
| 110 | return writer.flush(); | |
| 111 | } |
| … | … | @@ -0,0 +1,65 @@ |
| 1 | const bufLength = 1024 * 16; | |
| 2 | ||
| 3 | // Provide a fallback for older environments. | |
| 4 | const td = | |
| 5 | typeof TextDecoder !== 'undefined' | |
| 6 | ? /* #__PURE__ */ new TextDecoder() | |
| 7 | : typeof Buffer !== 'undefined' | |
| 8 | ? { | |
| 9 | decode(buf: Uint8Array): string { | |
| 10 | const out = Buffer.from(buf.buffer, buf.byteOffset, buf.byteLength); | |
| 11 | return out.toString(); | |
| 12 | }, | |
| 13 | } | |
| 14 | : { | |
| 15 | decode(buf: Uint8Array): string { | |
| 16 | let out = ''; | |
| 17 | for (let i = 0; i < buf.length; i++) { | |
| 18 | out += String.fromCharCode(buf[i]); | |
| 19 | } | |
| 20 | return out; | |
| 21 | }, | |
| 22 | }; | |
| 23 | ||
| 24 | export class StringWriter { | |
| 25 | pos = 0; | |
| 26 | private out = ''; | |
| 27 | private buffer = new Uint8Array(bufLength); | |
| 28 | ||
| 29 | write(v: number): void { | |
| 30 | const { buffer } = this; | |
| 31 | buffer[this.pos++] = v; | |
| 32 | if (this.pos === bufLength) { | |
| 33 | this.out += td.decode(buffer); | |
| 34 | this.pos = 0; | |
| 35 | } | |
| 36 | } | |
| 37 | ||
| 38 | flush(): string { | |
| 39 | const { buffer, out, pos } = this; | |
| 40 | return pos > 0 ? out + td.decode(buffer.subarray(0, pos)) : out; | |
| 41 | } | |
| 42 | } | |
| 43 | ||
| 44 | export class StringReader { | |
| 45 | pos = 0; | |
| 46 | declare private buffer: string; | |
| 47 | ||
| 48 | constructor(buffer: string) { | |
| 49 | this.buffer = buffer; | |
| 50 | } | |
| 51 | ||
| 52 | next(): number { | |
| 53 | return this.buffer.charCodeAt(this.pos++); | |
| 54 | } | |
| 55 | ||
| 56 | peek(): number { | |
| 57 | return this.buffer.charCodeAt(this.pos); | |
| 58 | } | |
| 59 | ||
| 60 | indexOf(char: string): number { | |
| 61 | const { buffer, pos } = this; | |
| 62 | const idx = buffer.indexOf(char, pos); | |
| 63 | return idx === -1 ? buffer.length : idx; | |
| 64 | } | |
| 65 | } |
| … | … | @@ -0,0 +1,55 @@ |
| 1 | import type { StringReader, StringWriter } from './strings'; | |
| 2 | ||
| 3 | export const comma = ','.charCodeAt(0); | |
| 4 | export const semicolon = ';'.charCodeAt(0); | |
| 5 | ||
| 6 | const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; | |
| 7 | const intToChar = new Uint8Array(64); // 64 possible chars. | |
| 8 | const charToInt = new Uint8Array(128); // z is 122 in ASCII | |
| 9 | ||
| 10 | for (let i = 0; i < chars.length; i++) { | |
| 11 | const c = chars.charCodeAt(i); | |
| 12 | intToChar[i] = c; | |
| 13 | charToInt[c] = i; | |
| 14 | } | |
| 15 | ||
| 16 | export function decodeInteger(reader: StringReader, relative: number): number { | |
| 17 | let value = 0; | |
| 18 | let shift = 0; | |
| 19 | let integer = 0; | |
| 20 | ||
| 21 | do { | |
| 22 | const c = reader.next(); | |
| 23 | integer = charToInt[c]; | |
| 24 | value |= (integer & 31) << shift; | |
| 25 | shift += 5; | |
| 26 | } while (integer & 32); | |
| 27 | ||
| 28 | const shouldNegate = value & 1; | |
| 29 | value >>>= 1; | |
| 30 | ||
| 31 | if (shouldNegate) { | |
| 32 | value = -0x80000000 | -value; | |
| 33 | } | |
| 34 | ||
| 35 | return relative + value; | |
| 36 | } | |
| 37 | ||
| 38 | export function encodeInteger(builder: StringWriter, num: number, relative: number): number { | |
| 39 | let delta = num - relative; | |
| 40 | ||
| 41 | delta = delta < 0 ? (-delta << 1) | 1 : delta << 1; | |
| 42 | do { | |
| 43 | let clamped = delta & 0b011111; | |
| 44 | delta >>>= 5; | |
| 45 | if (delta > 0) clamped |= 0b100000; | |
| 46 | builder.write(intToChar[clamped]); | |
| 47 | } while (delta > 0); | |
| 48 | ||
| 49 | return num; | |
| 50 | } | |
| 51 | ||
| 52 | export function hasMoreVlq(reader: StringReader, max: number) { | |
| 53 | if (reader.pos >= max) return false; | |
| 54 | return reader.peek() !== comma; | |
| 55 | } |
| … | … | @@ -0,0 +1,50 @@ |
| 1 | type Line = number; | |
| 2 | type Column = number; | |
| 3 | type Kind = number; | |
| 4 | type Name = number; | |
| 5 | type Var = number; | |
| 6 | type SourcesIndex = number; | |
| 7 | type ScopesIndex = number; | |
| 8 | type Mix<A, B, O> = (A & O) | (B & O); | |
| 9 | export type OriginalScope = Mix<[ | |
| 10 | Line, | |
| 11 | Column, | |
| 12 | Line, | |
| 13 | Column, | |
| 14 | Kind | |
| 15 | ], [ | |
| 16 | Line, | |
| 17 | Column, | |
| 18 | Line, | |
| 19 | Column, | |
| 20 | Kind, | |
| 21 | Name | |
| 22 | ], { | |
| 23 | vars: Var[]; | |
| 24 | }>; | |
| 25 | export type GeneratedRange = Mix<[ | |
| 26 | Line, | |
| 27 | Column, | |
| 28 | Line, | |
| 29 | Column | |
| 30 | ], [ | |
| 31 | Line, | |
| 32 | Column, | |
| 33 | Line, | |
| 34 | Column, | |
| 35 | SourcesIndex, | |
| 36 | ScopesIndex | |
| 37 | ], { | |
| 38 | callsite: CallSite | null; | |
| 39 | bindings: Binding[]; | |
| 40 | isScope: boolean; | |
| 41 | }>; | |
| 42 | export type CallSite = [SourcesIndex, Line, Column]; | |
| 43 | type Binding = BindingExpressionRange[]; | |
| 44 | export type BindingExpressionRange = [Name] | [Name, Line, Column]; | |
| 45 | export declare function decodeOriginalScopes(input: string): OriginalScope[]; | |
| 46 | export declare function encodeOriginalScopes(scopes: OriginalScope[]): string; | |
| 47 | export declare function decodeGeneratedRanges(input: string): GeneratedRange[]; | |
| 48 | export declare function encodeGeneratedRanges(ranges: GeneratedRange[]): string; | |
| 49 | export {}; | |
| 50 | //# sourceMappingURL=scopes.d.ts.map |
| … | … | @@ -0,0 +1 @@ |
| 1 | {"version":3,"file":"scopes.d.ts","sourceRoot":"","sources":["../src/scopes.ts"],"names":[],"mappings":"AAKA,KAAK,IAAI,GAAG,MAAM,CAAC;AACnB,KAAK,MAAM,GAAG,MAAM,CAAC;AACrB,KAAK,IAAI,GAAG,MAAM,CAAC;AACnB,KAAK,IAAI,GAAG,MAAM,CAAC;AACnB,KAAK,GAAG,GAAG,MAAM,CAAC;AAClB,KAAK,YAAY,GAAG,MAAM,CAAC;AAC3B,KAAK,WAAW,GAAG,MAAM,CAAC;AAE1B,KAAK,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;AAEtC,MAAM,MAAM,aAAa,GAAG,GAAG,CAC7B;IAAC,IAAI;IAAE,MAAM;IAAE,IAAI;IAAE,MAAM;IAAE,IAAI;CAAC,EAClC;IAAC,IAAI;IAAE,MAAM;IAAE,IAAI;IAAE,MAAM;IAAE,IAAI;IAAE,IAAI;CAAC,EACxC;IAAE,IAAI,EAAE,GAAG,EAAE,CAAA;CAAE,CAChB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG,GAAG,CAC9B;IAAC,IAAI;IAAE,MAAM;IAAE,IAAI;IAAE,MAAM;CAAC,EAC5B;IAAC,IAAI;IAAE,MAAM;IAAE,IAAI;IAAE,MAAM;IAAE,YAAY;IAAE,WAAW;CAAC,EACvD;IACE,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC1B,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,OAAO,EAAE,OAAO,CAAC;CAClB,CACF,CAAC;AACF,MAAM,MAAM,QAAQ,GAAG,CAAC,YAAY,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;AACpD,KAAK,OAAO,GAAG,sBAAsB,EAAE,CAAC;AACxC,MAAM,MAAM,sBAAsB,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;AAEnE,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa,EAAE,CAyCnE;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,aAAa,EAAE,GAAG,MAAM,CAQpE;AA2CD,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,EAAE,CAoGrE;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,cAAc,EAAE,GAAG,MAAM,CAUtE"} |
| … | … | @@ -0,0 +1,50 @@ |
| 1 | type Line = number; | |
| 2 | type Column = number; | |
| 3 | type Kind = number; | |
| 4 | type Name = number; | |
| 5 | type Var = number; | |
| 6 | type SourcesIndex = number; | |
| 7 | type ScopesIndex = number; | |
| 8 | type Mix<A, B, O> = (A & O) | (B & O); | |
| 9 | export type OriginalScope = Mix<[ | |
| 10 | Line, | |
| 11 | Column, | |
| 12 | Line, | |
| 13 | Column, | |
| 14 | Kind | |
| 15 | ], [ | |
| 16 | Line, | |
| 17 | Column, | |
| 18 | Line, | |
| 19 | Column, | |
| 20 | Kind, | |
| 21 | Name | |
| 22 | ], { | |
| 23 | vars: Var[]; | |
| 24 | }>; | |
| 25 | export type GeneratedRange = Mix<[ | |
| 26 | Line, | |
| 27 | Column, | |
| 28 | Line, | |
| 29 | Column | |
| 30 | ], [ | |
| 31 | Line, | |
| 32 | Column, | |
| 33 | Line, | |
| 34 | Column, | |
| 35 | SourcesIndex, | |
| 36 | ScopesIndex | |
| 37 | ], { | |
| 38 | callsite: CallSite | null; | |
| 39 | bindings: Binding[]; | |
| 40 | isScope: boolean; | |
| 41 | }>; | |
| 42 | export type CallSite = [SourcesIndex, Line, Column]; | |
| 43 | type Binding = BindingExpressionRange[]; | |
| 44 | export type BindingExpressionRange = [Name] | [Name, Line, Column]; | |
| 45 | export declare function decodeOriginalScopes(input: string): OriginalScope[]; | |
| 46 | export declare function encodeOriginalScopes(scopes: OriginalScope[]): string; | |
| 47 | export declare function decodeGeneratedRanges(input: string): GeneratedRange[]; | |
| 48 | export declare function encodeGeneratedRanges(ranges: GeneratedRange[]): string; | |
| 49 | export {}; | |
| 50 | //# sourceMappingURL=scopes.d.ts.map |
| … | … | @@ -0,0 +1 @@ |
| 1 | {"version":3,"file":"scopes.d.ts","sourceRoot":"","sources":["../src/scopes.ts"],"names":[],"mappings":"AAKA,KAAK,IAAI,GAAG,MAAM,CAAC;AACnB,KAAK,MAAM,GAAG,MAAM,CAAC;AACrB,KAAK,IAAI,GAAG,MAAM,CAAC;AACnB,KAAK,IAAI,GAAG,MAAM,CAAC;AACnB,KAAK,GAAG,GAAG,MAAM,CAAC;AAClB,KAAK,YAAY,GAAG,MAAM,CAAC;AAC3B,KAAK,WAAW,GAAG,MAAM,CAAC;AAE1B,KAAK,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;AAEtC,MAAM,MAAM,aAAa,GAAG,GAAG,CAC7B;IAAC,IAAI;IAAE,MAAM;IAAE,IAAI;IAAE,MAAM;IAAE,IAAI;CAAC,EAClC;IAAC,IAAI;IAAE,MAAM;IAAE,IAAI;IAAE,MAAM;IAAE,IAAI;IAAE,IAAI;CAAC,EACxC;IAAE,IAAI,EAAE,GAAG,EAAE,CAAA;CAAE,CAChB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG,GAAG,CAC9B;IAAC,IAAI;IAAE,MAAM;IAAE,IAAI;IAAE,MAAM;CAAC,EAC5B;IAAC,IAAI;IAAE,MAAM;IAAE,IAAI;IAAE,MAAM;IAAE,YAAY;IAAE,WAAW;CAAC,EACvD;IACE,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC1B,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,OAAO,EAAE,OAAO,CAAC;CAClB,CACF,CAAC;AACF,MAAM,MAAM,QAAQ,GAAG,CAAC,YAAY,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;AACpD,KAAK,OAAO,GAAG,sBAAsB,EAAE,CAAC;AACxC,MAAM,MAAM,sBAAsB,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;AAEnE,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa,EAAE,CAyCnE;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,aAAa,EAAE,GAAG,MAAM,CAQpE;AA2CD,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,EAAE,CAoGrE;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,cAAc,EAAE,GAAG,MAAM,CAUtE"} |
| … | … | @@ -0,0 +1,9 @@ |
| 1 | export { decodeOriginalScopes, encodeOriginalScopes, decodeGeneratedRanges, encodeGeneratedRanges, } from './scopes.cts'; | |
| 2 | export type { OriginalScope, GeneratedRange, CallSite, BindingExpressionRange } from './scopes.cts'; | |
| 3 | export type SourceMapSegment = [number] | [number, number, number, number] | [number, number, number, number, number]; | |
| 4 | export type SourceMapLine = SourceMapSegment[]; | |
| 5 | export type SourceMapMappings = SourceMapLine[]; | |
| 6 | export declare function decode(mappings: string): SourceMapMappings; | |
| 7 | export declare function encode(decoded: SourceMapMappings): string; | |
| 8 | export declare function encode(decoded: Readonly<SourceMapMappings>): string; | |
| 9 | //# sourceMappingURL=sourcemap-codec.d.ts.map |
| … | … | @@ -0,0 +1 @@ |
| 1 | {"version":3,"file":"sourcemap-codec.d.ts","sourceRoot":"","sources":["../src/sourcemap-codec.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,UAAU,CAAC;AAClB,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,QAAQ,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAEhG,MAAM,MAAM,gBAAgB,GACxB,CAAC,MAAM,CAAC,GACR,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,GAChC,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;AAC7C,MAAM,MAAM,aAAa,GAAG,gBAAgB,EAAE,CAAC;AAC/C,MAAM,MAAM,iBAAiB,GAAG,aAAa,EAAE,CAAC;AAEhD,wBAAgB,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,iBAAiB,CAiD1D;AAUD,wBAAgB,MAAM,CAAC,OAAO,EAAE,iBAAiB,GAAG,MAAM,CAAC;AAC3D,wBAAgB,MAAM,CAAC,OAAO,EAAE,QAAQ,CAAC,iBAAiB,CAAC,GAAG,MAAM,CAAC"} |
| … | … | @@ -0,0 +1,9 @@ |
| 1 | export { decodeOriginalScopes, encodeOriginalScopes, decodeGeneratedRanges, encodeGeneratedRanges, } from './scopes.mts'; | |
| 2 | export type { OriginalScope, GeneratedRange, CallSite, BindingExpressionRange } from './scopes.mts'; | |
| 3 | export type SourceMapSegment = [number] | [number, number, number, number] | [number, number, number, number, number]; | |
| 4 | export type SourceMapLine = SourceMapSegment[]; | |
| 5 | export type SourceMapMappings = SourceMapLine[]; | |
| 6 | export declare function decode(mappings: string): SourceMapMappings; | |
| 7 | export declare function encode(decoded: SourceMapMappings): string; | |
| 8 | export declare function encode(decoded: Readonly<SourceMapMappings>): string; | |
| 9 | //# sourceMappingURL=sourcemap-codec.d.ts.map |
| … | … | @@ -0,0 +1 @@ |
| 1 | {"version":3,"file":"sourcemap-codec.d.ts","sourceRoot":"","sources":["../src/sourcemap-codec.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,UAAU,CAAC;AAClB,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,QAAQ,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAEhG,MAAM,MAAM,gBAAgB,GACxB,CAAC,MAAM,CAAC,GACR,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,GAChC,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;AAC7C,MAAM,MAAM,aAAa,GAAG,gBAAgB,EAAE,CAAC;AAC/C,MAAM,MAAM,iBAAiB,GAAG,aAAa,EAAE,CAAC;AAEhD,wBAAgB,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,iBAAiB,CAiD1D;AAUD,wBAAgB,MAAM,CAAC,OAAO,EAAE,iBAAiB,GAAG,MAAM,CAAC;AAC3D,wBAAgB,MAAM,CAAC,OAAO,EAAE,QAAQ,CAAC,iBAAiB,CAAC,GAAG,MAAM,CAAC"} |
| … | … | @@ -0,0 +1,16 @@ |
| 1 | export declare class StringWriter { | |
| 2 | pos: number; | |
| 3 | private out; | |
| 4 | private buffer; | |
| 5 | write(v: number): void; | |
| 6 | flush(): string; | |
| 7 | } | |
| 8 | export declare class StringReader { | |
| 9 | pos: number; | |
| 10 | private buffer; | |
| 11 | constructor(buffer: string); | |
| 12 | next(): number; | |
| 13 | peek(): number; | |
| 14 | indexOf(char: string): number; | |
| 15 | } | |
| 16 | //# sourceMappingURL=strings.d.ts.map |
| … | … | @@ -0,0 +1 @@ |
| 1 | {"version":3,"file":"strings.d.ts","sourceRoot":"","sources":["../src/strings.ts"],"names":[],"mappings":"AAuBA,qBAAa,YAAY;IACvB,GAAG,SAAK;IACR,OAAO,CAAC,GAAG,CAAM;IACjB,OAAO,CAAC,MAAM,CAA6B;IAE3C,KAAK,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI;IAStB,KAAK,IAAI,MAAM;CAIhB;AAED,qBAAa,YAAY;IACvB,GAAG,SAAK;IACR,QAAgB,MAAM,CAAS;gBAEnB,MAAM,EAAE,MAAM;IAI1B,IAAI,IAAI,MAAM;IAId,IAAI,IAAI,MAAM;IAId,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;CAK9B"} |
| … | … | @@ -0,0 +1,16 @@ |
| 1 | export declare class StringWriter { | |
| 2 | pos: number; | |
| 3 | private out; | |
| 4 | private buffer; | |
| 5 | write(v: number): void; | |
| 6 | flush(): string; | |
| 7 | } | |
| 8 | export declare class StringReader { | |
| 9 | pos: number; | |
| 10 | private buffer; | |
| 11 | constructor(buffer: string); | |
| 12 | next(): number; | |
| 13 | peek(): number; | |
| 14 | indexOf(char: string): number; | |
| 15 | } | |
| 16 | //# sourceMappingURL=strings.d.ts.map |
| … | … | @@ -0,0 +1 @@ |
| 1 | {"version":3,"file":"strings.d.ts","sourceRoot":"","sources":["../src/strings.ts"],"names":[],"mappings":"AAuBA,qBAAa,YAAY;IACvB,GAAG,SAAK;IACR,OAAO,CAAC,GAAG,CAAM;IACjB,OAAO,CAAC,MAAM,CAA6B;IAE3C,KAAK,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI;IAStB,KAAK,IAAI,MAAM;CAIhB;AAED,qBAAa,YAAY;IACvB,GAAG,SAAK;IACR,QAAgB,MAAM,CAAS;gBAEnB,MAAM,EAAE,MAAM;IAI1B,IAAI,IAAI,MAAM;IAId,IAAI,IAAI,MAAM;IAId,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;CAK9B"} |
| … | … | @@ -0,0 +1,7 @@ |
| 1 | import type { StringReader, StringWriter } from './strings.cts'; | |
| 2 | export declare const comma: number; | |
| 3 | export declare const semicolon: number; | |
| 4 | export declare function decodeInteger(reader: StringReader, relative: number): number; | |
| 5 | export declare function encodeInteger(builder: StringWriter, num: number, relative: number): number; | |
| 6 | export declare function hasMoreVlq(reader: StringReader, max: number): boolean; | |
| 7 | //# sourceMappingURL=vlq.d.ts.map |
| … | … | @@ -0,0 +1 @@ |
| 1 | {"version":3,"file":"vlq.d.ts","sourceRoot":"","sources":["../src/vlq.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAE5D,eAAO,MAAM,KAAK,QAAoB,CAAC;AACvC,eAAO,MAAM,SAAS,QAAoB,CAAC;AAY3C,wBAAgB,aAAa,CAAC,MAAM,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAoB5E;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAY1F;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,WAG3D"} |
| … | … | @@ -0,0 +1,7 @@ |
| 1 | import type { StringReader, StringWriter } from './strings.mts'; | |
| 2 | export declare const comma: number; | |
| 3 | export declare const semicolon: number; | |
| 4 | export declare function decodeInteger(reader: StringReader, relative: number): number; | |
| 5 | export declare function encodeInteger(builder: StringWriter, num: number, relative: number): number; | |
| 6 | export declare function hasMoreVlq(reader: StringReader, max: number): boolean; | |
| 7 | //# sourceMappingURL=vlq.d.ts.map |
| … | … | @@ -0,0 +1 @@ |
| 1 | {"version":3,"file":"vlq.d.ts","sourceRoot":"","sources":["../src/vlq.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAE5D,eAAO,MAAM,KAAK,QAAoB,CAAC;AACvC,eAAO,MAAM,SAAS,QAAoB,CAAC;AAY3C,wBAAgB,aAAa,CAAC,MAAM,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAoB5E;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAY1F;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,WAG3D"} |
| … | … | @@ -0,0 +1,19 @@ |
| 1 | Copyright 2024 Justin Ridgewell <justin@ridgewell.name> | |
| 2 | ||
| 3 | Permission is hereby granted, free of charge, to any person obtaining a copy | |
| 4 | of this software and associated documentation files (the "Software"), to deal | |
| 5 | in the Software without restriction, including without limitation the rights | |
| 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| 7 | copies of the Software, and to permit persons to whom the Software is | |
| 8 | furnished to do so, subject to the following conditions: | |
| 9 | ||
| 10 | The above copyright notice and this permission notice shall be included in | |
| 11 | all copies or substantial portions of the Software. | |
| 12 | ||
| 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
| 19 | SOFTWARE. |
| … | … | @@ -0,0 +1,348 @@ |
| 1 | # @jridgewell/trace-mapping | |
| 2 | ||
| 3 | > Trace the original position through a source map | |
| 4 | ||
| 5 | `trace-mapping` allows you to take the line and column of an output file and trace it to the | |
| 6 | original location in the source file through a source map. | |
| 7 | ||
| 8 | You may already be familiar with the [`source-map`][source-map] package's `SourceMapConsumer`. This | |
| 9 | provides the same `originalPositionFor` and `generatedPositionFor` API, without requiring WASM. | |
| 10 | ||
| 11 | ## Installation | |
| 12 | ||
| 13 | ```sh | |
| 14 | npm install @jridgewell/trace-mapping | |
| 15 | ``` | |
| 16 | ||
| 17 | ## Usage | |
| 18 | ||
| 19 | ```typescript | |
| 20 | import { | |
| 21 | TraceMap, | |
| 22 | originalPositionFor, | |
| 23 | generatedPositionFor, | |
| 24 | sourceContentFor, | |
| 25 | isIgnored, | |
| 26 | } from '@jridgewell/trace-mapping'; | |
| 27 | ||
| 28 | const tracer = new TraceMap({ | |
| 29 | version: 3, | |
| 30 | sources: ['input.js'], | |
| 31 | sourcesContent: ['content of input.js'], | |
| 32 | names: ['foo'], | |
| 33 | mappings: 'KAyCIA', | |
| 34 | ignoreList: [], | |
| 35 | }); | |
| 36 | ||
| 37 | // Lines start at line 1, columns at column 0. | |
| 38 | const traced = originalPositionFor(tracer, { line: 1, column: 5 }); | |
| 39 | assert.deepEqual(traced, { | |
| 40 | source: 'input.js', | |
| 41 | line: 42, | |
| 42 | column: 4, | |
| 43 | name: 'foo', | |
| 44 | }); | |
| 45 | ||
| 46 | const content = sourceContentFor(tracer, traced.source); | |
| 47 | assert.strictEqual(content, 'content for input.js'); | |
| 48 | ||
| 49 | const generated = generatedPositionFor(tracer, { | |
| 50 | source: 'input.js', | |
| 51 | line: 42, | |
| 52 | column: 4, | |
| 53 | }); | |
| 54 | assert.deepEqual(generated, { | |
| 55 | line: 1, | |
| 56 | column: 5, | |
| 57 | }); | |
| 58 | ||
| 59 | const ignored = isIgnored(tracer, 'input.js'); | |
| 60 | assert.equal(ignored, false); | |
| 61 | ``` | |
| 62 | ||
| 63 | We also provide a lower level API to get the actual segment that matches our line and column. Unlike | |
| 64 | `originalPositionFor`, `traceSegment` uses a 0-base for `line`: | |
| 65 | ||
| 66 | ```typescript | |
| 67 | import { traceSegment } from '@jridgewell/trace-mapping'; | |
| 68 | ||
| 69 | // line is 0-base. | |
| 70 | const traced = traceSegment(tracer, /* line */ 0, /* column */ 5); | |
| 71 | ||
| 72 | // Segments are [outputColumn, sourcesIndex, sourceLine, sourceColumn, namesIndex] | |
| 73 | // Again, line is 0-base and so is sourceLine | |
| 74 | assert.deepEqual(traced, [5, 0, 41, 4, 0]); | |
| 75 | ``` | |
| 76 | ||
| 77 | ### SectionedSourceMaps | |
| 78 | ||
| 79 | The sourcemap spec defines a special `sections` field that's designed to handle concatenation of | |
| 80 | output code with associated sourcemaps. This type of sourcemap is rarely used (no major build tool | |
| 81 | produces it), but if you are hand coding a concatenation you may need it. We provide an `AnyMap` | |
| 82 | helper that can receive either a regular sourcemap or a `SectionedSourceMap` and returns a | |
| 83 | `TraceMap` instance: | |
| 84 | ||
| 85 | ```typescript | |
| 86 | import { AnyMap } from '@jridgewell/trace-mapping'; | |
| 87 | const fooOutput = 'foo'; | |
| 88 | const barOutput = 'bar'; | |
| 89 | const output = [fooOutput, barOutput].join('\n'); | |
| 90 | ||
| 91 | const sectioned = new AnyMap({ | |
| 92 | version: 3, | |
| 93 | sections: [ | |
| 94 | { | |
| 95 | // 0-base line and column | |
| 96 | offset: { line: 0, column: 0 }, | |
| 97 | // fooOutput's sourcemap | |
| 98 | map: { | |
| 99 | version: 3, | |
| 100 | sources: ['foo.js'], | |
| 101 | names: ['foo'], | |
| 102 | mappings: 'AAAAA', | |
| 103 | }, | |
| 104 | }, | |
| 105 | { | |
| 106 | // barOutput's sourcemap will not affect the first line, only the second | |
| 107 | offset: { line: 1, column: 0 }, | |
| 108 | map: { | |
| 109 | version: 3, | |
| 110 | sources: ['bar.js'], | |
| 111 | names: ['bar'], | |
| 112 | mappings: 'AAAAA', | |
| 113 | }, | |
| 114 | }, | |
| 115 | ], | |
| 116 | }); | |
| 117 | ||
| 118 | const traced = originalPositionFor(sectioned, { | |
| 119 | line: 2, | |
| 120 | column: 0, | |
| 121 | }); | |
| 122 | ||
| 123 | assert.deepEqual(traced, { | |
| 124 | source: 'bar.js', | |
| 125 | line: 1, | |
| 126 | column: 0, | |
| 127 | name: 'bar', | |
| 128 | }); | |
| 129 | ``` | |
| 130 | ||
| 131 | ## Benchmarks | |
| 132 | ||
| 133 | ``` | |
| 134 | node v20.10.0 | |
| 135 | ||
| 136 | amp.js.map - 45120 segments | |
| 137 | ||
| 138 | Memory Usage: | |
| 139 | trace-mapping decoded 414164 bytes | |
| 140 | trace-mapping encoded 6274352 bytes | |
| 141 | source-map-js 10968904 bytes | |
| 142 | source-map-0.6.1 17587160 bytes | |
| 143 | source-map-0.8.0 8812155 bytes | |
| 144 | Chrome dev tools 8672912 bytes | |
| 145 | Smallest memory usage is trace-mapping decoded | |
| 146 | ||
| 147 | Init speed: | |
| 148 | trace-mapping: decoded JSON input x 205 ops/sec ±0.19% (88 runs sampled) | |
| 149 | trace-mapping: encoded JSON input x 405 ops/sec ±1.47% (88 runs sampled) | |
| 150 | trace-mapping: decoded Object input x 4,645 ops/sec ±0.15% (98 runs sampled) | |
| 151 | trace-mapping: encoded Object input x 458 ops/sec ±1.63% (91 runs sampled) | |
| 152 | source-map-js: encoded Object input x 75.48 ops/sec ±1.64% (67 runs sampled) | |
| 153 | source-map-0.6.1: encoded Object input x 39.37 ops/sec ±1.44% (53 runs sampled) | |
| 154 | Chrome dev tools: encoded Object input x 150 ops/sec ±1.76% (79 runs sampled) | |
| 155 | Fastest is trace-mapping: decoded Object input | |
| 156 | ||
| 157 | Trace speed (random): | |
| 158 | trace-mapping: decoded originalPositionFor x 44,946 ops/sec ±0.16% (99 runs sampled) | |
| 159 | trace-mapping: encoded originalPositionFor x 37,995 ops/sec ±1.81% (89 runs sampled) | |
| 160 | source-map-js: encoded originalPositionFor x 9,230 ops/sec ±1.36% (93 runs sampled) | |
| 161 | source-map-0.6.1: encoded originalPositionFor x 8,057 ops/sec ±0.84% (96 runs sampled) | |
| 162 | source-map-0.8.0: encoded originalPositionFor x 28,198 ops/sec ±1.12% (91 runs sampled) | |
| 163 | Chrome dev tools: encoded originalPositionFor x 46,276 ops/sec ±1.35% (95 runs sampled) | |
| 164 | Fastest is Chrome dev tools: encoded originalPositionFor | |
| 165 | ||
| 166 | Trace speed (ascending): | |
| 167 | trace-mapping: decoded originalPositionFor x 204,406 ops/sec ±0.19% (97 runs sampled) | |
| 168 | trace-mapping: encoded originalPositionFor x 196,695 ops/sec ±0.24% (99 runs sampled) | |
| 169 | source-map-js: encoded originalPositionFor x 11,948 ops/sec ±0.94% (99 runs sampled) | |
| 170 | source-map-0.6.1: encoded originalPositionFor x 10,730 ops/sec ±0.36% (100 runs sampled) | |
| 171 | source-map-0.8.0: encoded originalPositionFor x 51,427 ops/sec ±0.21% (98 runs sampled) | |
| 172 | Chrome dev tools: encoded originalPositionFor x 162,615 ops/sec ±0.18% (98 runs sampled) | |
| 173 | Fastest is trace-mapping: decoded originalPositionFor | |
| 174 | ||
| 175 | ||
| 176 | *** | |
| 177 | ||
| 178 | ||
| 179 | babel.min.js.map - 347793 segments | |
| 180 | ||
| 181 | Memory Usage: | |
| 182 | trace-mapping decoded 18504 bytes | |
| 183 | trace-mapping encoded 35428008 bytes | |
| 184 | source-map-js 51676808 bytes | |
| 185 | source-map-0.6.1 63367136 bytes | |
| 186 | source-map-0.8.0 43158400 bytes | |
| 187 | Chrome dev tools 50721552 bytes | |
| 188 | Smallest memory usage is trace-mapping decoded | |
| 189 | ||
| 190 | Init speed: | |
| 191 | trace-mapping: decoded JSON input x 17.82 ops/sec ±6.35% (35 runs sampled) | |
| 192 | trace-mapping: encoded JSON input x 31.57 ops/sec ±7.50% (43 runs sampled) | |
| 193 | trace-mapping: decoded Object input x 867 ops/sec ±0.74% (94 runs sampled) | |
| 194 | trace-mapping: encoded Object input x 33.83 ops/sec ±7.66% (46 runs sampled) | |
| 195 | source-map-js: encoded Object input x 6.58 ops/sec ±3.31% (20 runs sampled) | |
| 196 | source-map-0.6.1: encoded Object input x 4.23 ops/sec ±3.43% (15 runs sampled) | |
| 197 | Chrome dev tools: encoded Object input x 22.14 ops/sec ±3.79% (41 runs sampled) | |
| 198 | Fastest is trace-mapping: decoded Object input | |
| 199 | ||
| 200 | Trace speed (random): | |
| 201 | trace-mapping: decoded originalPositionFor x 78,234 ops/sec ±1.48% (29 runs sampled) | |
| 202 | trace-mapping: encoded originalPositionFor x 60,761 ops/sec ±1.35% (21 runs sampled) | |
| 203 | source-map-js: encoded originalPositionFor x 51,448 ops/sec ±2.17% (89 runs sampled) | |
| 204 | source-map-0.6.1: encoded originalPositionFor x 47,221 ops/sec ±1.99% (15 runs sampled) | |
| 205 | source-map-0.8.0: encoded originalPositionFor x 84,002 ops/sec ±1.45% (27 runs sampled) | |
| 206 | Chrome dev tools: encoded originalPositionFor x 106,457 ops/sec ±1.38% (37 runs sampled) | |
| 207 | Fastest is Chrome dev tools: encoded originalPositionFor | |
| 208 | ||
| 209 | Trace speed (ascending): | |
| 210 | trace-mapping: decoded originalPositionFor x 930,943 ops/sec ±0.25% (99 runs sampled) | |
| 211 | trace-mapping: encoded originalPositionFor x 843,545 ops/sec ±0.34% (97 runs sampled) | |
| 212 | source-map-js: encoded originalPositionFor x 114,510 ops/sec ±1.37% (36 runs sampled) | |
| 213 | source-map-0.6.1: encoded originalPositionFor x 87,412 ops/sec ±0.72% (92 runs sampled) | |
| 214 | source-map-0.8.0: encoded originalPositionFor x 197,709 ops/sec ±0.89% (59 runs sampled) | |
| 215 | Chrome dev tools: encoded originalPositionFor x 688,983 ops/sec ±0.33% (98 runs sampled) | |
| 216 | Fastest is trace-mapping: decoded originalPositionFor | |
| 217 | ||
| 218 | ||
| 219 | *** | |
| 220 | ||
| 221 | ||
| 222 | preact.js.map - 1992 segments | |
| 223 | ||
| 224 | Memory Usage: | |
| 225 | trace-mapping decoded 33136 bytes | |
| 226 | trace-mapping encoded 254240 bytes | |
| 227 | source-map-js 837488 bytes | |
| 228 | source-map-0.6.1 961928 bytes | |
| 229 | source-map-0.8.0 54384 bytes | |
| 230 | Chrome dev tools 709680 bytes | |
| 231 | Smallest memory usage is trace-mapping decoded | |
| 232 | ||
| 233 | Init speed: | |
| 234 | trace-mapping: decoded JSON input x 3,709 ops/sec ±0.13% (99 runs sampled) | |
| 235 | trace-mapping: encoded JSON input x 6,447 ops/sec ±0.22% (101 runs sampled) | |
| 236 | trace-mapping: decoded Object input x 83,062 ops/sec ±0.23% (100 runs sampled) | |
| 237 | trace-mapping: encoded Object input x 14,980 ops/sec ±0.28% (100 runs sampled) | |
| 238 | source-map-js: encoded Object input x 2,544 ops/sec ±0.16% (99 runs sampled) | |
| 239 | source-map-0.6.1: encoded Object input x 1,221 ops/sec ±0.37% (97 runs sampled) | |
| 240 | Chrome dev tools: encoded Object input x 4,241 ops/sec ±0.39% (93 runs sampled) | |
| 241 | Fastest is trace-mapping: decoded Object input | |
| 242 | ||
| 243 | Trace speed (random): | |
| 244 | trace-mapping: decoded originalPositionFor x 91,028 ops/sec ±0.14% (94 runs sampled) | |
| 245 | trace-mapping: encoded originalPositionFor x 84,348 ops/sec ±0.26% (98 runs sampled) | |
| 246 | source-map-js: encoded originalPositionFor x 26,998 ops/sec ±0.23% (98 runs sampled) | |
| 247 | source-map-0.6.1: encoded originalPositionFor x 18,049 ops/sec ±0.26% (100 runs sampled) | |
| 248 | source-map-0.8.0: encoded originalPositionFor x 41,916 ops/sec ±0.28% (98 runs sampled) | |
| 249 | Chrome dev tools: encoded originalPositionFor x 88,616 ops/sec ±0.14% (98 runs sampled) | |
| 250 | Fastest is trace-mapping: decoded originalPositionFor | |
| 251 | ||
| 252 | Trace speed (ascending): | |
| 253 | trace-mapping: decoded originalPositionFor x 319,960 ops/sec ±0.16% (100 runs sampled) | |
| 254 | trace-mapping: encoded originalPositionFor x 302,153 ops/sec ±0.18% (100 runs sampled) | |
| 255 | source-map-js: encoded originalPositionFor x 35,574 ops/sec ±0.19% (100 runs sampled) | |
| 256 | source-map-0.6.1: encoded originalPositionFor x 19,943 ops/sec ±0.12% (101 runs sampled) | |
| 257 | source-map-0.8.0: encoded originalPositionFor x 54,648 ops/sec ±0.20% (99 runs sampled) | |
| 258 | Chrome dev tools: encoded originalPositionFor x 278,319 ops/sec ±0.17% (102 runs sampled) | |
| 259 | Fastest is trace-mapping: decoded originalPositionFor | |
| 260 | ||
| 261 | ||
| 262 | *** | |
| 263 | ||
| 264 | ||
| 265 | react.js.map - 5726 segments | |
| 266 | ||
| 267 | Memory Usage: | |
| 268 | trace-mapping decoded 10872 bytes | |
| 269 | trace-mapping encoded 681512 bytes | |
| 270 | source-map-js 2563944 bytes | |
| 271 | source-map-0.6.1 2150864 bytes | |
| 272 | source-map-0.8.0 88680 bytes | |
| 273 | Chrome dev tools 1149576 bytes | |
| 274 | Smallest memory usage is trace-mapping decoded | |
| 275 | ||
| 276 | Init speed: | |
| 277 | trace-mapping: decoded JSON input x 1,887 ops/sec ±0.28% (99 runs sampled) | |
| 278 | trace-mapping: encoded JSON input x 4,749 ops/sec ±0.48% (97 runs sampled) | |
| 279 | trace-mapping: decoded Object input x 74,236 ops/sec ±0.11% (99 runs sampled) | |
| 280 | trace-mapping: encoded Object input x 5,752 ops/sec ±0.38% (100 runs sampled) | |
| 281 | source-map-js: encoded Object input x 806 ops/sec ±0.19% (97 runs sampled) | |
| 282 | source-map-0.6.1: encoded Object input x 418 ops/sec ±0.33% (94 runs sampled) | |
| 283 | Chrome dev tools: encoded Object input x 1,524 ops/sec ±0.57% (92 runs sampled) | |
| 284 | Fastest is trace-mapping: decoded Object input | |
| 285 | ||
| 286 | Trace speed (random): | |
| 287 | trace-mapping: decoded originalPositionFor x 620,201 ops/sec ±0.33% (96 runs sampled) | |
| 288 | trace-mapping: encoded originalPositionFor x 579,548 ops/sec ±0.35% (97 runs sampled) | |
| 289 | source-map-js: encoded originalPositionFor x 230,983 ops/sec ±0.62% (54 runs sampled) | |
| 290 | source-map-0.6.1: encoded originalPositionFor x 158,145 ops/sec ±0.80% (46 runs sampled) | |
| 291 | source-map-0.8.0: encoded originalPositionFor x 343,801 ops/sec ±0.55% (96 runs sampled) | |
| 292 | Chrome dev tools: encoded originalPositionFor x 659,649 ops/sec ±0.49% (98 runs sampled) | |
| 293 | Fastest is Chrome dev tools: encoded originalPositionFor | |
| 294 | ||
| 295 | Trace speed (ascending): | |
| 296 | trace-mapping: decoded originalPositionFor x 2,368,079 ops/sec ±0.32% (98 runs sampled) | |
| 297 | trace-mapping: encoded originalPositionFor x 2,134,039 ops/sec ±2.72% (87 runs sampled) | |
| 298 | source-map-js: encoded originalPositionFor x 290,120 ops/sec ±2.49% (82 runs sampled) | |
| 299 | source-map-0.6.1: encoded originalPositionFor x 187,613 ops/sec ±0.86% (49 runs sampled) | |
| 300 | source-map-0.8.0: encoded originalPositionFor x 479,569 ops/sec ±0.65% (96 runs sampled) | |
| 301 | Chrome dev tools: encoded originalPositionFor x 2,048,414 ops/sec ±0.24% (98 runs sampled) | |
| 302 | Fastest is trace-mapping: decoded originalPositionFor | |
| 303 | ||
| 304 | ||
| 305 | *** | |
| 306 | ||
| 307 | ||
| 308 | vscode.map - 2141001 segments | |
| 309 | ||
| 310 | Memory Usage: | |
| 311 | trace-mapping decoded 5206584 bytes | |
| 312 | trace-mapping encoded 208370336 bytes | |
| 313 | source-map-js 278493008 bytes | |
| 314 | source-map-0.6.1 391564048 bytes | |
| 315 | source-map-0.8.0 257508787 bytes | |
| 316 | Chrome dev tools 291053000 bytes | |
| 317 | Smallest memory usage is trace-mapping decoded | |
| 318 | ||
| 319 | Init speed: | |
| 320 | trace-mapping: decoded JSON input x 1.63 ops/sec ±33.88% (9 runs sampled) | |
| 321 | trace-mapping: encoded JSON input x 3.29 ops/sec ±36.13% (13 runs sampled) | |
| 322 | trace-mapping: decoded Object input x 103 ops/sec ±0.93% (77 runs sampled) | |
| 323 | trace-mapping: encoded Object input x 5.42 ops/sec ±28.54% (19 runs sampled) | |
| 324 | source-map-js: encoded Object input x 1.07 ops/sec ±13.84% (7 runs sampled) | |
| 325 | source-map-0.6.1: encoded Object input x 0.60 ops/sec ±2.43% (6 runs sampled) | |
| 326 | Chrome dev tools: encoded Object input x 2.61 ops/sec ±22.00% (11 runs sampled) | |
| 327 | Fastest is trace-mapping: decoded Object input | |
| 328 | ||
| 329 | Trace speed (random): | |
| 330 | trace-mapping: decoded originalPositionFor x 257,019 ops/sec ±0.97% (93 runs sampled) | |
| 331 | trace-mapping: encoded originalPositionFor x 179,163 ops/sec ±0.83% (92 runs sampled) | |
| 332 | source-map-js: encoded originalPositionFor x 73,337 ops/sec ±1.35% (87 runs sampled) | |
| 333 | source-map-0.6.1: encoded originalPositionFor x 38,797 ops/sec ±1.66% (88 runs sampled) | |
| 334 | source-map-0.8.0: encoded originalPositionFor x 107,758 ops/sec ±1.94% (45 runs sampled) | |
| 335 | Chrome dev tools: encoded originalPositionFor x 188,550 ops/sec ±1.85% (79 runs sampled) | |
| 336 | Fastest is trace-mapping: decoded originalPositionFor | |
| 337 | ||
| 338 | Trace speed (ascending): | |
| 339 | trace-mapping: decoded originalPositionFor x 447,621 ops/sec ±3.64% (94 runs sampled) | |
| 340 | trace-mapping: encoded originalPositionFor x 323,698 ops/sec ±5.20% (88 runs sampled) | |
| 341 | source-map-js: encoded originalPositionFor x 78,387 ops/sec ±1.69% (89 runs sampled) | |
| 342 | source-map-0.6.1: encoded originalPositionFor x 41,016 ops/sec ±3.01% (25 runs sampled) | |
| 343 | source-map-0.8.0: encoded originalPositionFor x 124,204 ops/sec ±0.90% (92 runs sampled) | |
| 344 | Chrome dev tools: encoded originalPositionFor x 230,087 ops/sec ±2.61% (93 runs sampled) | |
| 345 | Fastest is trace-mapping: decoded originalPositionFor | |
| 346 | ``` | |
| 347 | ||
| 348 | [source-map]: https://www.npmjs.com/package/source-map |
| … | … | @@ -0,0 +1,493 @@ |
| 1 | // src/trace-mapping.ts | |
| 2 | import { encode, decode } from "@jridgewell/sourcemap-codec"; | |
| 3 | ||
| 4 | // src/resolve.ts | |
| 5 | import resolveUri from "@jridgewell/resolve-uri"; | |
| 6 | ||
| 7 | // src/strip-filename.ts | |
| 8 | function stripFilename(path) { | |
| 9 | if (!path) return ""; | |
| 10 | const index = path.lastIndexOf("/"); | |
| 11 | return path.slice(0, index + 1); | |
| 12 | } | |
| 13 | ||
| 14 | // src/resolve.ts | |
| 15 | function resolver(mapUrl, sourceRoot) { | |
| 16 | const from = stripFilename(mapUrl); | |
| 17 | const prefix = sourceRoot ? sourceRoot + "/" : ""; | |
| 18 | return (source) => resolveUri(prefix + (source || ""), from); | |
| 19 | } | |
| 20 | ||
| 21 | // src/sourcemap-segment.ts | |
| 22 | var COLUMN = 0; | |
| 23 | var SOURCES_INDEX = 1; | |
| 24 | var SOURCE_LINE = 2; | |
| 25 | var SOURCE_COLUMN = 3; | |
| 26 | var NAMES_INDEX = 4; | |
| 27 | var REV_GENERATED_LINE = 1; | |
| 28 | var REV_GENERATED_COLUMN = 2; | |
| 29 | ||
| 30 | // src/sort.ts | |
| 31 | function maybeSort(mappings, owned) { | |
| 32 | const unsortedIndex = nextUnsortedSegmentLine(mappings, 0); | |
| 33 | if (unsortedIndex === mappings.length) return mappings; | |
| 34 | if (!owned) mappings = mappings.slice(); | |
| 35 | for (let i = unsortedIndex; i < mappings.length; i = nextUnsortedSegmentLine(mappings, i + 1)) { | |
| 36 | mappings[i] = sortSegments(mappings[i], owned); | |
| 37 | } | |
| 38 | return mappings; | |
| 39 | } | |
| 40 | function nextUnsortedSegmentLine(mappings, start) { | |
| 41 | for (let i = start; i < mappings.length; i++) { | |
| 42 | if (!isSorted(mappings[i])) return i; | |
| 43 | } | |
| 44 | return mappings.length; | |
| 45 | } | |
| 46 | function isSorted(line) { | |
| 47 | for (let j = 1; j < line.length; j++) { | |
| 48 | if (line[j][COLUMN] < line[j - 1][COLUMN]) { | |
| 49 | return false; | |
| 50 | } | |
| 51 | } | |
| 52 | return true; | |
| 53 | } | |
| 54 | function sortSegments(line, owned) { | |
| 55 | if (!owned) line = line.slice(); | |
| 56 | return line.sort(sortComparator); | |
| 57 | } | |
| 58 | function sortComparator(a, b) { | |
| 59 | return a[COLUMN] - b[COLUMN]; | |
| 60 | } | |
| 61 | ||
| 62 | // src/by-source.ts | |
| 63 | function buildBySources(decoded, memos) { | |
| 64 | const sources = memos.map(() => []); | |
| 65 | for (let i = 0; i < decoded.length; i++) { | |
| 66 | const line = decoded[i]; | |
| 67 | for (let j = 0; j < line.length; j++) { | |
| 68 | const seg = line[j]; | |
| 69 | if (seg.length === 1) continue; | |
| 70 | const sourceIndex2 = seg[SOURCES_INDEX]; | |
| 71 | const sourceLine = seg[SOURCE_LINE]; | |
| 72 | const sourceColumn = seg[SOURCE_COLUMN]; | |
| 73 | const source = sources[sourceIndex2]; | |
| 74 | const segs = source[sourceLine] || (source[sourceLine] = []); | |
| 75 | segs.push([sourceColumn, i, seg[COLUMN]]); | |
| 76 | } | |
| 77 | } | |
| 78 | for (let i = 0; i < sources.length; i++) { | |
| 79 | const source = sources[i]; | |
| 80 | for (let j = 0; j < source.length; j++) { | |
| 81 | const line = source[j]; | |
| 82 | if (line) line.sort(sortComparator); | |
| 83 | } | |
| 84 | } | |
| 85 | return sources; | |
| 86 | } | |
| 87 | ||
| 88 | // src/binary-search.ts | |
| 89 | var found = false; | |
| 90 | function binarySearch(haystack, needle, low, high) { | |
| 91 | while (low <= high) { | |
| 92 | const mid = low + (high - low >> 1); | |
| 93 | const cmp = haystack[mid][COLUMN] - needle; | |
| 94 | if (cmp === 0) { | |
| 95 | found = true; | |
| 96 | return mid; | |
| 97 | } | |
| 98 | if (cmp < 0) { | |
| 99 | low = mid + 1; | |
| 100 | } else { | |
| 101 | high = mid - 1; | |
| 102 | } | |
| 103 | } | |
| 104 | found = false; | |
| 105 | return low - 1; | |
| 106 | } | |
| 107 | function upperBound(haystack, needle, index) { | |
| 108 | for (let i = index + 1; i < haystack.length; index = i++) { | |
| 109 | if (haystack[i][COLUMN] !== needle) break; | |
| 110 | } | |
| 111 | return index; | |
| 112 | } | |
| 113 | function lowerBound(haystack, needle, index) { | |
| 114 | for (let i = index - 1; i >= 0; index = i--) { | |
| 115 | if (haystack[i][COLUMN] !== needle) break; | |
| 116 | } | |
| 117 | return index; | |
| 118 | } | |
| 119 | function memoizedState() { | |
| 120 | return { | |
| 121 | lastKey: -1, | |
| 122 | lastNeedle: -1, | |
| 123 | lastIndex: -1 | |
| 124 | }; | |
| 125 | } | |
| 126 | function memoizedBinarySearch(haystack, needle, state, key) { | |
| 127 | const { lastKey, lastNeedle, lastIndex } = state; | |
| 128 | let low = 0; | |
| 129 | let high = haystack.length - 1; | |
| 130 | if (key === lastKey) { | |
| 131 | if (needle === lastNeedle) { | |
| 132 | found = lastIndex !== -1 && haystack[lastIndex][COLUMN] === needle; | |
| 133 | return lastIndex; | |
| 134 | } | |
| 135 | if (needle >= lastNeedle) { | |
| 136 | low = lastIndex === -1 ? 0 : lastIndex; | |
| 137 | } else { | |
| 138 | high = lastIndex; | |
| 139 | } | |
| 140 | } | |
| 141 | state.lastKey = key; | |
| 142 | state.lastNeedle = needle; | |
| 143 | return state.lastIndex = binarySearch(haystack, needle, low, high); | |
| 144 | } | |
| 145 | ||
| 146 | // src/types.ts | |
| 147 | function parse(map) { | |
| 148 | return typeof map === "string" ? JSON.parse(map) : map; | |
| 149 | } | |
| 150 | ||
| 151 | // src/flatten-map.ts | |
| 152 | var FlattenMap = function(map, mapUrl) { | |
| 153 | const parsed = parse(map); | |
| 154 | if (!("sections" in parsed)) { | |
| 155 | return new TraceMap(parsed, mapUrl); | |
| 156 | } | |
| 157 | const mappings = []; | |
| 158 | const sources = []; | |
| 159 | const sourcesContent = []; | |
| 160 | const names = []; | |
| 161 | const ignoreList = []; | |
| 162 | recurse( | |
| 163 | parsed, | |
| 164 | mapUrl, | |
| 165 | mappings, | |
| 166 | sources, | |
| 167 | sourcesContent, | |
| 168 | names, | |
| 169 | ignoreList, | |
| 170 | 0, | |
| 171 | 0, | |
| 172 | Infinity, | |
| 173 | Infinity | |
| 174 | ); | |
| 175 | const joined = { | |
| 176 | version: 3, | |
| 177 | file: parsed.file, | |
| 178 | names, | |
| 179 | sources, | |
| 180 | sourcesContent, | |
| 181 | mappings, | |
| 182 | ignoreList | |
| 183 | }; | |
| 184 | return presortedDecodedMap(joined); | |
| 185 | }; | |
| 186 | function recurse(input, mapUrl, mappings, sources, sourcesContent, names, ignoreList, lineOffset, columnOffset, stopLine, stopColumn) { | |
| 187 | const { sections } = input; | |
| 188 | for (let i = 0; i < sections.length; i++) { | |
| 189 | const { map, offset } = sections[i]; | |
| 190 | let sl = stopLine; | |
| 191 | let sc = stopColumn; | |
| 192 | if (i + 1 < sections.length) { | |
| 193 | const nextOffset = sections[i + 1].offset; | |
| 194 | sl = Math.min(stopLine, lineOffset + nextOffset.line); | |
| 195 | if (sl === stopLine) { | |
| 196 | sc = Math.min(stopColumn, columnOffset + nextOffset.column); | |
| 197 | } else if (sl < stopLine) { | |
| 198 | sc = columnOffset + nextOffset.column; | |
| 199 | } | |
| 200 | } | |
| 201 | addSection( | |
| 202 | map, | |
| 203 | mapUrl, | |
| 204 | mappings, | |
| 205 | sources, | |
| 206 | sourcesContent, | |
| 207 | names, | |
| 208 | ignoreList, | |
| 209 | lineOffset + offset.line, | |
| 210 | columnOffset + offset.column, | |
| 211 | sl, | |
| 212 | sc | |
| 213 | ); | |
| 214 | } | |
| 215 | } | |
| 216 | function addSection(input, mapUrl, mappings, sources, sourcesContent, names, ignoreList, lineOffset, columnOffset, stopLine, stopColumn) { | |
| 217 | const parsed = parse(input); | |
| 218 | if ("sections" in parsed) return recurse(...arguments); | |
| 219 | const map = new TraceMap(parsed, mapUrl); | |
| 220 | const sourcesOffset = sources.length; | |
| 221 | const namesOffset = names.length; | |
| 222 | const decoded = decodedMappings(map); | |
| 223 | const { resolvedSources, sourcesContent: contents, ignoreList: ignores } = map; | |
| 224 | append(sources, resolvedSources); | |
| 225 | append(names, map.names); | |
| 226 | if (contents) append(sourcesContent, contents); | |
| 227 | else for (let i = 0; i < resolvedSources.length; i++) sourcesContent.push(null); | |
| 228 | if (ignores) for (let i = 0; i < ignores.length; i++) ignoreList.push(ignores[i] + sourcesOffset); | |
| 229 | for (let i = 0; i < decoded.length; i++) { | |
| 230 | const lineI = lineOffset + i; | |
| 231 | if (lineI > stopLine) return; | |
| 232 | const out = getLine(mappings, lineI); | |
| 233 | const cOffset = i === 0 ? columnOffset : 0; | |
| 234 | const line = decoded[i]; | |
| 235 | for (let j = 0; j < line.length; j++) { | |
| 236 | const seg = line[j]; | |
| 237 | const column = cOffset + seg[COLUMN]; | |
| 238 | if (lineI === stopLine && column >= stopColumn) return; | |
| 239 | if (seg.length === 1) { | |
| 240 | out.push([column]); | |
| 241 | continue; | |
| 242 | } | |
| 243 | const sourcesIndex = sourcesOffset + seg[SOURCES_INDEX]; | |
| 244 | const sourceLine = seg[SOURCE_LINE]; | |
| 245 | const sourceColumn = seg[SOURCE_COLUMN]; | |
| 246 | out.push( | |
| 247 | seg.length === 4 ? [column, sourcesIndex, sourceLine, sourceColumn] : [column, sourcesIndex, sourceLine, sourceColumn, namesOffset + seg[NAMES_INDEX]] | |
| 248 | ); | |
| 249 | } | |
| 250 | } | |
| 251 | } | |
| 252 | function append(arr, other) { | |
| 253 | for (let i = 0; i < other.length; i++) arr.push(other[i]); | |
| 254 | } | |
| 255 | function getLine(arr, index) { | |
| 256 | for (let i = arr.length; i <= index; i++) arr[i] = []; | |
| 257 | return arr[index]; | |
| 258 | } | |
| 259 | ||
| 260 | // src/trace-mapping.ts | |
| 261 | var LINE_GTR_ZERO = "`line` must be greater than 0 (lines start at line 1)"; | |
| 262 | var COL_GTR_EQ_ZERO = "`column` must be greater than or equal to 0 (columns start at column 0)"; | |
| 263 | var LEAST_UPPER_BOUND = -1; | |
| 264 | var GREATEST_LOWER_BOUND = 1; | |
| 265 | var TraceMap = class { | |
| 266 | constructor(map, mapUrl) { | |
| 267 | const isString = typeof map === "string"; | |
| 268 | if (!isString && map._decodedMemo) return map; | |
| 269 | const parsed = parse(map); | |
| 270 | const { version, file, names, sourceRoot, sources, sourcesContent } = parsed; | |
| 271 | this.version = version; | |
| 272 | this.file = file; | |
| 273 | this.names = names || []; | |
| 274 | this.sourceRoot = sourceRoot; | |
| 275 | this.sources = sources; | |
| 276 | this.sourcesContent = sourcesContent; | |
| 277 | this.ignoreList = parsed.ignoreList || parsed.x_google_ignoreList || void 0; | |
| 278 | const resolve = resolver(mapUrl, sourceRoot); | |
| 279 | this.resolvedSources = sources.map(resolve); | |
| 280 | const { mappings } = parsed; | |
| 281 | if (typeof mappings === "string") { | |
| 282 | this._encoded = mappings; | |
| 283 | this._decoded = void 0; | |
| 284 | } else if (Array.isArray(mappings)) { | |
| 285 | this._encoded = void 0; | |
| 286 | this._decoded = maybeSort(mappings, isString); | |
| 287 | } else if (parsed.sections) { | |
| 288 | throw new Error(`TraceMap passed sectioned source map, please use FlattenMap export instead`); | |
| 289 | } else { | |
| 290 | throw new Error(`invalid source map: ${JSON.stringify(parsed)}`); | |
| 291 | } | |
| 292 | this._decodedMemo = memoizedState(); | |
| 293 | this._bySources = void 0; | |
| 294 | this._bySourceMemos = void 0; | |
| 295 | } | |
| 296 | }; | |
| 297 | function cast(map) { | |
| 298 | return map; | |
| 299 | } | |
| 300 | function encodedMappings(map) { | |
| 301 | var _a, _b; | |
| 302 | return (_b = (_a = cast(map))._encoded) != null ? _b : _a._encoded = encode(cast(map)._decoded); | |
| 303 | } | |
| 304 | function decodedMappings(map) { | |
| 305 | var _a; | |
| 306 | return (_a = cast(map))._decoded || (_a._decoded = decode(cast(map)._encoded)); | |
| 307 | } | |
| 308 | function traceSegment(map, line, column) { | |
| 309 | const decoded = decodedMappings(map); | |
| 310 | if (line >= decoded.length) return null; | |
| 311 | const segments = decoded[line]; | |
| 312 | const index = traceSegmentInternal( | |
| 313 | segments, | |
| 314 | cast(map)._decodedMemo, | |
| 315 | line, | |
| 316 | column, | |
| 317 | GREATEST_LOWER_BOUND | |
| 318 | ); | |
| 319 | return index === -1 ? null : segments[index]; | |
| 320 | } | |
| 321 | function originalPositionFor(map, needle) { | |
| 322 | let { line, column, bias } = needle; | |
| 323 | line--; | |
| 324 | if (line < 0) throw new Error(LINE_GTR_ZERO); | |
| 325 | if (column < 0) throw new Error(COL_GTR_EQ_ZERO); | |
| 326 | const decoded = decodedMappings(map); | |
| 327 | if (line >= decoded.length) return OMapping(null, null, null, null); | |
| 328 | const segments = decoded[line]; | |
| 329 | const index = traceSegmentInternal( | |
| 330 | segments, | |
| 331 | cast(map)._decodedMemo, | |
| 332 | line, | |
| 333 | column, | |
| 334 | bias || GREATEST_LOWER_BOUND | |
| 335 | ); | |
| 336 | if (index === -1) return OMapping(null, null, null, null); | |
| 337 | const segment = segments[index]; | |
| 338 | if (segment.length === 1) return OMapping(null, null, null, null); | |
| 339 | const { names, resolvedSources } = map; | |
| 340 | return OMapping( | |
| 341 | resolvedSources[segment[SOURCES_INDEX]], | |
| 342 | segment[SOURCE_LINE] + 1, | |
| 343 | segment[SOURCE_COLUMN], | |
| 344 | segment.length === 5 ? names[segment[NAMES_INDEX]] : null | |
| 345 | ); | |
| 346 | } | |
| 347 | function generatedPositionFor(map, needle) { | |
| 348 | const { source, line, column, bias } = needle; | |
| 349 | return generatedPosition(map, source, line, column, bias || GREATEST_LOWER_BOUND, false); | |
| 350 | } | |
| 351 | function allGeneratedPositionsFor(map, needle) { | |
| 352 | const { source, line, column, bias } = needle; | |
| 353 | return generatedPosition(map, source, line, column, bias || LEAST_UPPER_BOUND, true); | |
| 354 | } | |
| 355 | function eachMapping(map, cb) { | |
| 356 | const decoded = decodedMappings(map); | |
| 357 | const { names, resolvedSources } = map; | |
| 358 | for (let i = 0; i < decoded.length; i++) { | |
| 359 | const line = decoded[i]; | |
| 360 | for (let j = 0; j < line.length; j++) { | |
| 361 | const seg = line[j]; | |
| 362 | const generatedLine = i + 1; | |
| 363 | const generatedColumn = seg[0]; | |
| 364 | let source = null; | |
| 365 | let originalLine = null; | |
| 366 | let originalColumn = null; | |
| 367 | let name = null; | |
| 368 | if (seg.length !== 1) { | |
| 369 | source = resolvedSources[seg[1]]; | |
| 370 | originalLine = seg[2] + 1; | |
| 371 | originalColumn = seg[3]; | |
| 372 | } | |
| 373 | if (seg.length === 5) name = names[seg[4]]; | |
| 374 | cb({ | |
| 375 | generatedLine, | |
| 376 | generatedColumn, | |
| 377 | source, | |
| 378 | originalLine, | |
| 379 | originalColumn, | |
| 380 | name | |
| 381 | }); | |
| 382 | } | |
| 383 | } | |
| 384 | } | |
| 385 | function sourceIndex(map, source) { | |
| 386 | const { sources, resolvedSources } = map; | |
| 387 | let index = sources.indexOf(source); | |
| 388 | if (index === -1) index = resolvedSources.indexOf(source); | |
| 389 | return index; | |
| 390 | } | |
| 391 | function sourceContentFor(map, source) { | |
| 392 | const { sourcesContent } = map; | |
| 393 | if (sourcesContent == null) return null; | |
| 394 | const index = sourceIndex(map, source); | |
| 395 | return index === -1 ? null : sourcesContent[index]; | |
| 396 | } | |
| 397 | function isIgnored(map, source) { | |
| 398 | const { ignoreList } = map; | |
| 399 | if (ignoreList == null) return false; | |
| 400 | const index = sourceIndex(map, source); | |
| 401 | return index === -1 ? false : ignoreList.includes(index); | |
| 402 | } | |
| 403 | function presortedDecodedMap(map, mapUrl) { | |
| 404 | const tracer = new TraceMap(clone(map, []), mapUrl); | |
| 405 | cast(tracer)._decoded = map.mappings; | |
| 406 | return tracer; | |
| 407 | } | |
| 408 | function decodedMap(map) { | |
| 409 | return clone(map, decodedMappings(map)); | |
| 410 | } | |
| 411 | function encodedMap(map) { | |
| 412 | return clone(map, encodedMappings(map)); | |
| 413 | } | |
| 414 | function clone(map, mappings) { | |
| 415 | return { | |
| 416 | version: map.version, | |
| 417 | file: map.file, | |
| 418 | names: map.names, | |
| 419 | sourceRoot: map.sourceRoot, | |
| 420 | sources: map.sources, | |
| 421 | sourcesContent: map.sourcesContent, | |
| 422 | mappings, | |
| 423 | ignoreList: map.ignoreList || map.x_google_ignoreList | |
| 424 | }; | |
| 425 | } | |
| 426 | function OMapping(source, line, column, name) { | |
| 427 | return { source, line, column, name }; | |
| 428 | } | |
| 429 | function GMapping(line, column) { | |
| 430 | return { line, column }; | |
| 431 | } | |
| 432 | function traceSegmentInternal(segments, memo, line, column, bias) { | |
| 433 | let index = memoizedBinarySearch(segments, column, memo, line); | |
| 434 | if (found) { | |
| 435 | index = (bias === LEAST_UPPER_BOUND ? upperBound : lowerBound)(segments, column, index); | |
| 436 | } else if (bias === LEAST_UPPER_BOUND) index++; | |
| 437 | if (index === -1 || index === segments.length) return -1; | |
| 438 | return index; | |
| 439 | } | |
| 440 | function sliceGeneratedPositions(segments, memo, line, column, bias) { | |
| 441 | let min = traceSegmentInternal(segments, memo, line, column, GREATEST_LOWER_BOUND); | |
| 442 | if (!found && bias === LEAST_UPPER_BOUND) min++; | |
| 443 | if (min === -1 || min === segments.length) return []; | |
| 444 | const matchedColumn = found ? column : segments[min][COLUMN]; | |
| 445 | if (!found) min = lowerBound(segments, matchedColumn, min); | |
| 446 | const max = upperBound(segments, matchedColumn, min); | |
| 447 | const result = []; | |
| 448 | for (; min <= max; min++) { | |
| 449 | const segment = segments[min]; | |
| 450 | result.push(GMapping(segment[REV_GENERATED_LINE] + 1, segment[REV_GENERATED_COLUMN])); | |
| 451 | } | |
| 452 | return result; | |
| 453 | } | |
| 454 | function generatedPosition(map, source, line, column, bias, all) { | |
| 455 | var _a, _b; | |
| 456 | line--; | |
| 457 | if (line < 0) throw new Error(LINE_GTR_ZERO); | |
| 458 | if (column < 0) throw new Error(COL_GTR_EQ_ZERO); | |
| 459 | const { sources, resolvedSources } = map; | |
| 460 | let sourceIndex2 = sources.indexOf(source); | |
| 461 | if (sourceIndex2 === -1) sourceIndex2 = resolvedSources.indexOf(source); | |
| 462 | if (sourceIndex2 === -1) return all ? [] : GMapping(null, null); | |
| 463 | const bySourceMemos = (_a = cast(map))._bySourceMemos || (_a._bySourceMemos = sources.map(memoizedState)); | |
| 464 | const generated = (_b = cast(map))._bySources || (_b._bySources = buildBySources(decodedMappings(map), bySourceMemos)); | |
| 465 | const segments = generated[sourceIndex2][line]; | |
| 466 | if (segments == null) return all ? [] : GMapping(null, null); | |
| 467 | const memo = bySourceMemos[sourceIndex2]; | |
| 468 | if (all) return sliceGeneratedPositions(segments, memo, line, column, bias); | |
| 469 | const index = traceSegmentInternal(segments, memo, line, column, bias); | |
| 470 | if (index === -1) return GMapping(null, null); | |
| 471 | const segment = segments[index]; | |
| 472 | return GMapping(segment[REV_GENERATED_LINE] + 1, segment[REV_GENERATED_COLUMN]); | |
| 473 | } | |
| 474 | export { | |
| 475 | FlattenMap as AnyMap, | |
| 476 | FlattenMap, | |
| 477 | GREATEST_LOWER_BOUND, | |
| 478 | LEAST_UPPER_BOUND, | |
| 479 | TraceMap, | |
| 480 | allGeneratedPositionsFor, | |
| 481 | decodedMap, | |
| 482 | decodedMappings, | |
| 483 | eachMapping, | |
| 484 | encodedMap, | |
| 485 | encodedMappings, | |
| 486 | generatedPositionFor, | |
| 487 | isIgnored, | |
| 488 | originalPositionFor, | |
| 489 | presortedDecodedMap, | |
| 490 | sourceContentFor, | |
| 491 | traceSegment | |
| 492 | }; | |
| 493 | //# sourceMappingURL=trace-mapping.mjs.map |
| … | … | @@ -0,0 +1,6 @@ |
| 1 | { | |
| 2 | "version": 3, | |
| 3 | "sources": ["../src/trace-mapping.ts", "../src/resolve.ts", "../src/strip-filename.ts", "../src/sourcemap-segment.ts", "../src/sort.ts", "../src/by-source.ts", "../src/binary-search.ts", "../src/types.ts", "../src/flatten-map.ts"], | |
| 4 | "mappings": ";AAAA,SAAS,QAAQ,cAAc;;;ACA/B,OAAO,gBAAgB;;;ACGR,SAAR,cAA+B,MAAyC;AAC7E,MAAI,CAAC,KAAM,QAAO;AAClB,QAAM,QAAQ,KAAK,YAAY,GAAG;AAClC,SAAO,KAAK,MAAM,GAAG,QAAQ,CAAC;AAChC;;;ADHe,SAAR,SACL,QACA,YACS;AACT,QAAM,OAAO,cAAc,MAAM;AAIjC,QAAM,SAAS,aAAa,aAAa,MAAM;AAE/C,SAAO,CAAC,WAAW,WAAW,UAAU,UAAU,KAAK,IAAI;AAC7D;;;AEAO,IAAM,SAAS;AACf,IAAM,gBAAgB;AACtB,IAAM,cAAc;AACpB,IAAM,gBAAgB;AACtB,IAAM,cAAc;AAEpB,IAAM,qBAAqB;AAC3B,IAAM,uBAAuB;;;AClBrB,SAAR,UACL,UACA,OACsB;AACtB,QAAM,gBAAgB,wBAAwB,UAAU,CAAC;AACzD,MAAI,kBAAkB,SAAS,OAAQ,QAAO;AAI9C,MAAI,CAAC,MAAO,YAAW,SAAS,MAAM;AAEtC,WAAS,IAAI,eAAe,IAAI,SAAS,QAAQ,IAAI,wBAAwB,UAAU,IAAI,CAAC,GAAG;AAC7F,aAAS,CAAC,IAAI,aAAa,SAAS,CAAC,GAAG,KAAK;AAAA,EAC/C;AACA,SAAO;AACT;AAEA,SAAS,wBAAwB,UAAgC,OAAuB;AACtF,WAAS,IAAI,OAAO,IAAI,SAAS,QAAQ,KAAK;AAC5C,QAAI,CAAC,SAAS,SAAS,CAAC,CAAC,EAAG,QAAO;AAAA,EACrC;AACA,SAAO,SAAS;AAClB;AAEA,SAAS,SAAS,MAAmC;AACnD,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,QAAI,KAAK,CAAC,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC,EAAE,MAAM,GAAG;AACzC,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,aAAa,MAA0B,OAAoC;AAClF,MAAI,CAAC,MAAO,QAAO,KAAK,MAAM;AAC9B,SAAO,KAAK,KAAK,cAAc;AACjC;AAEO,SAAS,eAA4D,GAAM,GAAc;AAC9F,SAAO,EAAE,MAAM,IAAI,EAAE,MAAM;AAC7B;;;ACnCe,SAAR,eACL,SACA,OACU;AACV,QAAM,UAAoB,MAAM,IAAI,MAAM,CAAC,CAAC;AAE5C,WAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;AACvC,UAAM,OAAO,QAAQ,CAAC;AACtB,aAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,YAAM,MAAM,KAAK,CAAC;AAClB,UAAI,IAAI,WAAW,EAAG;AAEtB,YAAMA,eAAc,IAAI,aAAa;AACrC,YAAM,aAAa,IAAI,WAAW;AAClC,YAAM,eAAe,IAAI,aAAa;AAEtC,YAAM,SAAS,QAAQA,YAAW;AAClC,YAAM,OAAQ,4CAAuB,CAAC;AACtC,WAAK,KAAK,CAAC,cAAc,GAAG,IAAI,MAAM,CAAC,CAAC;AAAA,IAC1C;AAAA,EACF;AAEA,WAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;AACvC,UAAM,SAAS,QAAQ,CAAC;AACxB,aAAS,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK;AACtC,YAAM,OAAO,OAAO,CAAC;AACrB,UAAI,KAAM,MAAK,KAAK,cAAc;AAAA,IACpC;AAAA,EACF;AAEA,SAAO;AACT;;;AC/BO,IAAI,QAAQ;AAkBZ,SAAS,aACd,UACA,QACA,KACA,MACQ;AACR,SAAO,OAAO,MAAM;AAClB,UAAM,MAAM,OAAQ,OAAO,OAAQ;AACnC,UAAM,MAAM,SAAS,GAAG,EAAE,MAAM,IAAI;AAEpC,QAAI,QAAQ,GAAG;AACb,cAAQ;AACR,aAAO;AAAA,IACT;AAEA,QAAI,MAAM,GAAG;AACX,YAAM,MAAM;AAAA,IACd,OAAO;AACL,aAAO,MAAM;AAAA,IACf;AAAA,EACF;AAEA,UAAQ;AACR,SAAO,MAAM;AACf;AAEO,SAAS,WACd,UACA,QACA,OACQ;AACR,WAAS,IAAI,QAAQ,GAAG,IAAI,SAAS,QAAQ,QAAQ,KAAK;AACxD,QAAI,SAAS,CAAC,EAAE,MAAM,MAAM,OAAQ;AAAA,EACtC;AACA,SAAO;AACT;AAEO,SAAS,WACd,UACA,QACA,OACQ;AACR,WAAS,IAAI,QAAQ,GAAG,KAAK,GAAG,QAAQ,KAAK;AAC3C,QAAI,SAAS,CAAC,EAAE,MAAM,MAAM,OAAQ;AAAA,EACtC;AACA,SAAO;AACT;AAEO,SAAS,gBAA2B;AACzC,SAAO;AAAA,IACL,SAAS;AAAA,IACT,YAAY;AAAA,IACZ,WAAW;AAAA,EACb;AACF;AAMO,SAAS,qBACd,UACA,QACA,OACA,KACQ;AACR,QAAM,EAAE,SAAS,YAAY,UAAU,IAAI;AAE3C,MAAI,MAAM;AACV,MAAI,OAAO,SAAS,SAAS;AAC7B,MAAI,QAAQ,SAAS;AACnB,QAAI,WAAW,YAAY;AACzB,cAAQ,cAAc,MAAM,SAAS,SAAS,EAAE,MAAM,MAAM;AAC5D,aAAO;AAAA,IACT;AAEA,QAAI,UAAU,YAAY;AAExB,YAAM,cAAc,KAAK,IAAI;AAAA,IAC/B,OAAO;AACL,aAAO;AAAA,IACT;AAAA,EACF;AACA,QAAM,UAAU;AAChB,QAAM,aAAa;AAEnB,SAAQ,MAAM,YAAY,aAAa,UAAU,QAAQ,KAAK,IAAI;AACpE;;;ACHO,SAAS,MAAS,KAA4B;AACnD,SAAO,OAAO,QAAQ,WAAW,KAAK,MAAM,GAAG,IAAK;AACtD;;;ACvFO,IAAM,aAAyB,SAAU,KAAK,QAAQ;AAC3D,QAAM,SAAS,MAAM,GAA8B;AAEnD,MAAI,EAAE,cAAc,SAAS;AAC3B,WAAO,IAAI,SAAS,QAA2D,MAAM;AAAA,EACvF;AAEA,QAAM,WAAiC,CAAC;AACxC,QAAM,UAAoB,CAAC;AAC3B,QAAM,iBAAoC,CAAC;AAC3C,QAAM,QAAkB,CAAC;AACzB,QAAM,aAAuB,CAAC;AAE9B;AAAA,IACE;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,QAAM,SAA2B;AAAA,IAC/B,SAAS;AAAA,IACT,MAAM,OAAO;AAAA,IACb;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,SAAO,oBAAoB,MAAM;AACnC;AAEA,SAAS,QACP,OACA,QACA,UACA,SACA,gBACA,OACA,YACA,YACA,cACA,UACA,YACA;AACA,QAAM,EAAE,SAAS,IAAI;AACrB,WAAS,IAAI,GAAG,IAAI,SAAS,QAAQ,KAAK;AACxC,UAAM,EAAE,KAAK,OAAO,IAAI,SAAS,CAAC;AAElC,QAAI,KAAK;AACT,QAAI,KAAK;AACT,QAAI,IAAI,IAAI,SAAS,QAAQ;AAC3B,YAAM,aAAa,SAAS,IAAI,CAAC,EAAE;AACnC,WAAK,KAAK,IAAI,UAAU,aAAa,WAAW,IAAI;AAEpD,UAAI,OAAO,UAAU;AACnB,aAAK,KAAK,IAAI,YAAY,eAAe,WAAW,MAAM;AAAA,MAC5D,WAAW,KAAK,UAAU;AACxB,aAAK,eAAe,WAAW;AAAA,MACjC;AAAA,IACF;AAEA;AAAA,MACE;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,aAAa,OAAO;AAAA,MACpB,eAAe,OAAO;AAAA,MACtB;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACF;AAEA,SAAS,WACP,OACA,QACA,UACA,SACA,gBACA,OACA,YACA,YACA,cACA,UACA,YACA;AACA,QAAM,SAAS,MAAM,KAAK;AAC1B,MAAI,cAAc,OAAQ,QAAO,QAAQ,GAAI,SAAmD;AAEhG,QAAM,MAAM,IAAI,SAAS,QAAQ,MAAM;AACvC,QAAM,gBAAgB,QAAQ;AAC9B,QAAM,cAAc,MAAM;AAC1B,QAAM,UAAU,gBAAgB,GAAG;AACnC,QAAM,EAAE,iBAAiB,gBAAgB,UAAU,YAAY,QAAQ,IAAI;AAE3E,SAAO,SAAS,eAAe;AAC/B,SAAO,OAAO,IAAI,KAAK;AAEvB,MAAI,SAAU,QAAO,gBAAgB,QAAQ;AAAA,MACxC,UAAS,IAAI,GAAG,IAAI,gBAAgB,QAAQ,IAAK,gBAAe,KAAK,IAAI;AAE9E,MAAI,QAAS,UAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,IAAK,YAAW,KAAK,QAAQ,CAAC,IAAI,aAAa;AAEhG,WAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;AACvC,UAAM,QAAQ,aAAa;AAM3B,QAAI,QAAQ,SAAU;AAItB,UAAM,MAAM,QAAQ,UAAU,KAAK;AAGnC,UAAM,UAAU,MAAM,IAAI,eAAe;AAEzC,UAAM,OAAO,QAAQ,CAAC;AACtB,aAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,YAAM,MAAM,KAAK,CAAC;AAClB,YAAM,SAAS,UAAU,IAAI,MAAM;AAInC,UAAI,UAAU,YAAY,UAAU,WAAY;AAEhD,UAAI,IAAI,WAAW,GAAG;AACpB,YAAI,KAAK,CAAC,MAAM,CAAC;AACjB;AAAA,MACF;AAEA,YAAM,eAAe,gBAAgB,IAAI,aAAa;AACtD,YAAM,aAAa,IAAI,WAAW;AAClC,YAAM,eAAe,IAAI,aAAa;AACtC,UAAI;AAAA,QACF,IAAI,WAAW,IACX,CAAC,QAAQ,cAAc,YAAY,YAAY,IAC/C,CAAC,QAAQ,cAAc,YAAY,cAAc,cAAc,IAAI,WAAW,CAAC;AAAA,MACrF;AAAA,IACF;AAAA,EACF;AACF;AAEA,SAAS,OAAU,KAAU,OAAY;AACvC,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,IAAK,KAAI,KAAK,MAAM,CAAC,CAAC;AAC1D;AAEA,SAAS,QAAW,KAAY,OAAoB;AAClD,WAAS,IAAI,IAAI,QAAQ,KAAK,OAAO,IAAK,KAAI,CAAC,IAAI,CAAC;AACpD,SAAO,IAAI,KAAK;AAClB;;;ARhHA,IAAM,gBAAgB;AACtB,IAAM,kBAAkB;AAEjB,IAAM,oBAAoB;AAC1B,IAAM,uBAAuB;AAI7B,IAAM,WAAN,MAAoC;AAAA,EAkBzC,YAAY,KAAyB,QAAwB;AAC3D,UAAM,WAAW,OAAO,QAAQ;AAChC,QAAI,CAAC,YAAa,IAAyC,aAAc,QAAO;AAEhF,UAAM,SAAS,MAAM,GAAwC;AAE7D,UAAM,EAAE,SAAS,MAAM,OAAO,YAAY,SAAS,eAAe,IAAI;AACtE,SAAK,UAAU;AACf,SAAK,OAAO;AACZ,SAAK,QAAQ,SAAS,CAAC;AACvB,SAAK,aAAa;AAClB,SAAK,UAAU;AACf,SAAK,iBAAiB;AACtB,SAAK,aAAa,OAAO,cAAe,OAAkB,uBAAuB;AAEjF,UAAM,UAAU,SAAS,QAAQ,UAAU;AAC3C,SAAK,kBAAkB,QAAQ,IAAI,OAAO;AAE1C,UAAM,EAAE,SAAS,IAAI;AACrB,QAAI,OAAO,aAAa,UAAU;AAChC,WAAK,WAAW;AAChB,WAAK,WAAW;AAAA,IAClB,WAAW,MAAM,QAAQ,QAAQ,GAAG;AAClC,WAAK,WAAW;AAChB,WAAK,WAAW,UAAU,UAAU,QAAQ;AAAA,IAC9C,WAAY,OAAyC,UAAU;AAC7D,YAAM,IAAI,MAAM,4EAA4E;AAAA,IAC9F,OAAO;AACL,YAAM,IAAI,MAAM,uBAAuB,KAAK,UAAU,MAAM,CAAC,EAAE;AAAA,IACjE;AAEA,SAAK,eAAe,cAAc;AAClC,SAAK,aAAa;AAClB,SAAK,iBAAiB;AAAA,EACxB;AACF;AAMA,SAAS,KAAK,KAAyB;AACrC,SAAO;AACT;AAKO,SAAS,gBAAgB,KAA6C;AAzJ7E;AA0JE,UAAQ,gBAAK,GAAG,GAAE,aAAV,eAAU,WAAa,OAAO,KAAK,GAAG,EAAE,QAAS;AAC3D;AAKO,SAAS,gBAAgB,KAAuD;AAhKvF;AAiKE,UAAQ,UAAK,GAAG,GAAE,aAAV,GAAU,WAAa,OAAO,KAAK,GAAG,EAAE,QAAS;AAC3D;AAMO,SAAS,aACd,KACA,MACA,QACmC;AACnC,QAAM,UAAU,gBAAgB,GAAG;AAInC,MAAI,QAAQ,QAAQ,OAAQ,QAAO;AAEnC,QAAM,WAAW,QAAQ,IAAI;AAC7B,QAAM,QAAQ;AAAA,IACZ;AAAA,IACA,KAAK,GAAG,EAAE;AAAA,IACV;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,SAAO,UAAU,KAAK,OAAO,SAAS,KAAK;AAC7C;AAOO,SAAS,oBACd,KACA,QAC0C;AAC1C,MAAI,EAAE,MAAM,QAAQ,KAAK,IAAI;AAC7B;AACA,MAAI,OAAO,EAAG,OAAM,IAAI,MAAM,aAAa;AAC3C,MAAI,SAAS,EAAG,OAAM,IAAI,MAAM,eAAe;AAE/C,QAAM,UAAU,gBAAgB,GAAG;AAInC,MAAI,QAAQ,QAAQ,OAAQ,QAAO,SAAS,MAAM,MAAM,MAAM,IAAI;AAElE,QAAM,WAAW,QAAQ,IAAI;AAC7B,QAAM,QAAQ;AAAA,IACZ;AAAA,IACA,KAAK,GAAG,EAAE;AAAA,IACV;AAAA,IACA;AAAA,IACA,QAAQ;AAAA,EACV;AAEA,MAAI,UAAU,GAAI,QAAO,SAAS,MAAM,MAAM,MAAM,IAAI;AAExD,QAAM,UAAU,SAAS,KAAK;AAC9B,MAAI,QAAQ,WAAW,EAAG,QAAO,SAAS,MAAM,MAAM,MAAM,IAAI;AAEhE,QAAM,EAAE,OAAO,gBAAgB,IAAI;AACnC,SAAO;AAAA,IACL,gBAAgB,QAAQ,aAAa,CAAC;AAAA,IACtC,QAAQ,WAAW,IAAI;AAAA,IACvB,QAAQ,aAAa;AAAA,IACrB,QAAQ,WAAW,IAAI,MAAM,QAAQ,WAAW,CAAC,IAAI;AAAA,EACvD;AACF;AAKO,SAAS,qBACd,KACA,QAC4C;AAC5C,QAAM,EAAE,QAAQ,MAAM,QAAQ,KAAK,IAAI;AACvC,SAAO,kBAAkB,KAAK,QAAQ,MAAM,QAAQ,QAAQ,sBAAsB,KAAK;AACzF;AAKO,SAAS,yBAAyB,KAAe,QAA0C;AAChG,QAAM,EAAE,QAAQ,MAAM,QAAQ,KAAK,IAAI;AAEvC,SAAO,kBAAkB,KAAK,QAAQ,MAAM,QAAQ,QAAQ,mBAAmB,IAAI;AACrF;AAKO,SAAS,YAAY,KAAe,IAA0C;AACnF,QAAM,UAAU,gBAAgB,GAAG;AACnC,QAAM,EAAE,OAAO,gBAAgB,IAAI;AAEnC,WAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;AACvC,UAAM,OAAO,QAAQ,CAAC;AACtB,aAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,YAAM,MAAM,KAAK,CAAC;AAElB,YAAM,gBAAgB,IAAI;AAC1B,YAAM,kBAAkB,IAAI,CAAC;AAC7B,UAAI,SAAS;AACb,UAAI,eAAe;AACnB,UAAI,iBAAiB;AACrB,UAAI,OAAO;AACX,UAAI,IAAI,WAAW,GAAG;AACpB,iBAAS,gBAAgB,IAAI,CAAC,CAAC;AAC/B,uBAAe,IAAI,CAAC,IAAI;AACxB,yBAAiB,IAAI,CAAC;AAAA,MACxB;AACA,UAAI,IAAI,WAAW,EAAG,QAAO,MAAM,IAAI,CAAC,CAAC;AAEzC,SAAG;AAAA,QACD;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAgB;AAAA,IAClB;AAAA,EACF;AACF;AAEA,SAAS,YAAY,KAAe,QAAwB;AAC1D,QAAM,EAAE,SAAS,gBAAgB,IAAI;AACrC,MAAI,QAAQ,QAAQ,QAAQ,MAAM;AAClC,MAAI,UAAU,GAAI,SAAQ,gBAAgB,QAAQ,MAAM;AACxD,SAAO;AACT;AAKO,SAAS,iBAAiB,KAAe,QAA+B;AAC7E,QAAM,EAAE,eAAe,IAAI;AAC3B,MAAI,kBAAkB,KAAM,QAAO;AACnC,QAAM,QAAQ,YAAY,KAAK,MAAM;AACrC,SAAO,UAAU,KAAK,OAAO,eAAe,KAAK;AACnD;AAKO,SAAS,UAAU,KAAe,QAAyB;AAChE,QAAM,EAAE,WAAW,IAAI;AACvB,MAAI,cAAc,KAAM,QAAO;AAC/B,QAAM,QAAQ,YAAY,KAAK,MAAM;AACrC,SAAO,UAAU,KAAK,QAAQ,WAAW,SAAS,KAAK;AACzD;AAMO,SAAS,oBAAoB,KAAuB,QAA2B;AACpF,QAAM,SAAS,IAAI,SAAS,MAAM,KAAK,CAAC,CAAC,GAAG,MAAM;AAClD,OAAK,MAAM,EAAE,WAAW,IAAI;AAC5B,SAAO;AACT;AAMO,SAAS,WACd,KACkF;AAClF,SAAO,MAAM,KAAK,gBAAgB,GAAG,CAAC;AACxC;AAMO,SAAS,WAAW,KAAiC;AAC1D,SAAO,MAAM,KAAK,gBAAgB,GAAG,CAAC;AACxC;AAEA,SAAS,MACP,KACA,UACwD;AACxD,SAAO;AAAA,IACL,SAAS,IAAI;AAAA,IACb,MAAM,IAAI;AAAA,IACV,OAAO,IAAI;AAAA,IACX,YAAY,IAAI;AAAA,IAChB,SAAS,IAAI;AAAA,IACb,gBAAgB,IAAI;AAAA,IACpB;AAAA,IACA,YAAY,IAAI,cAAe,IAAe;AAAA,EAChD;AACF;AASA,SAAS,SACP,QACA,MACA,QACA,MAC0C;AAC1C,SAAO,EAAE,QAAQ,MAAM,QAAQ,KAAK;AACtC;AAIA,SAAS,SACP,MACA,QAC4C;AAC5C,SAAO,EAAE,MAAM,OAAO;AACxB;AAgBA,SAAS,qBACP,UACA,MACA,MACA,QACA,MACQ;AACR,MAAI,QAAQ,qBAAqB,UAAU,QAAQ,MAAM,IAAI;AAC7D,MAAI,OAAS;AACX,aAAS,SAAS,oBAAoB,aAAa,YAAY,UAAU,QAAQ,KAAK;AAAA,EACxF,WAAW,SAAS,kBAAmB;AAEvC,MAAI,UAAU,MAAM,UAAU,SAAS,OAAQ,QAAO;AACtD,SAAO;AACT;AAEA,SAAS,wBACP,UACA,MACA,MACA,QACA,MACoB;AACpB,MAAI,MAAM,qBAAqB,UAAU,MAAM,MAAM,QAAQ,oBAAoB;AAQjF,MAAI,CAAC,SAAW,SAAS,kBAAmB;AAE5C,MAAI,QAAQ,MAAM,QAAQ,SAAS,OAAQ,QAAO,CAAC;AAKnD,QAAM,gBAAgB,QAAU,SAAS,SAAS,GAAG,EAAE,MAAM;AAG7D,MAAI,CAAC,MAAS,OAAM,WAAW,UAAU,eAAe,GAAG;AAC3D,QAAM,MAAM,WAAW,UAAU,eAAe,GAAG;AAEnD,QAAM,SAAS,CAAC;AAChB,SAAO,OAAO,KAAK,OAAO;AACxB,UAAM,UAAU,SAAS,GAAG;AAC5B,WAAO,KAAK,SAAS,QAAQ,kBAAkB,IAAI,GAAG,QAAQ,oBAAoB,CAAC,CAAC;AAAA,EACtF;AACA,SAAO;AACT;AAkBA,SAAS,kBACP,KACA,QACA,MACA,QACA,MACA,KACiE;AA5dnE;AA6dE;AACA,MAAI,OAAO,EAAG,OAAM,IAAI,MAAM,aAAa;AAC3C,MAAI,SAAS,EAAG,OAAM,IAAI,MAAM,eAAe;AAE/C,QAAM,EAAE,SAAS,gBAAgB,IAAI;AACrC,MAAIC,eAAc,QAAQ,QAAQ,MAAM;AACxC,MAAIA,iBAAgB,GAAI,CAAAA,eAAc,gBAAgB,QAAQ,MAAM;AACpE,MAAIA,iBAAgB,GAAI,QAAO,MAAM,CAAC,IAAI,SAAS,MAAM,IAAI;AAE7D,QAAM,iBAAiB,UAAK,GAAG,GAAE,mBAAV,GAAU,iBAAmB,QAAQ,IAAI,aAAa;AAC7E,QAAM,aAAa,UAAK,GAAG,GAAE,eAAV,GAAU,aAAe,eAAe,gBAAgB,GAAG,GAAG,aAAa;AAE9F,QAAM,WAAW,UAAUA,YAAW,EAAE,IAAI;AAC5C,MAAI,YAAY,KAAM,QAAO,MAAM,CAAC,IAAI,SAAS,MAAM,IAAI;AAE3D,QAAM,OAAO,cAAcA,YAAW;AAEtC,MAAI,IAAK,QAAO,wBAAwB,UAAU,MAAM,MAAM,QAAQ,IAAI;AAE1E,QAAM,QAAQ,qBAAqB,UAAU,MAAM,MAAM,QAAQ,IAAI;AACrE,MAAI,UAAU,GAAI,QAAO,SAAS,MAAM,IAAI;AAE5C,QAAM,UAAU,SAAS,KAAK;AAC9B,SAAO,SAAS,QAAQ,kBAAkB,IAAI,GAAG,QAAQ,oBAAoB,CAAC;AAChF;", | |
| 5 | "names": ["sourceIndex", "sourceIndex"] | |
| 6 | } |
| … | … | @@ -0,0 +1,559 @@ |
| 1 | (function (global, factory) { | |
| 2 | if (typeof exports === 'object' && typeof module !== 'undefined') { | |
| 3 | factory(module, require('@jridgewell/resolve-uri'), require('@jridgewell/sourcemap-codec')); | |
| 4 | module.exports = def(module); | |
| 5 | } else if (typeof define === 'function' && define.amd) { | |
| 6 | define(['module', '@jridgewell/resolve-uri', '@jridgewell/sourcemap-codec'], function(mod) { | |
| 7 | factory.apply(this, arguments); | |
| 8 | mod.exports = def(mod); | |
| 9 | }); | |
| 10 | } else { | |
| 11 | const mod = { exports: {} }; | |
| 12 | factory(mod, global.resolveURI, global.sourcemapCodec); | |
| 13 | global = typeof globalThis !== 'undefined' ? globalThis : global || self; | |
| 14 | global.traceMapping = def(mod); | |
| 15 | } | |
| 16 | function def(m) { return 'default' in m.exports ? m.exports.default : m.exports; } | |
| 17 | })(this, (function (module, require_resolveURI, require_sourcemapCodec) { | |
| 18 | "use strict"; | |
| 19 | var __create = Object.create; | |
| 20 | var __defProp = Object.defineProperty; | |
| 21 | var __getOwnPropDesc = Object.getOwnPropertyDescriptor; | |
| 22 | var __getOwnPropNames = Object.getOwnPropertyNames; | |
| 23 | var __getProtoOf = Object.getPrototypeOf; | |
| 24 | var __hasOwnProp = Object.prototype.hasOwnProperty; | |
| 25 | var __commonJS = (cb, mod) => function __require() { | |
| 26 | return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; | |
| 27 | }; | |
| 28 | var __export = (target, all) => { | |
| 29 | for (var name in all) | |
| 30 | __defProp(target, name, { get: all[name], enumerable: true }); | |
| 31 | }; | |
| 32 | var __copyProps = (to, from, except, desc) => { | |
| 33 | if (from && typeof from === "object" || typeof from === "function") { | |
| 34 | for (let key of __getOwnPropNames(from)) | |
| 35 | if (!__hasOwnProp.call(to, key) && key !== except) | |
| 36 | __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); | |
| 37 | } | |
| 38 | return to; | |
| 39 | }; | |
| 40 | var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( | |
| 41 | // If the importer is in node compatibility mode or this is not an ESM | |
| 42 | // file that has been converted to a CommonJS file using a Babel- | |
| 43 | // compatible transform (i.e. "__esModule" has not been set), then set | |
| 44 | // "default" to the CommonJS "module.exports" for node compatibility. | |
| 45 | isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, | |
| 46 | mod | |
| 47 | )); | |
| 48 | var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); | |
| 49 | ||
| 50 | // umd:@jridgewell/sourcemap-codec | |
| 51 | var require_sourcemap_codec = __commonJS({ | |
| 52 | "umd:@jridgewell/sourcemap-codec"(exports, module2) { | |
| 53 | module2.exports = require_sourcemapCodec; | |
| 54 | } | |
| 55 | }); | |
| 56 | ||
| 57 | // umd:@jridgewell/resolve-uri | |
| 58 | var require_resolve_uri = __commonJS({ | |
| 59 | "umd:@jridgewell/resolve-uri"(exports, module2) { | |
| 60 | module2.exports = require_resolveURI; | |
| 61 | } | |
| 62 | }); | |
| 63 | ||
| 64 | // src/trace-mapping.ts | |
| 65 | var trace_mapping_exports = {}; | |
| 66 | __export(trace_mapping_exports, { | |
| 67 | AnyMap: () => FlattenMap, | |
| 68 | FlattenMap: () => FlattenMap, | |
| 69 | GREATEST_LOWER_BOUND: () => GREATEST_LOWER_BOUND, | |
| 70 | LEAST_UPPER_BOUND: () => LEAST_UPPER_BOUND, | |
| 71 | TraceMap: () => TraceMap, | |
| 72 | allGeneratedPositionsFor: () => allGeneratedPositionsFor, | |
| 73 | decodedMap: () => decodedMap, | |
| 74 | decodedMappings: () => decodedMappings, | |
| 75 | eachMapping: () => eachMapping, | |
| 76 | encodedMap: () => encodedMap, | |
| 77 | encodedMappings: () => encodedMappings, | |
| 78 | generatedPositionFor: () => generatedPositionFor, | |
| 79 | isIgnored: () => isIgnored, | |
| 80 | originalPositionFor: () => originalPositionFor, | |
| 81 | presortedDecodedMap: () => presortedDecodedMap, | |
| 82 | sourceContentFor: () => sourceContentFor, | |
| 83 | traceSegment: () => traceSegment | |
| 84 | }); | |
| 85 | module.exports = __toCommonJS(trace_mapping_exports); | |
| 86 | var import_sourcemap_codec = __toESM(require_sourcemap_codec()); | |
| 87 | ||
| 88 | // src/resolve.ts | |
| 89 | var import_resolve_uri = __toESM(require_resolve_uri()); | |
| 90 | ||
| 91 | // src/strip-filename.ts | |
| 92 | function stripFilename(path) { | |
| 93 | if (!path) return ""; | |
| 94 | const index = path.lastIndexOf("/"); | |
| 95 | return path.slice(0, index + 1); | |
| 96 | } | |
| 97 | ||
| 98 | // src/resolve.ts | |
| 99 | function resolver(mapUrl, sourceRoot) { | |
| 100 | const from = stripFilename(mapUrl); | |
| 101 | const prefix = sourceRoot ? sourceRoot + "/" : ""; | |
| 102 | return (source) => (0, import_resolve_uri.default)(prefix + (source || ""), from); | |
| 103 | } | |
| 104 | ||
| 105 | // src/sourcemap-segment.ts | |
| 106 | var COLUMN = 0; | |
| 107 | var SOURCES_INDEX = 1; | |
| 108 | var SOURCE_LINE = 2; | |
| 109 | var SOURCE_COLUMN = 3; | |
| 110 | var NAMES_INDEX = 4; | |
| 111 | var REV_GENERATED_LINE = 1; | |
| 112 | var REV_GENERATED_COLUMN = 2; | |
| 113 | ||
| 114 | // src/sort.ts | |
| 115 | function maybeSort(mappings, owned) { | |
| 116 | const unsortedIndex = nextUnsortedSegmentLine(mappings, 0); | |
| 117 | if (unsortedIndex === mappings.length) return mappings; | |
| 118 | if (!owned) mappings = mappings.slice(); | |
| 119 | for (let i = unsortedIndex; i < mappings.length; i = nextUnsortedSegmentLine(mappings, i + 1)) { | |
| 120 | mappings[i] = sortSegments(mappings[i], owned); | |
| 121 | } | |
| 122 | return mappings; | |
| 123 | } | |
| 124 | function nextUnsortedSegmentLine(mappings, start) { | |
| 125 | for (let i = start; i < mappings.length; i++) { | |
| 126 | if (!isSorted(mappings[i])) return i; | |
| 127 | } | |
| 128 | return mappings.length; | |
| 129 | } | |
| 130 | function isSorted(line) { | |
| 131 | for (let j = 1; j < line.length; j++) { | |
| 132 | if (line[j][COLUMN] < line[j - 1][COLUMN]) { | |
| 133 | return false; | |
| 134 | } | |
| 135 | } | |
| 136 | return true; | |
| 137 | } | |
| 138 | function sortSegments(line, owned) { | |
| 139 | if (!owned) line = line.slice(); | |
| 140 | return line.sort(sortComparator); | |
| 141 | } | |
| 142 | function sortComparator(a, b) { | |
| 143 | return a[COLUMN] - b[COLUMN]; | |
| 144 | } | |
| 145 | ||
| 146 | // src/by-source.ts | |
| 147 | function buildBySources(decoded, memos) { | |
| 148 | const sources = memos.map(() => []); | |
| 149 | for (let i = 0; i < decoded.length; i++) { | |
| 150 | const line = decoded[i]; | |
| 151 | for (let j = 0; j < line.length; j++) { | |
| 152 | const seg = line[j]; | |
| 153 | if (seg.length === 1) continue; | |
| 154 | const sourceIndex2 = seg[SOURCES_INDEX]; | |
| 155 | const sourceLine = seg[SOURCE_LINE]; | |
| 156 | const sourceColumn = seg[SOURCE_COLUMN]; | |
| 157 | const source = sources[sourceIndex2]; | |
| 158 | const segs = source[sourceLine] || (source[sourceLine] = []); | |
| 159 | segs.push([sourceColumn, i, seg[COLUMN]]); | |
| 160 | } | |
| 161 | } | |
| 162 | for (let i = 0; i < sources.length; i++) { | |
| 163 | const source = sources[i]; | |
| 164 | for (let j = 0; j < source.length; j++) { | |
| 165 | const line = source[j]; | |
| 166 | if (line) line.sort(sortComparator); | |
| 167 | } | |
| 168 | } | |
| 169 | return sources; | |
| 170 | } | |
| 171 | ||
| 172 | // src/binary-search.ts | |
| 173 | var found = false; | |
| 174 | function binarySearch(haystack, needle, low, high) { | |
| 175 | while (low <= high) { | |
| 176 | const mid = low + (high - low >> 1); | |
| 177 | const cmp = haystack[mid][COLUMN] - needle; | |
| 178 | if (cmp === 0) { | |
| 179 | found = true; | |
| 180 | return mid; | |
| 181 | } | |
| 182 | if (cmp < 0) { | |
| 183 | low = mid + 1; | |
| 184 | } else { | |
| 185 | high = mid - 1; | |
| 186 | } | |
| 187 | } | |
| 188 | found = false; | |
| 189 | return low - 1; | |
| 190 | } | |
| 191 | function upperBound(haystack, needle, index) { | |
| 192 | for (let i = index + 1; i < haystack.length; index = i++) { | |
| 193 | if (haystack[i][COLUMN] !== needle) break; | |
| 194 | } | |
| 195 | return index; | |
| 196 | } | |
| 197 | function lowerBound(haystack, needle, index) { | |
| 198 | for (let i = index - 1; i >= 0; index = i--) { | |
| 199 | if (haystack[i][COLUMN] !== needle) break; | |
| 200 | } | |
| 201 | return index; | |
| 202 | } | |
| 203 | function memoizedState() { | |
| 204 | return { | |
| 205 | lastKey: -1, | |
| 206 | lastNeedle: -1, | |
| 207 | lastIndex: -1 | |
| 208 | }; | |
| 209 | } | |
| 210 | function memoizedBinarySearch(haystack, needle, state, key) { | |
| 211 | const { lastKey, lastNeedle, lastIndex } = state; | |
| 212 | let low = 0; | |
| 213 | let high = haystack.length - 1; | |
| 214 | if (key === lastKey) { | |
| 215 | if (needle === lastNeedle) { | |
| 216 | found = lastIndex !== -1 && haystack[lastIndex][COLUMN] === needle; | |
| 217 | return lastIndex; | |
| 218 | } | |
| 219 | if (needle >= lastNeedle) { | |
| 220 | low = lastIndex === -1 ? 0 : lastIndex; | |
| 221 | } else { | |
| 222 | high = lastIndex; | |
| 223 | } | |
| 224 | } | |
| 225 | state.lastKey = key; | |
| 226 | state.lastNeedle = needle; | |
| 227 | return state.lastIndex = binarySearch(haystack, needle, low, high); | |
| 228 | } | |
| 229 | ||
| 230 | // src/types.ts | |
| 231 | function parse(map) { | |
| 232 | return typeof map === "string" ? JSON.parse(map) : map; | |
| 233 | } | |
| 234 | ||
| 235 | // src/flatten-map.ts | |
| 236 | var FlattenMap = function(map, mapUrl) { | |
| 237 | const parsed = parse(map); | |
| 238 | if (!("sections" in parsed)) { | |
| 239 | return new TraceMap(parsed, mapUrl); | |
| 240 | } | |
| 241 | const mappings = []; | |
| 242 | const sources = []; | |
| 243 | const sourcesContent = []; | |
| 244 | const names = []; | |
| 245 | const ignoreList = []; | |
| 246 | recurse( | |
| 247 | parsed, | |
| 248 | mapUrl, | |
| 249 | mappings, | |
| 250 | sources, | |
| 251 | sourcesContent, | |
| 252 | names, | |
| 253 | ignoreList, | |
| 254 | 0, | |
| 255 | 0, | |
| 256 | Infinity, | |
| 257 | Infinity | |
| 258 | ); | |
| 259 | const joined = { | |
| 260 | version: 3, | |
| 261 | file: parsed.file, | |
| 262 | names, | |
| 263 | sources, | |
| 264 | sourcesContent, | |
| 265 | mappings, | |
| 266 | ignoreList | |
| 267 | }; | |
| 268 | return presortedDecodedMap(joined); | |
| 269 | }; | |
| 270 | function recurse(input, mapUrl, mappings, sources, sourcesContent, names, ignoreList, lineOffset, columnOffset, stopLine, stopColumn) { | |
| 271 | const { sections } = input; | |
| 272 | for (let i = 0; i < sections.length; i++) { | |
| 273 | const { map, offset } = sections[i]; | |
| 274 | let sl = stopLine; | |
| 275 | let sc = stopColumn; | |
| 276 | if (i + 1 < sections.length) { | |
| 277 | const nextOffset = sections[i + 1].offset; | |
| 278 | sl = Math.min(stopLine, lineOffset + nextOffset.line); | |
| 279 | if (sl === stopLine) { | |
| 280 | sc = Math.min(stopColumn, columnOffset + nextOffset.column); | |
| 281 | } else if (sl < stopLine) { | |
| 282 | sc = columnOffset + nextOffset.column; | |
| 283 | } | |
| 284 | } | |
| 285 | addSection( | |
| 286 | map, | |
| 287 | mapUrl, | |
| 288 | mappings, | |
| 289 | sources, | |
| 290 | sourcesContent, | |
| 291 | names, | |
| 292 | ignoreList, | |
| 293 | lineOffset + offset.line, | |
| 294 | columnOffset + offset.column, | |
| 295 | sl, | |
| 296 | sc | |
| 297 | ); | |
| 298 | } | |
| 299 | } | |
| 300 | function addSection(input, mapUrl, mappings, sources, sourcesContent, names, ignoreList, lineOffset, columnOffset, stopLine, stopColumn) { | |
| 301 | const parsed = parse(input); | |
| 302 | if ("sections" in parsed) return recurse(...arguments); | |
| 303 | const map = new TraceMap(parsed, mapUrl); | |
| 304 | const sourcesOffset = sources.length; | |
| 305 | const namesOffset = names.length; | |
| 306 | const decoded = decodedMappings(map); | |
| 307 | const { resolvedSources, sourcesContent: contents, ignoreList: ignores } = map; | |
| 308 | append(sources, resolvedSources); | |
| 309 | append(names, map.names); | |
| 310 | if (contents) append(sourcesContent, contents); | |
| 311 | else for (let i = 0; i < resolvedSources.length; i++) sourcesContent.push(null); | |
| 312 | if (ignores) for (let i = 0; i < ignores.length; i++) ignoreList.push(ignores[i] + sourcesOffset); | |
| 313 | for (let i = 0; i < decoded.length; i++) { | |
| 314 | const lineI = lineOffset + i; | |
| 315 | if (lineI > stopLine) return; | |
| 316 | const out = getLine(mappings, lineI); | |
| 317 | const cOffset = i === 0 ? columnOffset : 0; | |
| 318 | const line = decoded[i]; | |
| 319 | for (let j = 0; j < line.length; j++) { | |
| 320 | const seg = line[j]; | |
| 321 | const column = cOffset + seg[COLUMN]; | |
| 322 | if (lineI === stopLine && column >= stopColumn) return; | |
| 323 | if (seg.length === 1) { | |
| 324 | out.push([column]); | |
| 325 | continue; | |
| 326 | } | |
| 327 | const sourcesIndex = sourcesOffset + seg[SOURCES_INDEX]; | |
| 328 | const sourceLine = seg[SOURCE_LINE]; | |
| 329 | const sourceColumn = seg[SOURCE_COLUMN]; | |
| 330 | out.push( | |
| 331 | seg.length === 4 ? [column, sourcesIndex, sourceLine, sourceColumn] : [column, sourcesIndex, sourceLine, sourceColumn, namesOffset + seg[NAMES_INDEX]] | |
| 332 | ); | |
| 333 | } | |
| 334 | } | |
| 335 | } | |
| 336 | function append(arr, other) { | |
| 337 | for (let i = 0; i < other.length; i++) arr.push(other[i]); | |
| 338 | } | |
| 339 | function getLine(arr, index) { | |
| 340 | for (let i = arr.length; i <= index; i++) arr[i] = []; | |
| 341 | return arr[index]; | |
| 342 | } | |
| 343 | ||
| 344 | // src/trace-mapping.ts | |
| 345 | var LINE_GTR_ZERO = "`line` must be greater than 0 (lines start at line 1)"; | |
| 346 | var COL_GTR_EQ_ZERO = "`column` must be greater than or equal to 0 (columns start at column 0)"; | |
| 347 | var LEAST_UPPER_BOUND = -1; | |
| 348 | var GREATEST_LOWER_BOUND = 1; | |
| 349 | var TraceMap = class { | |
| 350 | constructor(map, mapUrl) { | |
| 351 | const isString = typeof map === "string"; | |
| 352 | if (!isString && map._decodedMemo) return map; | |
| 353 | const parsed = parse(map); | |
| 354 | const { version, file, names, sourceRoot, sources, sourcesContent } = parsed; | |
| 355 | this.version = version; | |
| 356 | this.file = file; | |
| 357 | this.names = names || []; | |
| 358 | this.sourceRoot = sourceRoot; | |
| 359 | this.sources = sources; | |
| 360 | this.sourcesContent = sourcesContent; | |
| 361 | this.ignoreList = parsed.ignoreList || parsed.x_google_ignoreList || void 0; | |
| 362 | const resolve = resolver(mapUrl, sourceRoot); | |
| 363 | this.resolvedSources = sources.map(resolve); | |
| 364 | const { mappings } = parsed; | |
| 365 | if (typeof mappings === "string") { | |
| 366 | this._encoded = mappings; | |
| 367 | this._decoded = void 0; | |
| 368 | } else if (Array.isArray(mappings)) { | |
| 369 | this._encoded = void 0; | |
| 370 | this._decoded = maybeSort(mappings, isString); | |
| 371 | } else if (parsed.sections) { | |
| 372 | throw new Error(`TraceMap passed sectioned source map, please use FlattenMap export instead`); | |
| 373 | } else { | |
| 374 | throw new Error(`invalid source map: ${JSON.stringify(parsed)}`); | |
| 375 | } | |
| 376 | this._decodedMemo = memoizedState(); | |
| 377 | this._bySources = void 0; | |
| 378 | this._bySourceMemos = void 0; | |
| 379 | } | |
| 380 | }; | |
| 381 | function cast(map) { | |
| 382 | return map; | |
| 383 | } | |
| 384 | function encodedMappings(map) { | |
| 385 | var _a, _b; | |
| 386 | return (_b = (_a = cast(map))._encoded) != null ? _b : _a._encoded = (0, import_sourcemap_codec.encode)(cast(map)._decoded); | |
| 387 | } | |
| 388 | function decodedMappings(map) { | |
| 389 | var _a; | |
| 390 | return (_a = cast(map))._decoded || (_a._decoded = (0, import_sourcemap_codec.decode)(cast(map)._encoded)); | |
| 391 | } | |
| 392 | function traceSegment(map, line, column) { | |
| 393 | const decoded = decodedMappings(map); | |
| 394 | if (line >= decoded.length) return null; | |
| 395 | const segments = decoded[line]; | |
| 396 | const index = traceSegmentInternal( | |
| 397 | segments, | |
| 398 | cast(map)._decodedMemo, | |
| 399 | line, | |
| 400 | column, | |
| 401 | GREATEST_LOWER_BOUND | |
| 402 | ); | |
| 403 | return index === -1 ? null : segments[index]; | |
| 404 | } | |
| 405 | function originalPositionFor(map, needle) { | |
| 406 | let { line, column, bias } = needle; | |
| 407 | line--; | |
| 408 | if (line < 0) throw new Error(LINE_GTR_ZERO); | |
| 409 | if (column < 0) throw new Error(COL_GTR_EQ_ZERO); | |
| 410 | const decoded = decodedMappings(map); | |
| 411 | if (line >= decoded.length) return OMapping(null, null, null, null); | |
| 412 | const segments = decoded[line]; | |
| 413 | const index = traceSegmentInternal( | |
| 414 | segments, | |
| 415 | cast(map)._decodedMemo, | |
| 416 | line, | |
| 417 | column, | |
| 418 | bias || GREATEST_LOWER_BOUND | |
| 419 | ); | |
| 420 | if (index === -1) return OMapping(null, null, null, null); | |
| 421 | const segment = segments[index]; | |
| 422 | if (segment.length === 1) return OMapping(null, null, null, null); | |
| 423 | const { names, resolvedSources } = map; | |
| 424 | return OMapping( | |
| 425 | resolvedSources[segment[SOURCES_INDEX]], | |
| 426 | segment[SOURCE_LINE] + 1, | |
| 427 | segment[SOURCE_COLUMN], | |
| 428 | segment.length === 5 ? names[segment[NAMES_INDEX]] : null | |
| 429 | ); | |
| 430 | } | |
| 431 | function generatedPositionFor(map, needle) { | |
| 432 | const { source, line, column, bias } = needle; | |
| 433 | return generatedPosition(map, source, line, column, bias || GREATEST_LOWER_BOUND, false); | |
| 434 | } | |
| 435 | function allGeneratedPositionsFor(map, needle) { | |
| 436 | const { source, line, column, bias } = needle; | |
| 437 | return generatedPosition(map, source, line, column, bias || LEAST_UPPER_BOUND, true); | |
| 438 | } | |
| 439 | function eachMapping(map, cb) { | |
| 440 | const decoded = decodedMappings(map); | |
| 441 | const { names, resolvedSources } = map; | |
| 442 | for (let i = 0; i < decoded.length; i++) { | |
| 443 | const line = decoded[i]; | |
| 444 | for (let j = 0; j < line.length; j++) { | |
| 445 | const seg = line[j]; | |
| 446 | const generatedLine = i + 1; | |
| 447 | const generatedColumn = seg[0]; | |
| 448 | let source = null; | |
| 449 | let originalLine = null; | |
| 450 | let originalColumn = null; | |
| 451 | let name = null; | |
| 452 | if (seg.length !== 1) { | |
| 453 | source = resolvedSources[seg[1]]; | |
| 454 | originalLine = seg[2] + 1; | |
| 455 | originalColumn = seg[3]; | |
| 456 | } | |
| 457 | if (seg.length === 5) name = names[seg[4]]; | |
| 458 | cb({ | |
| 459 | generatedLine, | |
| 460 | generatedColumn, | |
| 461 | source, | |
| 462 | originalLine, | |
| 463 | originalColumn, | |
| 464 | name | |
| 465 | }); | |
| 466 | } | |
| 467 | } | |
| 468 | } | |
| 469 | function sourceIndex(map, source) { | |
| 470 | const { sources, resolvedSources } = map; | |
| 471 | let index = sources.indexOf(source); | |
| 472 | if (index === -1) index = resolvedSources.indexOf(source); | |
| 473 | return index; | |
| 474 | } | |
| 475 | function sourceContentFor(map, source) { | |
| 476 | const { sourcesContent } = map; | |
| 477 | if (sourcesContent == null) return null; | |
| 478 | const index = sourceIndex(map, source); | |
| 479 | return index === -1 ? null : sourcesContent[index]; | |
| 480 | } | |
| 481 | function isIgnored(map, source) { | |
| 482 | const { ignoreList } = map; | |
| 483 | if (ignoreList == null) return false; | |
| 484 | const index = sourceIndex(map, source); | |
| 485 | return index === -1 ? false : ignoreList.includes(index); | |
| 486 | } | |
| 487 | function presortedDecodedMap(map, mapUrl) { | |
| 488 | const tracer = new TraceMap(clone(map, []), mapUrl); | |
| 489 | cast(tracer)._decoded = map.mappings; | |
| 490 | return tracer; | |
| 491 | } | |
| 492 | function decodedMap(map) { | |
| 493 | return clone(map, decodedMappings(map)); | |
| 494 | } | |
| 495 | function encodedMap(map) { | |
| 496 | return clone(map, encodedMappings(map)); | |
| 497 | } | |
| 498 | function clone(map, mappings) { | |
| 499 | return { | |
| 500 | version: map.version, | |
| 501 | file: map.file, | |
| 502 | names: map.names, | |
| 503 | sourceRoot: map.sourceRoot, | |
| 504 | sources: map.sources, | |
| 505 | sourcesContent: map.sourcesContent, | |
| 506 | mappings, | |
| 507 | ignoreList: map.ignoreList || map.x_google_ignoreList | |
| 508 | }; | |
| 509 | } | |
| 510 | function OMapping(source, line, column, name) { | |
| 511 | return { source, line, column, name }; | |
| 512 | } | |
| 513 | function GMapping(line, column) { | |
| 514 | return { line, column }; | |
| 515 | } | |
| 516 | function traceSegmentInternal(segments, memo, line, column, bias) { | |
| 517 | let index = memoizedBinarySearch(segments, column, memo, line); | |
| 518 | if (found) { | |
| 519 | index = (bias === LEAST_UPPER_BOUND ? upperBound : lowerBound)(segments, column, index); | |
| 520 | } else if (bias === LEAST_UPPER_BOUND) index++; | |
| 521 | if (index === -1 || index === segments.length) return -1; | |
| 522 | return index; | |
| 523 | } | |
| 524 | function sliceGeneratedPositions(segments, memo, line, column, bias) { | |
| 525 | let min = traceSegmentInternal(segments, memo, line, column, GREATEST_LOWER_BOUND); | |
| 526 | if (!found && bias === LEAST_UPPER_BOUND) min++; | |
| 527 | if (min === -1 || min === segments.length) return []; | |
| 528 | const matchedColumn = found ? column : segments[min][COLUMN]; | |
| 529 | if (!found) min = lowerBound(segments, matchedColumn, min); | |
| 530 | const max = upperBound(segments, matchedColumn, min); | |
| 531 | const result = []; | |
| 532 | for (; min <= max; min++) { | |
| 533 | const segment = segments[min]; | |
| 534 | result.push(GMapping(segment[REV_GENERATED_LINE] + 1, segment[REV_GENERATED_COLUMN])); | |
| 535 | } | |
| 536 | return result; | |
| 537 | } | |
| 538 | function generatedPosition(map, source, line, column, bias, all) { | |
| 539 | var _a, _b; | |
| 540 | line--; | |
| 541 | if (line < 0) throw new Error(LINE_GTR_ZERO); | |
| 542 | if (column < 0) throw new Error(COL_GTR_EQ_ZERO); | |
| 543 | const { sources, resolvedSources } = map; | |
| 544 | let sourceIndex2 = sources.indexOf(source); | |
| 545 | if (sourceIndex2 === -1) sourceIndex2 = resolvedSources.indexOf(source); | |
| 546 | if (sourceIndex2 === -1) return all ? [] : GMapping(null, null); | |
| 547 | const bySourceMemos = (_a = cast(map))._bySourceMemos || (_a._bySourceMemos = sources.map(memoizedState)); | |
| 548 | const generated = (_b = cast(map))._bySources || (_b._bySources = buildBySources(decodedMappings(map), bySourceMemos)); | |
| 549 | const segments = generated[sourceIndex2][line]; | |
| 550 | if (segments == null) return all ? [] : GMapping(null, null); | |
| 551 | const memo = bySourceMemos[sourceIndex2]; | |
| 552 | if (all) return sliceGeneratedPositions(segments, memo, line, column, bias); | |
| 553 | const index = traceSegmentInternal(segments, memo, line, column, bias); | |
| 554 | if (index === -1) return GMapping(null, null); | |
| 555 | const segment = segments[index]; | |
| 556 | return GMapping(segment[REV_GENERATED_LINE] + 1, segment[REV_GENERATED_COLUMN]); | |
| 557 | } | |
| 558 | })); | |
| 559 | //# sourceMappingURL=trace-mapping.umd.js.map |
| … | … | @@ -0,0 +1,6 @@ |
| 1 | { | |
| 2 | "version": 3, | |
| 3 | "sources": ["umd:@jridgewell/sourcemap-codec", "umd:@jridgewell/resolve-uri", "../src/trace-mapping.ts", "../src/resolve.ts", "../src/strip-filename.ts", "../src/sourcemap-segment.ts", "../src/sort.ts", "../src/by-source.ts", "../src/binary-search.ts", "../src/types.ts", "../src/flatten-map.ts"], | |
| 4 | "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA,6CAAAA,SAAA;AAAA,IAAAA,QAAO,UAAU;AAAA;AAAA;;;ACAjB;AAAA,yCAAAC,SAAA;AAAA,IAAAA,QAAO,UAAU;AAAA;AAAA;;;ACAjB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,6BAA+B;;;ACA/B,yBAAuB;;;ACGR,SAAR,cAA+B,MAAyC;AAC7E,MAAI,CAAC,KAAM,QAAO;AAClB,QAAM,QAAQ,KAAK,YAAY,GAAG;AAClC,SAAO,KAAK,MAAM,GAAG,QAAQ,CAAC;AAChC;;;ADHe,SAAR,SACL,QACA,YACS;AACT,QAAM,OAAO,cAAc,MAAM;AAIjC,QAAM,SAAS,aAAa,aAAa,MAAM;AAE/C,SAAO,CAAC,eAAW,mBAAAC,SAAW,UAAU,UAAU,KAAK,IAAI;AAC7D;;;AEAO,IAAM,SAAS;AACf,IAAM,gBAAgB;AACtB,IAAM,cAAc;AACpB,IAAM,gBAAgB;AACtB,IAAM,cAAc;AAEpB,IAAM,qBAAqB;AAC3B,IAAM,uBAAuB;;;AClBrB,SAAR,UACL,UACA,OACsB;AACtB,QAAM,gBAAgB,wBAAwB,UAAU,CAAC;AACzD,MAAI,kBAAkB,SAAS,OAAQ,QAAO;AAI9C,MAAI,CAAC,MAAO,YAAW,SAAS,MAAM;AAEtC,WAAS,IAAI,eAAe,IAAI,SAAS,QAAQ,IAAI,wBAAwB,UAAU,IAAI,CAAC,GAAG;AAC7F,aAAS,CAAC,IAAI,aAAa,SAAS,CAAC,GAAG,KAAK;AAAA,EAC/C;AACA,SAAO;AACT;AAEA,SAAS,wBAAwB,UAAgC,OAAuB;AACtF,WAAS,IAAI,OAAO,IAAI,SAAS,QAAQ,KAAK;AAC5C,QAAI,CAAC,SAAS,SAAS,CAAC,CAAC,EAAG,QAAO;AAAA,EACrC;AACA,SAAO,SAAS;AAClB;AAEA,SAAS,SAAS,MAAmC;AACnD,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,QAAI,KAAK,CAAC,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC,EAAE,MAAM,GAAG;AACzC,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,aAAa,MAA0B,OAAoC;AAClF,MAAI,CAAC,MAAO,QAAO,KAAK,MAAM;AAC9B,SAAO,KAAK,KAAK,cAAc;AACjC;AAEO,SAAS,eAA4D,GAAM,GAAc;AAC9F,SAAO,EAAE,MAAM,IAAI,EAAE,MAAM;AAC7B;;;ACnCe,SAAR,eACL,SACA,OACU;AACV,QAAM,UAAoB,MAAM,IAAI,MAAM,CAAC,CAAC;AAE5C,WAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;AACvC,UAAM,OAAO,QAAQ,CAAC;AACtB,aAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,YAAM,MAAM,KAAK,CAAC;AAClB,UAAI,IAAI,WAAW,EAAG;AAEtB,YAAMC,eAAc,IAAI,aAAa;AACrC,YAAM,aAAa,IAAI,WAAW;AAClC,YAAM,eAAe,IAAI,aAAa;AAEtC,YAAM,SAAS,QAAQA,YAAW;AAClC,YAAM,OAAQ,4CAAuB,CAAC;AACtC,WAAK,KAAK,CAAC,cAAc,GAAG,IAAI,MAAM,CAAC,CAAC;AAAA,IAC1C;AAAA,EACF;AAEA,WAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;AACvC,UAAM,SAAS,QAAQ,CAAC;AACxB,aAAS,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK;AACtC,YAAM,OAAO,OAAO,CAAC;AACrB,UAAI,KAAM,MAAK,KAAK,cAAc;AAAA,IACpC;AAAA,EACF;AAEA,SAAO;AACT;;;AC/BO,IAAI,QAAQ;AAkBZ,SAAS,aACd,UACA,QACA,KACA,MACQ;AACR,SAAO,OAAO,MAAM;AAClB,UAAM,MAAM,OAAQ,OAAO,OAAQ;AACnC,UAAM,MAAM,SAAS,GAAG,EAAE,MAAM,IAAI;AAEpC,QAAI,QAAQ,GAAG;AACb,cAAQ;AACR,aAAO;AAAA,IACT;AAEA,QAAI,MAAM,GAAG;AACX,YAAM,MAAM;AAAA,IACd,OAAO;AACL,aAAO,MAAM;AAAA,IACf;AAAA,EACF;AAEA,UAAQ;AACR,SAAO,MAAM;AACf;AAEO,SAAS,WACd,UACA,QACA,OACQ;AACR,WAAS,IAAI,QAAQ,GAAG,IAAI,SAAS,QAAQ,QAAQ,KAAK;AACxD,QAAI,SAAS,CAAC,EAAE,MAAM,MAAM,OAAQ;AAAA,EACtC;AACA,SAAO;AACT;AAEO,SAAS,WACd,UACA,QACA,OACQ;AACR,WAAS,IAAI,QAAQ,GAAG,KAAK,GAAG,QAAQ,KAAK;AAC3C,QAAI,SAAS,CAAC,EAAE,MAAM,MAAM,OAAQ;AAAA,EACtC;AACA,SAAO;AACT;AAEO,SAAS,gBAA2B;AACzC,SAAO;AAAA,IACL,SAAS;AAAA,IACT,YAAY;AAAA,IACZ,WAAW;AAAA,EACb;AACF;AAMO,SAAS,qBACd,UACA,QACA,OACA,KACQ;AACR,QAAM,EAAE,SAAS,YAAY,UAAU,IAAI;AAE3C,MAAI,MAAM;AACV,MAAI,OAAO,SAAS,SAAS;AAC7B,MAAI,QAAQ,SAAS;AACnB,QAAI,WAAW,YAAY;AACzB,cAAQ,cAAc,MAAM,SAAS,SAAS,EAAE,MAAM,MAAM;AAC5D,aAAO;AAAA,IACT;AAEA,QAAI,UAAU,YAAY;AAExB,YAAM,cAAc,KAAK,IAAI;AAAA,IAC/B,OAAO;AACL,aAAO;AAAA,IACT;AAAA,EACF;AACA,QAAM,UAAU;AAChB,QAAM,aAAa;AAEnB,SAAQ,MAAM,YAAY,aAAa,UAAU,QAAQ,KAAK,IAAI;AACpE;;;ACHO,SAAS,MAAS,KAA4B;AACnD,SAAO,OAAO,QAAQ,WAAW,KAAK,MAAM,GAAG,IAAK;AACtD;;;ACvFO,IAAM,aAAyB,SAAU,KAAK,QAAQ;AAC3D,QAAM,SAAS,MAAM,GAA8B;AAEnD,MAAI,EAAE,cAAc,SAAS;AAC3B,WAAO,IAAI,SAAS,QAA2D,MAAM;AAAA,EACvF;AAEA,QAAM,WAAiC,CAAC;AACxC,QAAM,UAAoB,CAAC;AAC3B,QAAM,iBAAoC,CAAC;AAC3C,QAAM,QAAkB,CAAC;AACzB,QAAM,aAAuB,CAAC;AAE9B;AAAA,IACE;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,QAAM,SAA2B;AAAA,IAC/B,SAAS;AAAA,IACT,MAAM,OAAO;AAAA,IACb;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,SAAO,oBAAoB,MAAM;AACnC;AAEA,SAAS,QACP,OACA,QACA,UACA,SACA,gBACA,OACA,YACA,YACA,cACA,UACA,YACA;AACA,QAAM,EAAE,SAAS,IAAI;AACrB,WAAS,IAAI,GAAG,IAAI,SAAS,QAAQ,KAAK;AACxC,UAAM,EAAE,KAAK,OAAO,IAAI,SAAS,CAAC;AAElC,QAAI,KAAK;AACT,QAAI,KAAK;AACT,QAAI,IAAI,IAAI,SAAS,QAAQ;AAC3B,YAAM,aAAa,SAAS,IAAI,CAAC,EAAE;AACnC,WAAK,KAAK,IAAI,UAAU,aAAa,WAAW,IAAI;AAEpD,UAAI,OAAO,UAAU;AACnB,aAAK,KAAK,IAAI,YAAY,eAAe,WAAW,MAAM;AAAA,MAC5D,WAAW,KAAK,UAAU;AACxB,aAAK,eAAe,WAAW;AAAA,MACjC;AAAA,IACF;AAEA;AAAA,MACE;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,aAAa,OAAO;AAAA,MACpB,eAAe,OAAO;AAAA,MACtB;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACF;AAEA,SAAS,WACP,OACA,QACA,UACA,SACA,gBACA,OACA,YACA,YACA,cACA,UACA,YACA;AACA,QAAM,SAAS,MAAM,KAAK;AAC1B,MAAI,cAAc,OAAQ,QAAO,QAAQ,GAAI,SAAmD;AAEhG,QAAM,MAAM,IAAI,SAAS,QAAQ,MAAM;AACvC,QAAM,gBAAgB,QAAQ;AAC9B,QAAM,cAAc,MAAM;AAC1B,QAAM,UAAU,gBAAgB,GAAG;AACnC,QAAM,EAAE,iBAAiB,gBAAgB,UAAU,YAAY,QAAQ,IAAI;AAE3E,SAAO,SAAS,eAAe;AAC/B,SAAO,OAAO,IAAI,KAAK;AAEvB,MAAI,SAAU,QAAO,gBAAgB,QAAQ;AAAA,MACxC,UAAS,IAAI,GAAG,IAAI,gBAAgB,QAAQ,IAAK,gBAAe,KAAK,IAAI;AAE9E,MAAI,QAAS,UAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,IAAK,YAAW,KAAK,QAAQ,CAAC,IAAI,aAAa;AAEhG,WAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;AACvC,UAAM,QAAQ,aAAa;AAM3B,QAAI,QAAQ,SAAU;AAItB,UAAM,MAAM,QAAQ,UAAU,KAAK;AAGnC,UAAM,UAAU,MAAM,IAAI,eAAe;AAEzC,UAAM,OAAO,QAAQ,CAAC;AACtB,aAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,YAAM,MAAM,KAAK,CAAC;AAClB,YAAM,SAAS,UAAU,IAAI,MAAM;AAInC,UAAI,UAAU,YAAY,UAAU,WAAY;AAEhD,UAAI,IAAI,WAAW,GAAG;AACpB,YAAI,KAAK,CAAC,MAAM,CAAC;AACjB;AAAA,MACF;AAEA,YAAM,eAAe,gBAAgB,IAAI,aAAa;AACtD,YAAM,aAAa,IAAI,WAAW;AAClC,YAAM,eAAe,IAAI,aAAa;AACtC,UAAI;AAAA,QACF,IAAI,WAAW,IACX,CAAC,QAAQ,cAAc,YAAY,YAAY,IAC/C,CAAC,QAAQ,cAAc,YAAY,cAAc,cAAc,IAAI,WAAW,CAAC;AAAA,MACrF;AAAA,IACF;AAAA,EACF;AACF;AAEA,SAAS,OAAU,KAAU,OAAY;AACvC,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,IAAK,KAAI,KAAK,MAAM,CAAC,CAAC;AAC1D;AAEA,SAAS,QAAW,KAAY,OAAoB;AAClD,WAAS,IAAI,IAAI,QAAQ,KAAK,OAAO,IAAK,KAAI,CAAC,IAAI,CAAC;AACpD,SAAO,IAAI,KAAK;AAClB;;;ARhHA,IAAM,gBAAgB;AACtB,IAAM,kBAAkB;AAEjB,IAAM,oBAAoB;AAC1B,IAAM,uBAAuB;AAI7B,IAAM,WAAN,MAAoC;AAAA,EAkBzC,YAAY,KAAyB,QAAwB;AAC3D,UAAM,WAAW,OAAO,QAAQ;AAChC,QAAI,CAAC,YAAa,IAAyC,aAAc,QAAO;AAEhF,UAAM,SAAS,MAAM,GAAwC;AAE7D,UAAM,EAAE,SAAS,MAAM,OAAO,YAAY,SAAS,eAAe,IAAI;AACtE,SAAK,UAAU;AACf,SAAK,OAAO;AACZ,SAAK,QAAQ,SAAS,CAAC;AACvB,SAAK,aAAa;AAClB,SAAK,UAAU;AACf,SAAK,iBAAiB;AACtB,SAAK,aAAa,OAAO,cAAe,OAAkB,uBAAuB;AAEjF,UAAM,UAAU,SAAS,QAAQ,UAAU;AAC3C,SAAK,kBAAkB,QAAQ,IAAI,OAAO;AAE1C,UAAM,EAAE,SAAS,IAAI;AACrB,QAAI,OAAO,aAAa,UAAU;AAChC,WAAK,WAAW;AAChB,WAAK,WAAW;AAAA,IAClB,WAAW,MAAM,QAAQ,QAAQ,GAAG;AAClC,WAAK,WAAW;AAChB,WAAK,WAAW,UAAU,UAAU,QAAQ;AAAA,IAC9C,WAAY,OAAyC,UAAU;AAC7D,YAAM,IAAI,MAAM,4EAA4E;AAAA,IAC9F,OAAO;AACL,YAAM,IAAI,MAAM,uBAAuB,KAAK,UAAU,MAAM,CAAC,EAAE;AAAA,IACjE;AAEA,SAAK,eAAe,cAAc;AAClC,SAAK,aAAa;AAClB,SAAK,iBAAiB;AAAA,EACxB;AACF;AAMA,SAAS,KAAK,KAAyB;AACrC,SAAO;AACT;AAKO,SAAS,gBAAgB,KAA6C;AAzJ7E;AA0JE,UAAQ,gBAAK,GAAG,GAAE,aAAV,eAAU,eAAa,+BAAO,KAAK,GAAG,EAAE,QAAS;AAC3D;AAKO,SAAS,gBAAgB,KAAuD;AAhKvF;AAiKE,UAAQ,UAAK,GAAG,GAAE,aAAV,GAAU,eAAa,+BAAO,KAAK,GAAG,EAAE,QAAS;AAC3D;AAMO,SAAS,aACd,KACA,MACA,QACmC;AACnC,QAAM,UAAU,gBAAgB,GAAG;AAInC,MAAI,QAAQ,QAAQ,OAAQ,QAAO;AAEnC,QAAM,WAAW,QAAQ,IAAI;AAC7B,QAAM,QAAQ;AAAA,IACZ;AAAA,IACA,KAAK,GAAG,EAAE;AAAA,IACV;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,SAAO,UAAU,KAAK,OAAO,SAAS,KAAK;AAC7C;AAOO,SAAS,oBACd,KACA,QAC0C;AAC1C,MAAI,EAAE,MAAM,QAAQ,KAAK,IAAI;AAC7B;AACA,MAAI,OAAO,EAAG,OAAM,IAAI,MAAM,aAAa;AAC3C,MAAI,SAAS,EAAG,OAAM,IAAI,MAAM,eAAe;AAE/C,QAAM,UAAU,gBAAgB,GAAG;AAInC,MAAI,QAAQ,QAAQ,OAAQ,QAAO,SAAS,MAAM,MAAM,MAAM,IAAI;AAElE,QAAM,WAAW,QAAQ,IAAI;AAC7B,QAAM,QAAQ;AAAA,IACZ;AAAA,IACA,KAAK,GAAG,EAAE;AAAA,IACV;AAAA,IACA;AAAA,IACA,QAAQ;AAAA,EACV;AAEA,MAAI,UAAU,GAAI,QAAO,SAAS,MAAM,MAAM,MAAM,IAAI;AAExD,QAAM,UAAU,SAAS,KAAK;AAC9B,MAAI,QAAQ,WAAW,EAAG,QAAO,SAAS,MAAM,MAAM,MAAM,IAAI;AAEhE,QAAM,EAAE,OAAO,gBAAgB,IAAI;AACnC,SAAO;AAAA,IACL,gBAAgB,QAAQ,aAAa,CAAC;AAAA,IACtC,QAAQ,WAAW,IAAI;AAAA,IACvB,QAAQ,aAAa;AAAA,IACrB,QAAQ,WAAW,IAAI,MAAM,QAAQ,WAAW,CAAC,IAAI;AAAA,EACvD;AACF;AAKO,SAAS,qBACd,KACA,QAC4C;AAC5C,QAAM,EAAE,QAAQ,MAAM,QAAQ,KAAK,IAAI;AACvC,SAAO,kBAAkB,KAAK,QAAQ,MAAM,QAAQ,QAAQ,sBAAsB,KAAK;AACzF;AAKO,SAAS,yBAAyB,KAAe,QAA0C;AAChG,QAAM,EAAE,QAAQ,MAAM,QAAQ,KAAK,IAAI;AAEvC,SAAO,kBAAkB,KAAK,QAAQ,MAAM,QAAQ,QAAQ,mBAAmB,IAAI;AACrF;AAKO,SAAS,YAAY,KAAe,IAA0C;AACnF,QAAM,UAAU,gBAAgB,GAAG;AACnC,QAAM,EAAE,OAAO,gBAAgB,IAAI;AAEnC,WAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;AACvC,UAAM,OAAO,QAAQ,CAAC;AACtB,aAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,YAAM,MAAM,KAAK,CAAC;AAElB,YAAM,gBAAgB,IAAI;AAC1B,YAAM,kBAAkB,IAAI,CAAC;AAC7B,UAAI,SAAS;AACb,UAAI,eAAe;AACnB,UAAI,iBAAiB;AACrB,UAAI,OAAO;AACX,UAAI,IAAI,WAAW,GAAG;AACpB,iBAAS,gBAAgB,IAAI,CAAC,CAAC;AAC/B,uBAAe,IAAI,CAAC,IAAI;AACxB,yBAAiB,IAAI,CAAC;AAAA,MACxB;AACA,UAAI,IAAI,WAAW,EAAG,QAAO,MAAM,IAAI,CAAC,CAAC;AAEzC,SAAG;AAAA,QACD;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAgB;AAAA,IAClB;AAAA,EACF;AACF;AAEA,SAAS,YAAY,KAAe,QAAwB;AAC1D,QAAM,EAAE,SAAS,gBAAgB,IAAI;AACrC,MAAI,QAAQ,QAAQ,QAAQ,MAAM;AAClC,MAAI,UAAU,GAAI,SAAQ,gBAAgB,QAAQ,MAAM;AACxD,SAAO;AACT;AAKO,SAAS,iBAAiB,KAAe,QAA+B;AAC7E,QAAM,EAAE,eAAe,IAAI;AAC3B,MAAI,kBAAkB,KAAM,QAAO;AACnC,QAAM,QAAQ,YAAY,KAAK,MAAM;AACrC,SAAO,UAAU,KAAK,OAAO,eAAe,KAAK;AACnD;AAKO,SAAS,UAAU,KAAe,QAAyB;AAChE,QAAM,EAAE,WAAW,IAAI;AACvB,MAAI,cAAc,KAAM,QAAO;AAC/B,QAAM,QAAQ,YAAY,KAAK,MAAM;AACrC,SAAO,UAAU,KAAK,QAAQ,WAAW,SAAS,KAAK;AACzD;AAMO,SAAS,oBAAoB,KAAuB,QAA2B;AACpF,QAAM,SAAS,IAAI,SAAS,MAAM,KAAK,CAAC,CAAC,GAAG,MAAM;AAClD,OAAK,MAAM,EAAE,WAAW,IAAI;AAC5B,SAAO;AACT;AAMO,SAAS,WACd,KACkF;AAClF,SAAO,MAAM,KAAK,gBAAgB,GAAG,CAAC;AACxC;AAMO,SAAS,WAAW,KAAiC;AAC1D,SAAO,MAAM,KAAK,gBAAgB,GAAG,CAAC;AACxC;AAEA,SAAS,MACP,KACA,UACwD;AACxD,SAAO;AAAA,IACL,SAAS,IAAI;AAAA,IACb,MAAM,IAAI;AAAA,IACV,OAAO,IAAI;AAAA,IACX,YAAY,IAAI;AAAA,IAChB,SAAS,IAAI;AAAA,IACb,gBAAgB,IAAI;AAAA,IACpB;AAAA,IACA,YAAY,IAAI,cAAe,IAAe;AAAA,EAChD;AACF;AASA,SAAS,SACP,QACA,MACA,QACA,MAC0C;AAC1C,SAAO,EAAE,QAAQ,MAAM,QAAQ,KAAK;AACtC;AAIA,SAAS,SACP,MACA,QAC4C;AAC5C,SAAO,EAAE,MAAM,OAAO;AACxB;AAgBA,SAAS,qBACP,UACA,MACA,MACA,QACA,MACQ;AACR,MAAI,QAAQ,qBAAqB,UAAU,QAAQ,MAAM,IAAI;AAC7D,MAAI,OAAS;AACX,aAAS,SAAS,oBAAoB,aAAa,YAAY,UAAU,QAAQ,KAAK;AAAA,EACxF,WAAW,SAAS,kBAAmB;AAEvC,MAAI,UAAU,MAAM,UAAU,SAAS,OAAQ,QAAO;AACtD,SAAO;AACT;AAEA,SAAS,wBACP,UACA,MACA,MACA,QACA,MACoB;AACpB,MAAI,MAAM,qBAAqB,UAAU,MAAM,MAAM,QAAQ,oBAAoB;AAQjF,MAAI,CAAC,SAAW,SAAS,kBAAmB;AAE5C,MAAI,QAAQ,MAAM,QAAQ,SAAS,OAAQ,QAAO,CAAC;AAKnD,QAAM,gBAAgB,QAAU,SAAS,SAAS,GAAG,EAAE,MAAM;AAG7D,MAAI,CAAC,MAAS,OAAM,WAAW,UAAU,eAAe,GAAG;AAC3D,QAAM,MAAM,WAAW,UAAU,eAAe,GAAG;AAEnD,QAAM,SAAS,CAAC;AAChB,SAAO,OAAO,KAAK,OAAO;AACxB,UAAM,UAAU,SAAS,GAAG;AAC5B,WAAO,KAAK,SAAS,QAAQ,kBAAkB,IAAI,GAAG,QAAQ,oBAAoB,CAAC,CAAC;AAAA,EACtF;AACA,SAAO;AACT;AAkBA,SAAS,kBACP,KACA,QACA,MACA,QACA,MACA,KACiE;AA5dnE;AA6dE;AACA,MAAI,OAAO,EAAG,OAAM,IAAI,MAAM,aAAa;AAC3C,MAAI,SAAS,EAAG,OAAM,IAAI,MAAM,eAAe;AAE/C,QAAM,EAAE,SAAS,gBAAgB,IAAI;AACrC,MAAIC,eAAc,QAAQ,QAAQ,MAAM;AACxC,MAAIA,iBAAgB,GAAI,CAAAA,eAAc,gBAAgB,QAAQ,MAAM;AACpE,MAAIA,iBAAgB,GAAI,QAAO,MAAM,CAAC,IAAI,SAAS,MAAM,IAAI;AAE7D,QAAM,iBAAiB,UAAK,GAAG,GAAE,mBAAV,GAAU,iBAAmB,QAAQ,IAAI,aAAa;AAC7E,QAAM,aAAa,UAAK,GAAG,GAAE,eAAV,GAAU,aAAe,eAAe,gBAAgB,GAAG,GAAG,aAAa;AAE9F,QAAM,WAAW,UAAUA,YAAW,EAAE,IAAI;AAC5C,MAAI,YAAY,KAAM,QAAO,MAAM,CAAC,IAAI,SAAS,MAAM,IAAI;AAE3D,QAAM,OAAO,cAAcA,YAAW;AAEtC,MAAI,IAAK,QAAO,wBAAwB,UAAU,MAAM,MAAM,QAAQ,IAAI;AAE1E,QAAM,QAAQ,qBAAqB,UAAU,MAAM,MAAM,QAAQ,IAAI;AACrE,MAAI,UAAU,GAAI,QAAO,SAAS,MAAM,IAAI;AAE5C,QAAM,UAAU,SAAS,KAAK;AAC9B,SAAO,SAAS,QAAQ,kBAAkB,IAAI,GAAG,QAAQ,oBAAoB,CAAC;AAChF;", | |
| 5 | "names": ["module", "module", "resolveUri", "sourceIndex", "sourceIndex"] | |
| 6 | } |
| … | … | @@ -0,0 +1,67 @@ |
| 1 | { | |
| 2 | "name": "@jridgewell/trace-mapping", | |
| 3 | "version": "0.3.31", | |
| 4 | "description": "Trace the original position through a source map", | |
| 5 | "keywords": [ | |
| 6 | "source", | |
| 7 | "map" | |
| 8 | ], | |
| 9 | "main": "dist/trace-mapping.umd.js", | |
| 10 | "module": "dist/trace-mapping.mjs", | |
| 11 | "types": "types/trace-mapping.d.cts", | |
| 12 | "files": [ | |
| 13 | "dist", | |
| 14 | "src", | |
| 15 | "types" | |
| 16 | ], | |
| 17 | "exports": { | |
| 18 | ".": [ | |
| 19 | { | |
| 20 | "import": { | |
| 21 | "types": "./types/trace-mapping.d.mts", | |
| 22 | "default": "./dist/trace-mapping.mjs" | |
| 23 | }, | |
| 24 | "default": { | |
| 25 | "types": "./types/trace-mapping.d.cts", | |
| 26 | "default": "./dist/trace-mapping.umd.js" | |
| 27 | } | |
| 28 | }, | |
| 29 | "./dist/trace-mapping.umd.js" | |
| 30 | ], | |
| 31 | "./package.json": "./package.json" | |
| 32 | }, | |
| 33 | "scripts": { | |
| 34 | "benchmark": "run-s build:code benchmark:*", | |
| 35 | "benchmark:install": "cd benchmark && npm install", | |
| 36 | "benchmark:only": "node --expose-gc benchmark/index.mjs", | |
| 37 | "build": "run-s -n build:code build:types", | |
| 38 | "build:code": "node ../../esbuild.mjs trace-mapping.ts", | |
| 39 | "build:types": "run-s build:types:force build:types:emit build:types:mts", | |
| 40 | "build:types:force": "rimraf tsconfig.build.tsbuildinfo", | |
| 41 | "build:types:emit": "tsc --project tsconfig.build.json", | |
| 42 | "build:types:mts": "node ../../mts-types.mjs", | |
| 43 | "clean": "run-s -n clean:code clean:types", | |
| 44 | "clean:code": "tsc --build --clean tsconfig.build.json", | |
| 45 | "clean:types": "rimraf dist types", | |
| 46 | "test": "run-s -n test:types test:only test:format", | |
| 47 | "test:format": "prettier --check '{src,test}/**/*.ts'", | |
| 48 | "test:only": "mocha", | |
| 49 | "test:types": "eslint '{src,test}/**/*.ts'", | |
| 50 | "lint": "run-s -n lint:types lint:format", | |
| 51 | "lint:format": "npm run test:format -- --write", | |
| 52 | "lint:types": "npm run test:types -- --fix", | |
| 53 | "prepublishOnly": "npm run-s -n build test" | |
| 54 | }, | |
| 55 | "homepage": "https://github.com/jridgewell/sourcemaps/tree/main/packages/trace-mapping", | |
| 56 | "repository": { | |
| 57 | "type": "git", | |
| 58 | "url": "git+https://github.com/jridgewell/sourcemaps.git", | |
| 59 | "directory": "packages/trace-mapping" | |
| 60 | }, | |
| 61 | "author": "Justin Ridgewell <justin@ridgewell.name>", | |
| 62 | "license": "MIT", | |
| 63 | "dependencies": { | |
| 64 | "@jridgewell/resolve-uri": "^3.1.0", | |
| 65 | "@jridgewell/sourcemap-codec": "^1.4.14" | |
| 66 | } | |
| 67 | } |
| … | … | @@ -0,0 +1,115 @@ |
| 1 | import type { SourceMapSegment, ReverseSegment } from './sourcemap-segment'; | |
| 2 | import { COLUMN } from './sourcemap-segment'; | |
| 3 | ||
| 4 | export type MemoState = { | |
| 5 | lastKey: number; | |
| 6 | lastNeedle: number; | |
| 7 | lastIndex: number; | |
| 8 | }; | |
| 9 | ||
| 10 | export let found = false; | |
| 11 | ||
| 12 | /** | |
| 13 | * A binary search implementation that returns the index if a match is found. | |
| 14 | * If no match is found, then the left-index (the index associated with the item that comes just | |
| 15 | * before the desired index) is returned. To maintain proper sort order, a splice would happen at | |
| 16 | * the next index: | |
| 17 | * | |
| 18 | * ```js | |
| 19 | * const array = [1, 3]; | |
| 20 | * const needle = 2; | |
| 21 | * const index = binarySearch(array, needle, (item, needle) => item - needle); | |
| 22 | * | |
| 23 | * assert.equal(index, 0); | |
| 24 | * array.splice(index + 1, 0, needle); | |
| 25 | * assert.deepEqual(array, [1, 2, 3]); | |
| 26 | * ``` | |
| 27 | */ | |
| 28 | export function binarySearch( | |
| 29 | haystack: SourceMapSegment[] | ReverseSegment[], | |
| 30 | needle: number, | |
| 31 | low: number, | |
| 32 | high: number, | |
| 33 | ): number { | |
| 34 | while (low <= high) { | |
| 35 | const mid = low + ((high - low) >> 1); | |
| 36 | const cmp = haystack[mid][COLUMN] - needle; | |
| 37 | ||
| 38 | if (cmp === 0) { | |
| 39 | found = true; | |
| 40 | return mid; | |
| 41 | } | |
| 42 | ||
| 43 | if (cmp < 0) { | |
| 44 | low = mid + 1; | |
| 45 | } else { | |
| 46 | high = mid - 1; | |
| 47 | } | |
| 48 | } | |
| 49 | ||
| 50 | found = false; | |
| 51 | return low - 1; | |
| 52 | } | |
| 53 | ||
| 54 | export function upperBound( | |
| 55 | haystack: SourceMapSegment[] | ReverseSegment[], | |
| 56 | needle: number, | |
| 57 | index: number, | |
| 58 | ): number { | |
| 59 | for (let i = index + 1; i < haystack.length; index = i++) { | |
| 60 | if (haystack[i][COLUMN] !== needle) break; | |
| 61 | } | |
| 62 | return index; | |
| 63 | } | |
| 64 | ||
| 65 | export function lowerBound( | |
| 66 | haystack: SourceMapSegment[] | ReverseSegment[], | |
| 67 | needle: number, | |
| 68 | index: number, | |
| 69 | ): number { | |
| 70 | for (let i = index - 1; i >= 0; index = i--) { | |
| 71 | if (haystack[i][COLUMN] !== needle) break; | |
| 72 | } | |
| 73 | return index; | |
| 74 | } | |
| 75 | ||
| 76 | export function memoizedState(): MemoState { | |
| 77 | return { | |
| 78 | lastKey: -1, | |
| 79 | lastNeedle: -1, | |
| 80 | lastIndex: -1, | |
| 81 | }; | |
| 82 | } | |
| 83 | ||
| 84 | /** | |
| 85 | * This overly complicated beast is just to record the last tested line/column and the resulting | |
| 86 | * index, allowing us to skip a few tests if mappings are monotonically increasing. | |
| 87 | */ | |
| 88 | export function memoizedBinarySearch( | |
| 89 | haystack: SourceMapSegment[] | ReverseSegment[], | |
| 90 | needle: number, | |
| 91 | state: MemoState, | |
| 92 | key: number, | |
| 93 | ): number { | |
| 94 | const { lastKey, lastNeedle, lastIndex } = state; | |
| 95 | ||
| 96 | let low = 0; | |
| 97 | let high = haystack.length - 1; | |
| 98 | if (key === lastKey) { | |
| 99 | if (needle === lastNeedle) { | |
| 100 | found = lastIndex !== -1 && haystack[lastIndex][COLUMN] === needle; | |
| 101 | return lastIndex; | |
| 102 | } | |
| 103 | ||
| 104 | if (needle >= lastNeedle) { | |
| 105 | // lastIndex may be -1 if the previous needle was not found. | |
| 106 | low = lastIndex === -1 ? 0 : lastIndex; | |
| 107 | } else { | |
| 108 | high = lastIndex; | |
| 109 | } | |
| 110 | } | |
| 111 | state.lastKey = key; | |
| 112 | state.lastNeedle = needle; | |
| 113 | ||
| 114 | return (state.lastIndex = binarySearch(haystack, needle, low, high)); | |
| 115 | } |
| … | … | @@ -0,0 +1,41 @@ |
| 1 | import { COLUMN, SOURCES_INDEX, SOURCE_LINE, SOURCE_COLUMN } from './sourcemap-segment'; | |
| 2 | import { sortComparator } from './sort'; | |
| 3 | ||
| 4 | import type { ReverseSegment, SourceMapSegment } from './sourcemap-segment'; | |
| 5 | ||
| 6 | export type Source = ReverseSegment[][]; | |
| 7 | ||
| 8 | // Rebuilds the original source files, with mappings that are ordered by source line/column instead | |
| 9 | // of generated line/column. | |
| 10 | export default function buildBySources( | |
| 11 | decoded: readonly SourceMapSegment[][], | |
| 12 | memos: unknown[], | |
| 13 | ): Source[] { | |
| 14 | const sources: Source[] = memos.map(() => []); | |
| 15 | ||
| 16 | for (let i = 0; i < decoded.length; i++) { | |
| 17 | const line = decoded[i]; | |
| 18 | for (let j = 0; j < line.length; j++) { | |
| 19 | const seg = line[j]; | |
| 20 | if (seg.length === 1) continue; | |
| 21 | ||
| 22 | const sourceIndex = seg[SOURCES_INDEX]; | |
| 23 | const sourceLine = seg[SOURCE_LINE]; | |
| 24 | const sourceColumn = seg[SOURCE_COLUMN]; | |
| 25 | ||
| 26 | const source = sources[sourceIndex]; | |
| 27 | const segs = (source[sourceLine] ||= []); | |
| 28 | segs.push([sourceColumn, i, seg[COLUMN]]); | |
| 29 | } | |
| 30 | } | |
| 31 | ||
| 32 | for (let i = 0; i < sources.length; i++) { | |
| 33 | const source = sources[i]; | |
| 34 | for (let j = 0; j < source.length; j++) { | |
| 35 | const line = source[j]; | |
| 36 | if (line) line.sort(sortComparator); | |
| 37 | } | |
| 38 | } | |
| 39 | ||
| 40 | return sources; | |
| 41 | } |
| … | … | @@ -0,0 +1,192 @@ |
| 1 | import { TraceMap, presortedDecodedMap, decodedMappings } from './trace-mapping'; | |
| 2 | import { | |
| 3 | COLUMN, | |
| 4 | SOURCES_INDEX, | |
| 5 | SOURCE_LINE, | |
| 6 | SOURCE_COLUMN, | |
| 7 | NAMES_INDEX, | |
| 8 | } from './sourcemap-segment'; | |
| 9 | import { parse } from './types'; | |
| 10 | ||
| 11 | import type { | |
| 12 | DecodedSourceMap, | |
| 13 | DecodedSourceMapXInput, | |
| 14 | EncodedSourceMapXInput, | |
| 15 | SectionedSourceMapXInput, | |
| 16 | SectionedSourceMapInput, | |
| 17 | SectionXInput, | |
| 18 | Ro, | |
| 19 | } from './types'; | |
| 20 | import type { SourceMapSegment } from './sourcemap-segment'; | |
| 21 | ||
| 22 | type FlattenMap = { | |
| 23 | new (map: Ro<SectionedSourceMapInput>, mapUrl?: string | null): TraceMap; | |
| 24 | (map: Ro<SectionedSourceMapInput>, mapUrl?: string | null): TraceMap; | |
| 25 | }; | |
| 26 | ||
| 27 | export const FlattenMap: FlattenMap = function (map, mapUrl) { | |
| 28 | const parsed = parse(map as SectionedSourceMapInput); | |
| 29 | ||
| 30 | if (!('sections' in parsed)) { | |
| 31 | return new TraceMap(parsed as DecodedSourceMapXInput | EncodedSourceMapXInput, mapUrl); | |
| 32 | } | |
| 33 | ||
| 34 | const mappings: SourceMapSegment[][] = []; | |
| 35 | const sources: string[] = []; | |
| 36 | const sourcesContent: (string | null)[] = []; | |
| 37 | const names: string[] = []; | |
| 38 | const ignoreList: number[] = []; | |
| 39 | ||
| 40 | recurse( | |
| 41 | parsed, | |
| 42 | mapUrl, | |
| 43 | mappings, | |
| 44 | sources, | |
| 45 | sourcesContent, | |
| 46 | names, | |
| 47 | ignoreList, | |
| 48 | 0, | |
| 49 | 0, | |
| 50 | Infinity, | |
| 51 | Infinity, | |
| 52 | ); | |
| 53 | ||
| 54 | const joined: DecodedSourceMap = { | |
| 55 | version: 3, | |
| 56 | file: parsed.file, | |
| 57 | names, | |
| 58 | sources, | |
| 59 | sourcesContent, | |
| 60 | mappings, | |
| 61 | ignoreList, | |
| 62 | }; | |
| 63 | ||
| 64 | return presortedDecodedMap(joined); | |
| 65 | } as FlattenMap; | |
| 66 | ||
| 67 | function recurse( | |
| 68 | input: SectionedSourceMapXInput, | |
| 69 | mapUrl: string | null | undefined, | |
| 70 | mappings: SourceMapSegment[][], | |
| 71 | sources: string[], | |
| 72 | sourcesContent: (string | null)[], | |
| 73 | names: string[], | |
| 74 | ignoreList: number[], | |
| 75 | lineOffset: number, | |
| 76 | columnOffset: number, | |
| 77 | stopLine: number, | |
| 78 | stopColumn: number, | |
| 79 | ) { | |
| 80 | const { sections } = input; | |
| 81 | for (let i = 0; i < sections.length; i++) { | |
| 82 | const { map, offset } = sections[i]; | |
| 83 | ||
| 84 | let sl = stopLine; | |
| 85 | let sc = stopColumn; | |
| 86 | if (i + 1 < sections.length) { | |
| 87 | const nextOffset = sections[i + 1].offset; | |
| 88 | sl = Math.min(stopLine, lineOffset + nextOffset.line); | |
| 89 | ||
| 90 | if (sl === stopLine) { | |
| 91 | sc = Math.min(stopColumn, columnOffset + nextOffset.column); | |
| 92 | } else if (sl < stopLine) { | |
| 93 | sc = columnOffset + nextOffset.column; | |
| 94 | } | |
| 95 | } | |
| 96 | ||
| 97 | addSection( | |
| 98 | map, | |
| 99 | mapUrl, | |
| 100 | mappings, | |
| 101 | sources, | |
| 102 | sourcesContent, | |
| 103 | names, | |
| 104 | ignoreList, | |
| 105 | lineOffset + offset.line, | |
| 106 | columnOffset + offset.column, | |
| 107 | sl, | |
| 108 | sc, | |
| 109 | ); | |
| 110 | } | |
| 111 | } | |
| 112 | ||
| 113 | function addSection( | |
| 114 | input: SectionXInput['map'], | |
| 115 | mapUrl: string | null | undefined, | |
| 116 | mappings: SourceMapSegment[][], | |
| 117 | sources: string[], | |
| 118 | sourcesContent: (string | null)[], | |
| 119 | names: string[], | |
| 120 | ignoreList: number[], | |
| 121 | lineOffset: number, | |
| 122 | columnOffset: number, | |
| 123 | stopLine: number, | |
| 124 | stopColumn: number, | |
| 125 | ) { | |
| 126 | const parsed = parse(input); | |
| 127 | if ('sections' in parsed) return recurse(...(arguments as unknown as Parameters<typeof recurse>)); | |
| 128 | ||
| 129 | const map = new TraceMap(parsed, mapUrl); | |
| 130 | const sourcesOffset = sources.length; | |
| 131 | const namesOffset = names.length; | |
| 132 | const decoded = decodedMappings(map); | |
| 133 | const { resolvedSources, sourcesContent: contents, ignoreList: ignores } = map; | |
| 134 | ||
| 135 | append(sources, resolvedSources); | |
| 136 | append(names, map.names); | |
| 137 | ||
| 138 | if (contents) append(sourcesContent, contents); | |
| 139 | else for (let i = 0; i < resolvedSources.length; i++) sourcesContent.push(null); | |
| 140 | ||
| 141 | if (ignores) for (let i = 0; i < ignores.length; i++) ignoreList.push(ignores[i] + sourcesOffset); | |
| 142 | ||
| 143 | for (let i = 0; i < decoded.length; i++) { | |
| 144 | const lineI = lineOffset + i; | |
| 145 | ||
| 146 | // We can only add so many lines before we step into the range that the next section's map | |
| 147 | // controls. When we get to the last line, then we'll start checking the segments to see if | |
| 148 | // they've crossed into the column range. But it may not have any columns that overstep, so we | |
| 149 | // still need to check that we don't overstep lines, too. | |
| 150 | if (lineI > stopLine) return; | |
| 151 | ||
| 152 | // The out line may already exist in mappings (if we're continuing the line started by a | |
| 153 | // previous section). Or, we may have jumped ahead several lines to start this section. | |
| 154 | const out = getLine(mappings, lineI); | |
| 155 | // On the 0th loop, the section's column offset shifts us forward. On all other lines (since the | |
| 156 | // map can be multiple lines), it doesn't. | |
| 157 | const cOffset = i === 0 ? columnOffset : 0; | |
| 158 | ||
| 159 | const line = decoded[i]; | |
| 160 | for (let j = 0; j < line.length; j++) { | |
| 161 | const seg = line[j]; | |
| 162 | const column = cOffset + seg[COLUMN]; | |
| 163 | ||
| 164 | // If this segment steps into the column range that the next section's map controls, we need | |
| 165 | // to stop early. | |
| 166 | if (lineI === stopLine && column >= stopColumn) return; | |
| 167 | ||
| 168 | if (seg.length === 1) { | |
| 169 | out.push([column]); | |
| 170 | continue; | |
| 171 | } | |
| 172 | ||
| 173 | const sourcesIndex = sourcesOffset + seg[SOURCES_INDEX]; | |
| 174 | const sourceLine = seg[SOURCE_LINE]; | |
| 175 | const sourceColumn = seg[SOURCE_COLUMN]; | |
| 176 | out.push( | |
| 177 | seg.length === 4 | |
| 178 | ? [column, sourcesIndex, sourceLine, sourceColumn] | |
| 179 | : [column, sourcesIndex, sourceLine, sourceColumn, namesOffset + seg[NAMES_INDEX]], | |
| 180 | ); | |
| 181 | } | |
| 182 | } | |
| 183 | } | |
| 184 | ||
| 185 | function append<T>(arr: T[], other: T[]) { | |
| 186 | for (let i = 0; i < other.length; i++) arr.push(other[i]); | |
| 187 | } | |
| 188 | ||
| 189 | function getLine<T>(arr: T[][], index: number): T[] { | |
| 190 | for (let i = arr.length; i <= index; i++) arr[i] = []; | |
| 191 | return arr[index]; | |
| 192 | } |
| … | … | @@ -0,0 +1,16 @@ |
| 1 | import resolveUri from '@jridgewell/resolve-uri'; | |
| 2 | import stripFilename from './strip-filename'; | |
| 3 | ||
| 4 | type Resolve = (source: string | null) => string; | |
| 5 | export default function resolver( | |
| 6 | mapUrl: string | null | undefined, | |
| 7 | sourceRoot: string | undefined, | |
| 8 | ): Resolve { | |
| 9 | const from = stripFilename(mapUrl); | |
| 10 | // The sourceRoot is always treated as a directory, if it's not empty. | |
| 11 | // https://github.com/mozilla/source-map/blob/8cb3ee57/lib/util.js#L327 | |
| 12 | // https://github.com/chromium/chromium/blob/da4adbb3/third_party/blink/renderer/devtools/front_end/sdk/SourceMap.js#L400-L401 | |
| 13 | const prefix = sourceRoot ? sourceRoot + '/' : ''; | |
| 14 | ||
| 15 | return (source) => resolveUri(prefix + (source || ''), from); | |
| 16 | } |
| … | … | @@ -0,0 +1,45 @@ |
| 1 | import { COLUMN } from './sourcemap-segment'; | |
| 2 | ||
| 3 | import type { ReverseSegment, SourceMapSegment } from './sourcemap-segment'; | |
| 4 | ||
| 5 | export default function maybeSort( | |
| 6 | mappings: SourceMapSegment[][], | |
| 7 | owned: boolean, | |
| 8 | ): SourceMapSegment[][] { | |
| 9 | const unsortedIndex = nextUnsortedSegmentLine(mappings, 0); | |
| 10 | if (unsortedIndex === mappings.length) return mappings; | |
| 11 | ||
| 12 | // If we own the array (meaning we parsed it from JSON), then we're free to directly mutate it. If | |
| 13 | // not, we do not want to modify the consumer's input array. | |
| 14 | if (!owned) mappings = mappings.slice(); | |
| 15 | ||
| 16 | for (let i = unsortedIndex; i < mappings.length; i = nextUnsortedSegmentLine(mappings, i + 1)) { | |
| 17 | mappings[i] = sortSegments(mappings[i], owned); | |
| 18 | } | |
| 19 | return mappings; | |
| 20 | } | |
| 21 | ||
| 22 | function nextUnsortedSegmentLine(mappings: SourceMapSegment[][], start: number): number { | |
| 23 | for (let i = start; i < mappings.length; i++) { | |
| 24 | if (!isSorted(mappings[i])) return i; | |
| 25 | } | |
| 26 | return mappings.length; | |
| 27 | } | |
| 28 | ||
| 29 | function isSorted(line: SourceMapSegment[]): boolean { | |
| 30 | for (let j = 1; j < line.length; j++) { | |
| 31 | if (line[j][COLUMN] < line[j - 1][COLUMN]) { | |
| 32 | return false; | |
| 33 | } | |
| 34 | } | |
| 35 | return true; | |
| 36 | } | |
| 37 | ||
| 38 | function sortSegments(line: SourceMapSegment[], owned: boolean): SourceMapSegment[] { | |
| 39 | if (!owned) line = line.slice(); | |
| 40 | return line.sort(sortComparator); | |
| 41 | } | |
| 42 | ||
| 43 | export function sortComparator<T extends SourceMapSegment | ReverseSegment>(a: T, b: T): number { | |
| 44 | return a[COLUMN] - b[COLUMN]; | |
| 45 | } |
| … | … | @@ -0,0 +1,23 @@ |
| 1 | type GeneratedColumn = number; | |
| 2 | type SourcesIndex = number; | |
| 3 | type SourceLine = number; | |
| 4 | type SourceColumn = number; | |
| 5 | type NamesIndex = number; | |
| 6 | ||
| 7 | type GeneratedLine = number; | |
| 8 | ||
| 9 | export type SourceMapSegment = | |
| 10 | | [GeneratedColumn] | |
| 11 | | [GeneratedColumn, SourcesIndex, SourceLine, SourceColumn] | |
| 12 | | [GeneratedColumn, SourcesIndex, SourceLine, SourceColumn, NamesIndex]; | |
| 13 | ||
| 14 | export type ReverseSegment = [SourceColumn, GeneratedLine, GeneratedColumn]; | |
| 15 | ||
| 16 | export const COLUMN = 0; | |
| 17 | export const SOURCES_INDEX = 1; | |
| 18 | export const SOURCE_LINE = 2; | |
| 19 | export const SOURCE_COLUMN = 3; | |
| 20 | export const NAMES_INDEX = 4; | |
| 21 | ||
| 22 | export const REV_GENERATED_LINE = 1; | |
| 23 | export const REV_GENERATED_COLUMN = 2; |
| … | … | @@ -0,0 +1,8 @@ |
| 1 | /** | |
| 2 | * Removes everything after the last "/", but leaves the slash. | |
| 3 | */ | |
| 4 | export default function stripFilename(path: string | undefined | null): string { | |
| 5 | if (!path) return ''; | |
| 6 | const index = path.lastIndexOf('/'); | |
| 7 | return path.slice(0, index + 1); | |
| 8 | } |
| … | … | @@ -0,0 +1,502 @@ |
| 1 | import { encode, decode } from '@jridgewell/sourcemap-codec'; | |
| 2 | ||
| 3 | import resolver from './resolve'; | |
| 4 | import maybeSort from './sort'; | |
| 5 | import buildBySources from './by-source'; | |
| 6 | import { | |
| 7 | memoizedState, | |
| 8 | memoizedBinarySearch, | |
| 9 | upperBound, | |
| 10 | lowerBound, | |
| 11 | found as bsFound, | |
| 12 | } from './binary-search'; | |
| 13 | import { | |
| 14 | COLUMN, | |
| 15 | SOURCES_INDEX, | |
| 16 | SOURCE_LINE, | |
| 17 | SOURCE_COLUMN, | |
| 18 | NAMES_INDEX, | |
| 19 | REV_GENERATED_LINE, | |
| 20 | REV_GENERATED_COLUMN, | |
| 21 | } from './sourcemap-segment'; | |
| 22 | import { parse } from './types'; | |
| 23 | ||
| 24 | import type { SourceMapSegment, ReverseSegment } from './sourcemap-segment'; | |
| 25 | import type { | |
| 26 | SourceMapV3, | |
| 27 | DecodedSourceMap, | |
| 28 | EncodedSourceMap, | |
| 29 | InvalidOriginalMapping, | |
| 30 | OriginalMapping, | |
| 31 | InvalidGeneratedMapping, | |
| 32 | GeneratedMapping, | |
| 33 | SourceMapInput, | |
| 34 | Needle, | |
| 35 | SourceNeedle, | |
| 36 | SourceMap, | |
| 37 | EachMapping, | |
| 38 | Bias, | |
| 39 | XInput, | |
| 40 | SectionedSourceMap, | |
| 41 | Ro, | |
| 42 | } from './types'; | |
| 43 | import type { Source } from './by-source'; | |
| 44 | import type { MemoState } from './binary-search'; | |
| 45 | ||
| 46 | export type { SourceMapSegment } from './sourcemap-segment'; | |
| 47 | export type { | |
| 48 | SourceMap, | |
| 49 | DecodedSourceMap, | |
| 50 | EncodedSourceMap, | |
| 51 | Section, | |
| 52 | SectionedSourceMap, | |
| 53 | SourceMapV3, | |
| 54 | Bias, | |
| 55 | EachMapping, | |
| 56 | GeneratedMapping, | |
| 57 | InvalidGeneratedMapping, | |
| 58 | InvalidOriginalMapping, | |
| 59 | Needle, | |
| 60 | OriginalMapping, | |
| 61 | OriginalMapping as Mapping, | |
| 62 | SectionedSourceMapInput, | |
| 63 | SourceMapInput, | |
| 64 | SourceNeedle, | |
| 65 | XInput, | |
| 66 | EncodedSourceMapXInput, | |
| 67 | DecodedSourceMapXInput, | |
| 68 | SectionedSourceMapXInput, | |
| 69 | SectionXInput, | |
| 70 | } from './types'; | |
| 71 | ||
| 72 | interface PublicMap { | |
| 73 | _encoded: TraceMap['_encoded']; | |
| 74 | _decoded: TraceMap['_decoded']; | |
| 75 | _decodedMemo: TraceMap['_decodedMemo']; | |
| 76 | _bySources: TraceMap['_bySources']; | |
| 77 | _bySourceMemos: TraceMap['_bySourceMemos']; | |
| 78 | } | |
| 79 | ||
| 80 | const LINE_GTR_ZERO = '`line` must be greater than 0 (lines start at line 1)'; | |
| 81 | const COL_GTR_EQ_ZERO = '`column` must be greater than or equal to 0 (columns start at column 0)'; | |
| 82 | ||
| 83 | export const LEAST_UPPER_BOUND = -1; | |
| 84 | export const GREATEST_LOWER_BOUND = 1; | |
| 85 | ||
| 86 | export { FlattenMap, FlattenMap as AnyMap } from './flatten-map'; | |
| 87 | ||
| 88 | export class TraceMap implements SourceMap { | |
| 89 | declare version: SourceMapV3['version']; | |
| 90 | declare file: SourceMapV3['file']; | |
| 91 | declare names: SourceMapV3['names']; | |
| 92 | declare sourceRoot: SourceMapV3['sourceRoot']; | |
| 93 | declare sources: SourceMapV3['sources']; | |
| 94 | declare sourcesContent: SourceMapV3['sourcesContent']; | |
| 95 | declare ignoreList: SourceMapV3['ignoreList']; | |
| 96 | ||
| 97 | declare resolvedSources: string[]; | |
| 98 | declare private _encoded: string | undefined; | |
| 99 | ||
| 100 | declare private _decoded: SourceMapSegment[][] | undefined; | |
| 101 | declare private _decodedMemo: MemoState; | |
| 102 | ||
| 103 | declare private _bySources: Source[] | undefined; | |
| 104 | declare private _bySourceMemos: MemoState[] | undefined; | |
| 105 | ||
| 106 | constructor(map: Ro<SourceMapInput>, mapUrl?: string | null) { | |
| 107 | const isString = typeof map === 'string'; | |
| 108 | if (!isString && (map as unknown as { _decodedMemo: any })._decodedMemo) return map as TraceMap; | |
| 109 | ||
| 110 | const parsed = parse(map as Exclude<SourceMapInput, TraceMap>); | |
| 111 | ||
| 112 | const { version, file, names, sourceRoot, sources, sourcesContent } = parsed; | |
| 113 | this.version = version; | |
| 114 | this.file = file; | |
| 115 | this.names = names || []; | |
| 116 | this.sourceRoot = sourceRoot; | |
| 117 | this.sources = sources; | |
| 118 | this.sourcesContent = sourcesContent; | |
| 119 | this.ignoreList = parsed.ignoreList || (parsed as XInput).x_google_ignoreList || undefined; | |
| 120 | ||
| 121 | const resolve = resolver(mapUrl, sourceRoot); | |
| 122 | this.resolvedSources = sources.map(resolve); | |
| 123 | ||
| 124 | const { mappings } = parsed; | |
| 125 | if (typeof mappings === 'string') { | |
| 126 | this._encoded = mappings; | |
| 127 | this._decoded = undefined; | |
| 128 | } else if (Array.isArray(mappings)) { | |
| 129 | this._encoded = undefined; | |
| 130 | this._decoded = maybeSort(mappings, isString); | |
| 131 | } else if ((parsed as unknown as SectionedSourceMap).sections) { | |
| 132 | throw new Error(`TraceMap passed sectioned source map, please use FlattenMap export instead`); | |
| 133 | } else { | |
| 134 | throw new Error(`invalid source map: ${JSON.stringify(parsed)}`); | |
| 135 | } | |
| 136 | ||
| 137 | this._decodedMemo = memoizedState(); | |
| 138 | this._bySources = undefined; | |
| 139 | this._bySourceMemos = undefined; | |
| 140 | } | |
| 141 | } | |
| 142 | ||
| 143 | /** | |
| 144 | * Typescript doesn't allow friend access to private fields, so this just casts the map into a type | |
| 145 | * with public access modifiers. | |
| 146 | */ | |
| 147 | function cast(map: unknown): PublicMap { | |
| 148 | return map as any; | |
| 149 | } | |
| 150 | ||
| 151 | /** | |
| 152 | * Returns the encoded (VLQ string) form of the SourceMap's mappings field. | |
| 153 | */ | |
| 154 | export function encodedMappings(map: TraceMap): EncodedSourceMap['mappings'] { | |
| 155 | return (cast(map)._encoded ??= encode(cast(map)._decoded!)); | |
| 156 | } | |
| 157 | ||
| 158 | /** | |
| 159 | * Returns the decoded (array of lines of segments) form of the SourceMap's mappings field. | |
| 160 | */ | |
| 161 | export function decodedMappings(map: TraceMap): Readonly<DecodedSourceMap['mappings']> { | |
| 162 | return (cast(map)._decoded ||= decode(cast(map)._encoded!)); | |
| 163 | } | |
| 164 | ||
| 165 | /** | |
| 166 | * A low-level API to find the segment associated with a generated line/column (think, from a | |
| 167 | * stack trace). Line and column here are 0-based, unlike `originalPositionFor`. | |
| 168 | */ | |
| 169 | export function traceSegment( | |
| 170 | map: TraceMap, | |
| 171 | line: number, | |
| 172 | column: number, | |
| 173 | ): Readonly<SourceMapSegment> | null { | |
| 174 | const decoded = decodedMappings(map); | |
| 175 | ||
| 176 | // It's common for parent source maps to have pointers to lines that have no | |
| 177 | // mapping (like a "//# sourceMappingURL=") at the end of the child file. | |
| 178 | if (line >= decoded.length) return null; | |
| 179 | ||
| 180 | const segments = decoded[line]; | |
| 181 | const index = traceSegmentInternal( | |
| 182 | segments, | |
| 183 | cast(map)._decodedMemo, | |
| 184 | line, | |
| 185 | column, | |
| 186 | GREATEST_LOWER_BOUND, | |
| 187 | ); | |
| 188 | ||
| 189 | return index === -1 ? null : segments[index]; | |
| 190 | } | |
| 191 | ||
| 192 | /** | |
| 193 | * A higher-level API to find the source/line/column associated with a generated line/column | |
| 194 | * (think, from a stack trace). Line is 1-based, but column is 0-based, due to legacy behavior in | |
| 195 | * `source-map` library. | |
| 196 | */ | |
| 197 | export function originalPositionFor( | |
| 198 | map: TraceMap, | |
| 199 | needle: Needle, | |
| 200 | ): OriginalMapping | InvalidOriginalMapping { | |
| 201 | let { line, column, bias } = needle; | |
| 202 | line--; | |
| 203 | if (line < 0) throw new Error(LINE_GTR_ZERO); | |
| 204 | if (column < 0) throw new Error(COL_GTR_EQ_ZERO); | |
| 205 | ||
| 206 | const decoded = decodedMappings(map); | |
| 207 | ||
| 208 | // It's common for parent source maps to have pointers to lines that have no | |
| 209 | // mapping (like a "//# sourceMappingURL=") at the end of the child file. | |
| 210 | if (line >= decoded.length) return OMapping(null, null, null, null); | |
| 211 | ||
| 212 | const segments = decoded[line]; | |
| 213 | const index = traceSegmentInternal( | |
| 214 | segments, | |
| 215 | cast(map)._decodedMemo, | |
| 216 | line, | |
| 217 | column, | |
| 218 | bias || GREATEST_LOWER_BOUND, | |
| 219 | ); | |
| 220 | ||
| 221 | if (index === -1) return OMapping(null, null, null, null); | |
| 222 | ||
| 223 | const segment = segments[index]; | |
| 224 | if (segment.length === 1) return OMapping(null, null, null, null); | |
| 225 | ||
| 226 | const { names, resolvedSources } = map; | |
| 227 | return OMapping( | |
| 228 | resolvedSources[segment[SOURCES_INDEX]], | |
| 229 | segment[SOURCE_LINE] + 1, | |
| 230 | segment[SOURCE_COLUMN], | |
| 231 | segment.length === 5 ? names[segment[NAMES_INDEX]] : null, | |
| 232 | ); | |
| 233 | } | |
| 234 | ||
| 235 | /** | |
| 236 | * Finds the generated line/column position of the provided source/line/column source position. | |
| 237 | */ | |
| 238 | export function generatedPositionFor( | |
| 239 | map: TraceMap, | |
| 240 | needle: SourceNeedle, | |
| 241 | ): GeneratedMapping | InvalidGeneratedMapping { | |
| 242 | const { source, line, column, bias } = needle; | |
| 243 | return generatedPosition(map, source, line, column, bias || GREATEST_LOWER_BOUND, false); | |
| 244 | } | |
| 245 | ||
| 246 | /** | |
| 247 | * Finds all generated line/column positions of the provided source/line/column source position. | |
| 248 | */ | |
| 249 | export function allGeneratedPositionsFor(map: TraceMap, needle: SourceNeedle): GeneratedMapping[] { | |
| 250 | const { source, line, column, bias } = needle; | |
| 251 | // SourceMapConsumer uses LEAST_UPPER_BOUND for some reason, so we follow suit. | |
| 252 | return generatedPosition(map, source, line, column, bias || LEAST_UPPER_BOUND, true); | |
| 253 | } | |
| 254 | ||
| 255 | /** | |
| 256 | * Iterates each mapping in generated position order. | |
| 257 | */ | |
| 258 | export function eachMapping(map: TraceMap, cb: (mapping: EachMapping) => void): void { | |
| 259 | const decoded = decodedMappings(map); | |
| 260 | const { names, resolvedSources } = map; | |
| 261 | ||
| 262 | for (let i = 0; i < decoded.length; i++) { | |
| 263 | const line = decoded[i]; | |
| 264 | for (let j = 0; j < line.length; j++) { | |
| 265 | const seg = line[j]; | |
| 266 | ||
| 267 | const generatedLine = i + 1; | |
| 268 | const generatedColumn = seg[0]; | |
| 269 | let source = null; | |
| 270 | let originalLine = null; | |
| 271 | let originalColumn = null; | |
| 272 | let name = null; | |
| 273 | if (seg.length !== 1) { | |
| 274 | source = resolvedSources[seg[1]]; | |
| 275 | originalLine = seg[2] + 1; | |
| 276 | originalColumn = seg[3]; | |
| 277 | } | |
| 278 | if (seg.length === 5) name = names[seg[4]]; | |
| 279 | ||
| 280 | cb({ | |
| 281 | generatedLine, | |
| 282 | generatedColumn, | |
| 283 | source, | |
| 284 | originalLine, | |
| 285 | originalColumn, | |
| 286 | name, | |
| 287 | } as EachMapping); | |
| 288 | } | |
| 289 | } | |
| 290 | } | |
| 291 | ||
| 292 | function sourceIndex(map: TraceMap, source: string): number { | |
| 293 | const { sources, resolvedSources } = map; | |
| 294 | let index = sources.indexOf(source); | |
| 295 | if (index === -1) index = resolvedSources.indexOf(source); | |
| 296 | return index; | |
| 297 | } | |
| 298 | ||
| 299 | /** | |
| 300 | * Retrieves the source content for a particular source, if its found. Returns null if not. | |
| 301 | */ | |
| 302 | export function sourceContentFor(map: TraceMap, source: string): string | null { | |
| 303 | const { sourcesContent } = map; | |
| 304 | if (sourcesContent == null) return null; | |
| 305 | const index = sourceIndex(map, source); | |
| 306 | return index === -1 ? null : sourcesContent[index]; | |
| 307 | } | |
| 308 | ||
| 309 | /** | |
| 310 | * Determines if the source is marked to ignore by the source map. | |
| 311 | */ | |
| 312 | export function isIgnored(map: TraceMap, source: string): boolean { | |
| 313 | const { ignoreList } = map; | |
| 314 | if (ignoreList == null) return false; | |
| 315 | const index = sourceIndex(map, source); | |
| 316 | return index === -1 ? false : ignoreList.includes(index); | |
| 317 | } | |
| 318 | ||
| 319 | /** | |
| 320 | * A helper that skips sorting of the input map's mappings array, which can be expensive for larger | |
| 321 | * maps. | |
| 322 | */ | |
| 323 | export function presortedDecodedMap(map: DecodedSourceMap, mapUrl?: string): TraceMap { | |
| 324 | const tracer = new TraceMap(clone(map, []), mapUrl); | |
| 325 | cast(tracer)._decoded = map.mappings; | |
| 326 | return tracer; | |
| 327 | } | |
| 328 | ||
| 329 | /** | |
| 330 | * Returns a sourcemap object (with decoded mappings) suitable for passing to a library that expects | |
| 331 | * a sourcemap, or to JSON.stringify. | |
| 332 | */ | |
| 333 | export function decodedMap( | |
| 334 | map: TraceMap, | |
| 335 | ): Omit<DecodedSourceMap, 'mappings'> & { mappings: readonly SourceMapSegment[][] } { | |
| 336 | return clone(map, decodedMappings(map)); | |
| 337 | } | |
| 338 | ||
| 339 | /** | |
| 340 | * Returns a sourcemap object (with encoded mappings) suitable for passing to a library that expects | |
| 341 | * a sourcemap, or to JSON.stringify. | |
| 342 | */ | |
| 343 | export function encodedMap(map: TraceMap): EncodedSourceMap { | |
| 344 | return clone(map, encodedMappings(map)); | |
| 345 | } | |
| 346 | ||
| 347 | function clone<T extends string | readonly SourceMapSegment[][]>( | |
| 348 | map: TraceMap | DecodedSourceMap, | |
| 349 | mappings: T, | |
| 350 | ): T extends string ? EncodedSourceMap : DecodedSourceMap { | |
| 351 | return { | |
| 352 | version: map.version, | |
| 353 | file: map.file, | |
| 354 | names: map.names, | |
| 355 | sourceRoot: map.sourceRoot, | |
| 356 | sources: map.sources, | |
| 357 | sourcesContent: map.sourcesContent, | |
| 358 | mappings, | |
| 359 | ignoreList: map.ignoreList || (map as XInput).x_google_ignoreList, | |
| 360 | } as any; | |
| 361 | } | |
| 362 | ||
| 363 | function OMapping(source: null, line: null, column: null, name: null): InvalidOriginalMapping; | |
| 364 | function OMapping( | |
| 365 | source: string, | |
| 366 | line: number, | |
| 367 | column: number, | |
| 368 | name: string | null, | |
| 369 | ): OriginalMapping; | |
| 370 | function OMapping( | |
| 371 | source: string | null, | |
| 372 | line: number | null, | |
| 373 | column: number | null, | |
| 374 | name: string | null, | |
| 375 | ): OriginalMapping | InvalidOriginalMapping { | |
| 376 | return { source, line, column, name } as any; | |
| 377 | } | |
| 378 | ||
| 379 | function GMapping(line: null, column: null): InvalidGeneratedMapping; | |
| 380 | function GMapping(line: number, column: number): GeneratedMapping; | |
| 381 | function GMapping( | |
| 382 | line: number | null, | |
| 383 | column: number | null, | |
| 384 | ): GeneratedMapping | InvalidGeneratedMapping { | |
| 385 | return { line, column } as any; | |
| 386 | } | |
| 387 | ||
| 388 | function traceSegmentInternal( | |
| 389 | segments: SourceMapSegment[], | |
| 390 | memo: MemoState, | |
| 391 | line: number, | |
| 392 | column: number, | |
| 393 | bias: Bias, | |
| 394 | ): number; | |
| 395 | function traceSegmentInternal( | |
| 396 | segments: ReverseSegment[], | |
| 397 | memo: MemoState, | |
| 398 | line: number, | |
| 399 | column: number, | |
| 400 | bias: Bias, | |
| 401 | ): number; | |
| 402 | function traceSegmentInternal( | |
| 403 | segments: SourceMapSegment[] | ReverseSegment[], | |
| 404 | memo: MemoState, | |
| 405 | line: number, | |
| 406 | column: number, | |
| 407 | bias: Bias, | |
| 408 | ): number { | |
| 409 | let index = memoizedBinarySearch(segments, column, memo, line); | |
| 410 | if (bsFound) { | |
| 411 | index = (bias === LEAST_UPPER_BOUND ? upperBound : lowerBound)(segments, column, index); | |
| 412 | } else if (bias === LEAST_UPPER_BOUND) index++; | |
| 413 | ||
| 414 | if (index === -1 || index === segments.length) return -1; | |
| 415 | return index; | |
| 416 | } | |
| 417 | ||
| 418 | function sliceGeneratedPositions( | |
| 419 | segments: ReverseSegment[], | |
| 420 | memo: MemoState, | |
| 421 | line: number, | |
| 422 | column: number, | |
| 423 | bias: Bias, | |
| 424 | ): GeneratedMapping[] { | |
| 425 | let min = traceSegmentInternal(segments, memo, line, column, GREATEST_LOWER_BOUND); | |
| 426 | ||
| 427 | // We ignored the bias when tracing the segment so that we're guarnateed to find the first (in | |
| 428 | // insertion order) segment that matched. Even if we did respect the bias when tracing, we would | |
| 429 | // still need to call `lowerBound()` to find the first segment, which is slower than just looking | |
| 430 | // for the GREATEST_LOWER_BOUND to begin with. The only difference that matters for us is when the | |
| 431 | // binary search didn't match, in which case GREATEST_LOWER_BOUND just needs to increment to | |
| 432 | // match LEAST_UPPER_BOUND. | |
| 433 | if (!bsFound && bias === LEAST_UPPER_BOUND) min++; | |
| 434 | ||
| 435 | if (min === -1 || min === segments.length) return []; | |
| 436 | ||
| 437 | // We may have found the segment that started at an earlier column. If this is the case, then we | |
| 438 | // need to slice all generated segments that match _that_ column, because all such segments span | |
| 439 | // to our desired column. | |
| 440 | const matchedColumn = bsFound ? column : segments[min][COLUMN]; | |
| 441 | ||
| 442 | // The binary search is not guaranteed to find the lower bound when a match wasn't found. | |
| 443 | if (!bsFound) min = lowerBound(segments, matchedColumn, min); | |
| 444 | const max = upperBound(segments, matchedColumn, min); | |
| 445 | ||
| 446 | const result = []; | |
| 447 | for (; min <= max; min++) { | |
| 448 | const segment = segments[min]; | |
| 449 | result.push(GMapping(segment[REV_GENERATED_LINE] + 1, segment[REV_GENERATED_COLUMN])); | |
| 450 | } | |
| 451 | return result; | |
| 452 | } | |
| 453 | ||
| 454 | function generatedPosition( | |
| 455 | map: TraceMap, | |
| 456 | source: string, | |
| 457 | line: number, | |
| 458 | column: number, | |
| 459 | bias: Bias, | |
| 460 | all: false, | |
| 461 | ): GeneratedMapping | InvalidGeneratedMapping; | |
| 462 | function generatedPosition( | |
| 463 | map: TraceMap, | |
| 464 | source: string, | |
| 465 | line: number, | |
| 466 | column: number, | |
| 467 | bias: Bias, | |
| 468 | all: true, | |
| 469 | ): GeneratedMapping[]; | |
| 470 | function generatedPosition( | |
| 471 | map: TraceMap, | |
| 472 | source: string, | |
| 473 | line: number, | |
| 474 | column: number, | |
| 475 | bias: Bias, | |
| 476 | all: boolean, | |
| 477 | ): GeneratedMapping | InvalidGeneratedMapping | GeneratedMapping[] { | |
| 478 | line--; | |
| 479 | if (line < 0) throw new Error(LINE_GTR_ZERO); | |
| 480 | if (column < 0) throw new Error(COL_GTR_EQ_ZERO); | |
| 481 | ||
| 482 | const { sources, resolvedSources } = map; | |
| 483 | let sourceIndex = sources.indexOf(source); | |
| 484 | if (sourceIndex === -1) sourceIndex = resolvedSources.indexOf(source); | |
| 485 | if (sourceIndex === -1) return all ? [] : GMapping(null, null); | |
| 486 | ||
| 487 | const bySourceMemos = (cast(map)._bySourceMemos ||= sources.map(memoizedState)); | |
| 488 | const generated = (cast(map)._bySources ||= buildBySources(decodedMappings(map), bySourceMemos)); | |
| 489 | ||
| 490 | const segments = generated[sourceIndex][line]; | |
| 491 | if (segments == null) return all ? [] : GMapping(null, null); | |
| 492 | ||
| 493 | const memo = bySourceMemos[sourceIndex]; | |
| 494 | ||
| 495 | if (all) return sliceGeneratedPositions(segments, memo, line, column, bias); | |
| 496 | ||
| 497 | const index = traceSegmentInternal(segments, memo, line, column, bias); | |
| 498 | if (index === -1) return GMapping(null, null); | |
| 499 | ||
| 500 | const segment = segments[index]; | |
| 501 | return GMapping(segment[REV_GENERATED_LINE] + 1, segment[REV_GENERATED_COLUMN]); | |
| 502 | } |
| … | … | @@ -0,0 +1,114 @@ |
| 1 | import type { SourceMapSegment } from './sourcemap-segment'; | |
| 2 | import type { GREATEST_LOWER_BOUND, LEAST_UPPER_BOUND, TraceMap } from './trace-mapping'; | |
| 3 | ||
| 4 | export interface SourceMapV3 { | |
| 5 | file?: string | null; | |
| 6 | names: string[]; | |
| 7 | sourceRoot?: string; | |
| 8 | sources: (string | null)[]; | |
| 9 | sourcesContent?: (string | null)[]; | |
| 10 | version: 3; | |
| 11 | ignoreList?: number[]; | |
| 12 | } | |
| 13 | ||
| 14 | export interface EncodedSourceMap extends SourceMapV3 { | |
| 15 | mappings: string; | |
| 16 | } | |
| 17 | ||
| 18 | export interface DecodedSourceMap extends SourceMapV3 { | |
| 19 | mappings: SourceMapSegment[][]; | |
| 20 | } | |
| 21 | ||
| 22 | export interface Section { | |
| 23 | offset: { line: number; column: number }; | |
| 24 | map: EncodedSourceMap | DecodedSourceMap | SectionedSourceMap; | |
| 25 | } | |
| 26 | ||
| 27 | export interface SectionedSourceMap { | |
| 28 | file?: string | null; | |
| 29 | sections: Section[]; | |
| 30 | version: 3; | |
| 31 | } | |
| 32 | ||
| 33 | export type OriginalMapping = { | |
| 34 | source: string | null; | |
| 35 | line: number; | |
| 36 | column: number; | |
| 37 | name: string | null; | |
| 38 | }; | |
| 39 | ||
| 40 | export type InvalidOriginalMapping = { | |
| 41 | source: null; | |
| 42 | line: null; | |
| 43 | column: null; | |
| 44 | name: null; | |
| 45 | }; | |
| 46 | ||
| 47 | export type GeneratedMapping = { | |
| 48 | line: number; | |
| 49 | column: number; | |
| 50 | }; | |
| 51 | export type InvalidGeneratedMapping = { | |
| 52 | line: null; | |
| 53 | column: null; | |
| 54 | }; | |
| 55 | ||
| 56 | export type Bias = typeof GREATEST_LOWER_BOUND | typeof LEAST_UPPER_BOUND; | |
| 57 | ||
| 58 | export type XInput = { x_google_ignoreList?: SourceMapV3['ignoreList'] }; | |
| 59 | export type EncodedSourceMapXInput = EncodedSourceMap & XInput; | |
| 60 | export type DecodedSourceMapXInput = DecodedSourceMap & XInput; | |
| 61 | export type SectionedSourceMapXInput = Omit<SectionedSourceMap, 'sections'> & { | |
| 62 | sections: SectionXInput[]; | |
| 63 | }; | |
| 64 | export type SectionXInput = Omit<Section, 'map'> & { | |
| 65 | map: SectionedSourceMapInput; | |
| 66 | }; | |
| 67 | ||
| 68 | export type SourceMapInput = string | EncodedSourceMapXInput | DecodedSourceMapXInput | TraceMap; | |
| 69 | export type SectionedSourceMapInput = SourceMapInput | SectionedSourceMapXInput; | |
| 70 | ||
| 71 | export type Needle = { line: number; column: number; bias?: Bias }; | |
| 72 | export type SourceNeedle = { source: string; line: number; column: number; bias?: Bias }; | |
| 73 | ||
| 74 | export type EachMapping = | |
| 75 | | { | |
| 76 | generatedLine: number; | |
| 77 | generatedColumn: number; | |
| 78 | source: null; | |
| 79 | originalLine: null; | |
| 80 | originalColumn: null; | |
| 81 | name: null; | |
| 82 | } | |
| 83 | | { | |
| 84 | generatedLine: number; | |
| 85 | generatedColumn: number; | |
| 86 | source: string | null; | |
| 87 | originalLine: number; | |
| 88 | originalColumn: number; | |
| 89 | name: string | null; | |
| 90 | }; | |
| 91 | ||
| 92 | export abstract class SourceMap { | |
| 93 | declare version: SourceMapV3['version']; | |
| 94 | declare file: SourceMapV3['file']; | |
| 95 | declare names: SourceMapV3['names']; | |
| 96 | declare sourceRoot: SourceMapV3['sourceRoot']; | |
| 97 | declare sources: SourceMapV3['sources']; | |
| 98 | declare sourcesContent: SourceMapV3['sourcesContent']; | |
| 99 | declare resolvedSources: SourceMapV3['sources']; | |
| 100 | declare ignoreList: SourceMapV3['ignoreList']; | |
| 101 | } | |
| 102 | ||
| 103 | export type Ro<T> = | |
| 104 | T extends Array<infer V> | |
| 105 | ? V[] | Readonly<V[]> | RoArray<V> | Readonly<RoArray<V>> | |
| 106 | : T extends object | |
| 107 | ? T | Readonly<T> | RoObject<T> | Readonly<RoObject<T>> | |
| 108 | : T; | |
| 109 | type RoArray<T> = Ro<T>[]; | |
| 110 | type RoObject<T> = { [K in keyof T]: T[K] | Ro<T[K]> }; | |
| 111 | ||
| 112 | export function parse<T>(map: T): Exclude<T, string> { | |
| 113 | return typeof map === 'string' ? JSON.parse(map) : (map as Exclude<T, string>); | |
| 114 | } |
| … | … | @@ -0,0 +1,33 @@ |
| 1 | import type { SourceMapSegment, ReverseSegment } from './sourcemap-segment.cts'; | |
| 2 | export type MemoState = { | |
| 3 | lastKey: number; | |
| 4 | lastNeedle: number; | |
| 5 | lastIndex: number; | |
| 6 | }; | |
| 7 | export declare let found: boolean; | |
| 8 | /** | |
| 9 | * A binary search implementation that returns the index if a match is found. | |
| 10 | * If no match is found, then the left-index (the index associated with the item that comes just | |
| 11 | * before the desired index) is returned. To maintain proper sort order, a splice would happen at | |
| 12 | * the next index: | |
| 13 | * | |
| 14 | * ```js | |
| 15 | * const array = [1, 3]; | |
| 16 | * const needle = 2; | |
| 17 | * const index = binarySearch(array, needle, (item, needle) => item - needle); | |
| 18 | * | |
| 19 | * assert.equal(index, 0); | |
| 20 | * array.splice(index + 1, 0, needle); | |
| 21 | * assert.deepEqual(array, [1, 2, 3]); | |
| 22 | * ``` | |
| 23 | */ | |
| 24 | export declare function binarySearch(haystack: SourceMapSegment[] | ReverseSegment[], needle: number, low: number, high: number): number; | |
| 25 | export declare function upperBound(haystack: SourceMapSegment[] | ReverseSegment[], needle: number, index: number): number; | |
| 26 | export declare function lowerBound(haystack: SourceMapSegment[] | ReverseSegment[], needle: number, index: number): number; | |
| 27 | export declare function memoizedState(): MemoState; | |
| 28 | /** | |
| 29 | * This overly complicated beast is just to record the last tested line/column and the resulting | |
| 30 | * index, allowing us to skip a few tests if mappings are monotonically increasing. | |
| 31 | */ | |
| 32 | export declare function memoizedBinarySearch(haystack: SourceMapSegment[] | ReverseSegment[], needle: number, state: MemoState, key: number): number; | |
| 33 | //# sourceMappingURL=binary-search.d.ts.map |
| … | … | @@ -0,0 +1 @@ |
| 1 | {"version":3,"file":"binary-search.d.ts","sourceRoot":"","sources":["../src/binary-search.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAG5E,MAAM,MAAM,SAAS,GAAG;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,eAAO,IAAI,KAAK,SAAQ,CAAC;AAEzB;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,gBAAgB,EAAE,GAAG,cAAc,EAAE,EAC/C,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,GACX,MAAM,CAmBR;AAED,wBAAgB,UAAU,CACxB,QAAQ,EAAE,gBAAgB,EAAE,GAAG,cAAc,EAAE,EAC/C,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,GACZ,MAAM,CAKR;AAED,wBAAgB,UAAU,CACxB,QAAQ,EAAE,gBAAgB,EAAE,GAAG,cAAc,EAAE,EAC/C,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,GACZ,MAAM,CAKR;AAED,wBAAgB,aAAa,IAAI,SAAS,CAMzC;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,gBAAgB,EAAE,GAAG,cAAc,EAAE,EAC/C,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,SAAS,EAChB,GAAG,EAAE,MAAM,GACV,MAAM,CAsBR"} |
| … | … | @@ -0,0 +1,33 @@ |
| 1 | import type { SourceMapSegment, ReverseSegment } from './sourcemap-segment.mts'; | |
| 2 | export type MemoState = { | |
| 3 | lastKey: number; | |
| 4 | lastNeedle: number; | |
| 5 | lastIndex: number; | |
| 6 | }; | |
| 7 | export declare let found: boolean; | |
| 8 | /** | |
| 9 | * A binary search implementation that returns the index if a match is found. | |
| 10 | * If no match is found, then the left-index (the index associated with the item that comes just | |
| 11 | * before the desired index) is returned. To maintain proper sort order, a splice would happen at | |
| 12 | * the next index: | |
| 13 | * | |
| 14 | * ```js | |
| 15 | * const array = [1, 3]; | |
| 16 | * const needle = 2; | |
| 17 | * const index = binarySearch(array, needle, (item, needle) => item - needle); | |
| 18 | * | |
| 19 | * assert.equal(index, 0); | |
| 20 | * array.splice(index + 1, 0, needle); | |
| 21 | * assert.deepEqual(array, [1, 2, 3]); | |
| 22 | * ``` | |
| 23 | */ | |
| 24 | export declare function binarySearch(haystack: SourceMapSegment[] | ReverseSegment[], needle: number, low: number, high: number): number; | |
| 25 | export declare function upperBound(haystack: SourceMapSegment[] | ReverseSegment[], needle: number, index: number): number; | |
| 26 | export declare function lowerBound(haystack: SourceMapSegment[] | ReverseSegment[], needle: number, index: number): number; | |
| 27 | export declare function memoizedState(): MemoState; | |
| 28 | /** | |
| 29 | * This overly complicated beast is just to record the last tested line/column and the resulting | |
| 30 | * index, allowing us to skip a few tests if mappings are monotonically increasing. | |
| 31 | */ | |
| 32 | export declare function memoizedBinarySearch(haystack: SourceMapSegment[] | ReverseSegment[], needle: number, state: MemoState, key: number): number; | |
| 33 | //# sourceMappingURL=binary-search.d.ts.map |
| … | … | @@ -0,0 +1 @@ |
| 1 | {"version":3,"file":"binary-search.d.ts","sourceRoot":"","sources":["../src/binary-search.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAG5E,MAAM,MAAM,SAAS,GAAG;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,eAAO,IAAI,KAAK,SAAQ,CAAC;AAEzB;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,gBAAgB,EAAE,GAAG,cAAc,EAAE,EAC/C,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,GACX,MAAM,CAmBR;AAED,wBAAgB,UAAU,CACxB,QAAQ,EAAE,gBAAgB,EAAE,GAAG,cAAc,EAAE,EAC/C,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,GACZ,MAAM,CAKR;AAED,wBAAgB,UAAU,CACxB,QAAQ,EAAE,gBAAgB,EAAE,GAAG,cAAc,EAAE,EAC/C,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,GACZ,MAAM,CAKR;AAED,wBAAgB,aAAa,IAAI,SAAS,CAMzC;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,gBAAgB,EAAE,GAAG,cAAc,EAAE,EAC/C,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,SAAS,EAChB,GAAG,EAAE,MAAM,GACV,MAAM,CAsBR"} |
| … | … | @@ -0,0 +1,4 @@ |
| 1 | import type { ReverseSegment, SourceMapSegment } from './sourcemap-segment.cts'; | |
| 2 | export type Source = ReverseSegment[][]; | |
| 3 | export = function buildBySources(decoded: readonly SourceMapSegment[][], memos: unknown[]): Source[]; | |
| 4 | //# sourceMappingURL=by-source.d.ts.map |
| … | … | @@ -0,0 +1 @@ |
| 1 | {"version":3,"file":"by-source.d.ts","sourceRoot":"","sources":["../src/by-source.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5E,MAAM,MAAM,MAAM,GAAG,cAAc,EAAE,EAAE,CAAC;AAIxC,MAAM,CAAC,OAAO,UAAU,cAAc,CACpC,OAAO,EAAE,SAAS,gBAAgB,EAAE,EAAE,EACtC,KAAK,EAAE,OAAO,EAAE,GACf,MAAM,EAAE,CA4BV"} |
| … | … | @@ -0,0 +1,4 @@ |
| 1 | import type { ReverseSegment, SourceMapSegment } from './sourcemap-segment.mts'; | |
| 2 | export type Source = ReverseSegment[][]; | |
| 3 | export default function buildBySources(decoded: readonly SourceMapSegment[][], memos: unknown[]): Source[]; | |
| 4 | //# sourceMappingURL=by-source.d.ts.map |
| … | … | @@ -0,0 +1 @@ |
| 1 | {"version":3,"file":"by-source.d.ts","sourceRoot":"","sources":["../src/by-source.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5E,MAAM,MAAM,MAAM,GAAG,cAAc,EAAE,EAAE,CAAC;AAIxC,MAAM,CAAC,OAAO,UAAU,cAAc,CACpC,OAAO,EAAE,SAAS,gBAAgB,EAAE,EAAE,EACtC,KAAK,EAAE,OAAO,EAAE,GACf,MAAM,EAAE,CA4BV"} |
| … | … | @@ -0,0 +1,9 @@ |
| 1 | import { TraceMap } from './trace-mapping.cts'; | |
| 2 | import type { SectionedSourceMapInput, Ro } from './types.cts'; | |
| 3 | type FlattenMap = { | |
| 4 | new (map: Ro<SectionedSourceMapInput>, mapUrl?: string | null): TraceMap; | |
| 5 | (map: Ro<SectionedSourceMapInput>, mapUrl?: string | null): TraceMap; | |
| 6 | }; | |
| 7 | export declare const FlattenMap: FlattenMap; | |
| 8 | export {}; | |
| 9 | //# sourceMappingURL=flatten-map.d.ts.map |
| … | … | @@ -0,0 +1 @@ |
| 1 | {"version":3,"file":"flatten-map.d.ts","sourceRoot":"","sources":["../src/flatten-map.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAwC,MAAM,iBAAiB,CAAC;AAUjF,OAAO,KAAK,EAKV,uBAAuB,EAEvB,EAAE,EACH,MAAM,SAAS,CAAC;AAGjB,KAAK,UAAU,GAAG;IAChB,KAAK,GAAG,EAAE,EAAE,CAAC,uBAAuB,CAAC,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,QAAQ,CAAC;IACzE,CAAC,GAAG,EAAE,EAAE,CAAC,uBAAuB,CAAC,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,QAAQ,CAAC;CACtE,CAAC;AAEF,eAAO,MAAM,UAAU,EAAE,UAsCV,CAAC"} |
| … | … | @@ -0,0 +1,9 @@ |
| 1 | import { TraceMap } from './trace-mapping.mts'; | |
| 2 | import type { SectionedSourceMapInput, Ro } from './types.mts'; | |
| 3 | type FlattenMap = { | |
| 4 | new (map: Ro<SectionedSourceMapInput>, mapUrl?: string | null): TraceMap; | |
| 5 | (map: Ro<SectionedSourceMapInput>, mapUrl?: string | null): TraceMap; | |
| 6 | }; | |
| 7 | export declare const FlattenMap: FlattenMap; | |
| 8 | export {}; | |
| 9 | //# sourceMappingURL=flatten-map.d.ts.map |
| … | … | @@ -0,0 +1 @@ |
| 1 | {"version":3,"file":"flatten-map.d.ts","sourceRoot":"","sources":["../src/flatten-map.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAwC,MAAM,iBAAiB,CAAC;AAUjF,OAAO,KAAK,EAKV,uBAAuB,EAEvB,EAAE,EACH,MAAM,SAAS,CAAC;AAGjB,KAAK,UAAU,GAAG;IAChB,KAAK,GAAG,EAAE,EAAE,CAAC,uBAAuB,CAAC,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,QAAQ,CAAC;IACzE,CAAC,GAAG,EAAE,EAAE,CAAC,uBAAuB,CAAC,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,QAAQ,CAAC;CACtE,CAAC;AAEF,eAAO,MAAM,UAAU,EAAE,UAsCV,CAAC"} |
| … | … | @@ -0,0 +1,4 @@ |
| 1 | type Resolve = (source: string | null) => string; | |
| 2 | export = function resolver(mapUrl: string | null | undefined, sourceRoot: string | undefined): Resolve; | |
| 3 | export {}; | |
| 4 | //# sourceMappingURL=resolve.d.ts.map |
| … | … | @@ -0,0 +1 @@ |
| 1 | {"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../src/resolve.ts"],"names":[],"mappings":"AAGA,KAAK,OAAO,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,KAAK,MAAM,CAAC;AACjD,MAAM,CAAC,OAAO,UAAU,QAAQ,CAC9B,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACjC,UAAU,EAAE,MAAM,GAAG,SAAS,GAC7B,OAAO,CAQT"} |
| … | … | @@ -0,0 +1,4 @@ |
| 1 | type Resolve = (source: string | null) => string; | |
| 2 | export default function resolver(mapUrl: string | null | undefined, sourceRoot: string | undefined): Resolve; | |
| 3 | export {}; | |
| 4 | //# sourceMappingURL=resolve.d.ts.map |
| … | … | @@ -0,0 +1 @@ |
| 1 | {"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../src/resolve.ts"],"names":[],"mappings":"AAGA,KAAK,OAAO,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,KAAK,MAAM,CAAC;AACjD,MAAM,CAAC,OAAO,UAAU,QAAQ,CAC9B,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACjC,UAAU,EAAE,MAAM,GAAG,SAAS,GAC7B,OAAO,CAQT"} |
| … | … | @@ -0,0 +1,4 @@ |
| 1 | import type { ReverseSegment, SourceMapSegment } from './sourcemap-segment.cts'; | |
| 2 | export = function maybeSort(mappings: SourceMapSegment[][], owned: boolean): SourceMapSegment[][]; | |
| 3 | export declare function sortComparator<T extends SourceMapSegment | ReverseSegment>(a: T, b: T): number; | |
| 4 | //# sourceMappingURL=sort.d.ts.map |
| … | … | @@ -0,0 +1 @@ |
| 1 | {"version":3,"file":"sort.d.ts","sourceRoot":"","sources":["../src/sort.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5E,MAAM,CAAC,OAAO,UAAU,SAAS,CAC/B,QAAQ,EAAE,gBAAgB,EAAE,EAAE,EAC9B,KAAK,EAAE,OAAO,GACb,gBAAgB,EAAE,EAAE,CAYtB;AAuBD,wBAAgB,cAAc,CAAC,CAAC,SAAS,gBAAgB,GAAG,cAAc,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,MAAM,CAE9F"} |
| … | … | @@ -0,0 +1,4 @@ |
| 1 | import type { ReverseSegment, SourceMapSegment } from './sourcemap-segment.mts'; | |
| 2 | export default function maybeSort(mappings: SourceMapSegment[][], owned: boolean): SourceMapSegment[][]; | |
| 3 | export declare function sortComparator<T extends SourceMapSegment | ReverseSegment>(a: T, b: T): number; | |
| 4 | //# sourceMappingURL=sort.d.ts.map |
| … | … | @@ -0,0 +1 @@ |
| 1 | {"version":3,"file":"sort.d.ts","sourceRoot":"","sources":["../src/sort.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5E,MAAM,CAAC,OAAO,UAAU,SAAS,CAC/B,QAAQ,EAAE,gBAAgB,EAAE,EAAE,EAC9B,KAAK,EAAE,OAAO,GACb,gBAAgB,EAAE,EAAE,CAYtB;AAuBD,wBAAgB,cAAc,CAAC,CAAC,SAAS,gBAAgB,GAAG,cAAc,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,MAAM,CAE9F"} |
| … | … | @@ -0,0 +1,17 @@ |
| 1 | type GeneratedColumn = number; | |
| 2 | type SourcesIndex = number; | |
| 3 | type SourceLine = number; | |
| 4 | type SourceColumn = number; | |
| 5 | type NamesIndex = number; | |
| 6 | type GeneratedLine = number; | |
| 7 | export type SourceMapSegment = [GeneratedColumn] | [GeneratedColumn, SourcesIndex, SourceLine, SourceColumn] | [GeneratedColumn, SourcesIndex, SourceLine, SourceColumn, NamesIndex]; | |
| 8 | export type ReverseSegment = [SourceColumn, GeneratedLine, GeneratedColumn]; | |
| 9 | export declare const COLUMN = 0; | |
| 10 | export declare const SOURCES_INDEX = 1; | |
| 11 | export declare const SOURCE_LINE = 2; | |
| 12 | export declare const SOURCE_COLUMN = 3; | |
| 13 | export declare const NAMES_INDEX = 4; | |
| 14 | export declare const REV_GENERATED_LINE = 1; | |
| 15 | export declare const REV_GENERATED_COLUMN = 2; | |
| 16 | export {}; | |
| 17 | //# sourceMappingURL=sourcemap-segment.d.ts.map |
| … | … | @@ -0,0 +1 @@ |
| 1 | {"version":3,"file":"sourcemap-segment.d.ts","sourceRoot":"","sources":["../src/sourcemap-segment.ts"],"names":[],"mappings":"AAAA,KAAK,eAAe,GAAG,MAAM,CAAC;AAC9B,KAAK,YAAY,GAAG,MAAM,CAAC;AAC3B,KAAK,UAAU,GAAG,MAAM,CAAC;AACzB,KAAK,YAAY,GAAG,MAAM,CAAC;AAC3B,KAAK,UAAU,GAAG,MAAM,CAAC;AAEzB,KAAK,aAAa,GAAG,MAAM,CAAC;AAE5B,MAAM,MAAM,gBAAgB,GACxB,CAAC,eAAe,CAAC,GACjB,CAAC,eAAe,EAAE,YAAY,EAAE,UAAU,EAAE,YAAY,CAAC,GACzD,CAAC,eAAe,EAAE,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,UAAU,CAAC,CAAC;AAE1E,MAAM,MAAM,cAAc,GAAG,CAAC,YAAY,EAAE,aAAa,EAAE,eAAe,CAAC,CAAC;AAE5E,eAAO,MAAM,MAAM,IAAI,CAAC;AACxB,eAAO,MAAM,aAAa,IAAI,CAAC;AAC/B,eAAO,MAAM,WAAW,IAAI,CAAC;AAC7B,eAAO,MAAM,aAAa,IAAI,CAAC;AAC/B,eAAO,MAAM,WAAW,IAAI,CAAC;AAE7B,eAAO,MAAM,kBAAkB,IAAI,CAAC;AACpC,eAAO,MAAM,oBAAoB,IAAI,CAAC"} |
| … | … | @@ -0,0 +1,17 @@ |
| 1 | type GeneratedColumn = number; | |
| 2 | type SourcesIndex = number; | |
| 3 | type SourceLine = number; | |
| 4 | type SourceColumn = number; | |
| 5 | type NamesIndex = number; | |
| 6 | type GeneratedLine = number; | |
| 7 | export type SourceMapSegment = [GeneratedColumn] | [GeneratedColumn, SourcesIndex, SourceLine, SourceColumn] | [GeneratedColumn, SourcesIndex, SourceLine, SourceColumn, NamesIndex]; | |
| 8 | export type ReverseSegment = [SourceColumn, GeneratedLine, GeneratedColumn]; | |
| 9 | export declare const COLUMN = 0; | |
| 10 | export declare const SOURCES_INDEX = 1; | |
| 11 | export declare const SOURCE_LINE = 2; | |
| 12 | export declare const SOURCE_COLUMN = 3; | |
| 13 | export declare const NAMES_INDEX = 4; | |
| 14 | export declare const REV_GENERATED_LINE = 1; | |
| 15 | export declare const REV_GENERATED_COLUMN = 2; | |
| 16 | export {}; | |
| 17 | //# sourceMappingURL=sourcemap-segment.d.ts.map |
| … | … | @@ -0,0 +1 @@ |
| 1 | {"version":3,"file":"sourcemap-segment.d.ts","sourceRoot":"","sources":["../src/sourcemap-segment.ts"],"names":[],"mappings":"AAAA,KAAK,eAAe,GAAG,MAAM,CAAC;AAC9B,KAAK,YAAY,GAAG,MAAM,CAAC;AAC3B,KAAK,UAAU,GAAG,MAAM,CAAC;AACzB,KAAK,YAAY,GAAG,MAAM,CAAC;AAC3B,KAAK,UAAU,GAAG,MAAM,CAAC;AAEzB,KAAK,aAAa,GAAG,MAAM,CAAC;AAE5B,MAAM,MAAM,gBAAgB,GACxB,CAAC,eAAe,CAAC,GACjB,CAAC,eAAe,EAAE,YAAY,EAAE,UAAU,EAAE,YAAY,CAAC,GACzD,CAAC,eAAe,EAAE,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,UAAU,CAAC,CAAC;AAE1E,MAAM,MAAM,cAAc,GAAG,CAAC,YAAY,EAAE,aAAa,EAAE,eAAe,CAAC,CAAC;AAE5E,eAAO,MAAM,MAAM,IAAI,CAAC;AACxB,eAAO,MAAM,aAAa,IAAI,CAAC;AAC/B,eAAO,MAAM,WAAW,IAAI,CAAC;AAC7B,eAAO,MAAM,aAAa,IAAI,CAAC;AAC/B,eAAO,MAAM,WAAW,IAAI,CAAC;AAE7B,eAAO,MAAM,kBAAkB,IAAI,CAAC;AACpC,eAAO,MAAM,oBAAoB,IAAI,CAAC"} |
| … | … | @@ -0,0 +1,5 @@ |
| 1 | /** | |
| 2 | * Removes everything after the last "/", but leaves the slash. | |
| 3 | */ | |
| 4 | export = function stripFilename(path: string | undefined | null): string; | |
| 5 | //# sourceMappingURL=strip-filename.d.ts.map |
| … | … | @@ -0,0 +1 @@ |
| 1 | {"version":3,"file":"strip-filename.d.ts","sourceRoot":"","sources":["../src/strip-filename.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,CAAC,OAAO,UAAU,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,MAAM,CAI7E"} |
| … | … | @@ -0,0 +1,5 @@ |
| 1 | /** | |
| 2 | * Removes everything after the last "/", but leaves the slash. | |
| 3 | */ | |
| 4 | export default function stripFilename(path: string | undefined | null): string; | |
| 5 | //# sourceMappingURL=strip-filename.d.ts.map |
| … | … | @@ -0,0 +1 @@ |
| 1 | {"version":3,"file":"strip-filename.d.ts","sourceRoot":"","sources":["../src/strip-filename.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,CAAC,OAAO,UAAU,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,MAAM,CAI7E"} |
| … | … | @@ -0,0 +1,80 @@ |
| 1 | import type { SourceMapSegment } from './sourcemap-segment.cts'; | |
| 2 | import type { SourceMapV3, DecodedSourceMap, EncodedSourceMap, InvalidOriginalMapping, OriginalMapping, InvalidGeneratedMapping, GeneratedMapping, SourceMapInput, Needle, SourceNeedle, SourceMap, EachMapping, Ro } from './types.cts'; | |
| 3 | export type { SourceMapSegment } from './sourcemap-segment.cts'; | |
| 4 | export type { SourceMap, DecodedSourceMap, EncodedSourceMap, Section, SectionedSourceMap, SourceMapV3, Bias, EachMapping, GeneratedMapping, InvalidGeneratedMapping, InvalidOriginalMapping, Needle, OriginalMapping, OriginalMapping as Mapping, SectionedSourceMapInput, SourceMapInput, SourceNeedle, XInput, EncodedSourceMapXInput, DecodedSourceMapXInput, SectionedSourceMapXInput, SectionXInput, } from './types.cts'; | |
| 5 | export declare const LEAST_UPPER_BOUND = -1; | |
| 6 | export declare const GREATEST_LOWER_BOUND = 1; | |
| 7 | export { FlattenMap, FlattenMap as AnyMap } from './flatten-map.cts'; | |
| 8 | export declare class TraceMap implements SourceMap { | |
| 9 | version: SourceMapV3['version']; | |
| 10 | file: SourceMapV3['file']; | |
| 11 | names: SourceMapV3['names']; | |
| 12 | sourceRoot: SourceMapV3['sourceRoot']; | |
| 13 | sources: SourceMapV3['sources']; | |
| 14 | sourcesContent: SourceMapV3['sourcesContent']; | |
| 15 | ignoreList: SourceMapV3['ignoreList']; | |
| 16 | resolvedSources: string[]; | |
| 17 | private _encoded; | |
| 18 | private _decoded; | |
| 19 | private _decodedMemo; | |
| 20 | private _bySources; | |
| 21 | private _bySourceMemos; | |
| 22 | constructor(map: Ro<SourceMapInput>, mapUrl?: string | null); | |
| 23 | } | |
| 24 | /** | |
| 25 | * Returns the encoded (VLQ string) form of the SourceMap's mappings field. | |
| 26 | */ | |
| 27 | export declare function encodedMappings(map: TraceMap): EncodedSourceMap['mappings']; | |
| 28 | /** | |
| 29 | * Returns the decoded (array of lines of segments) form of the SourceMap's mappings field. | |
| 30 | */ | |
| 31 | export declare function decodedMappings(map: TraceMap): Readonly<DecodedSourceMap['mappings']>; | |
| 32 | /** | |
| 33 | * A low-level API to find the segment associated with a generated line/column (think, from a | |
| 34 | * stack trace). Line and column here are 0-based, unlike `originalPositionFor`. | |
| 35 | */ | |
| 36 | export declare function traceSegment(map: TraceMap, line: number, column: number): Readonly<SourceMapSegment> | null; | |
| 37 | /** | |
| 38 | * A higher-level API to find the source/line/column associated with a generated line/column | |
| 39 | * (think, from a stack trace). Line is 1-based, but column is 0-based, due to legacy behavior in | |
| 40 | * `source-map` library. | |
| 41 | */ | |
| 42 | export declare function originalPositionFor(map: TraceMap, needle: Needle): OriginalMapping | InvalidOriginalMapping; | |
| 43 | /** | |
| 44 | * Finds the generated line/column position of the provided source/line/column source position. | |
| 45 | */ | |
| 46 | export declare function generatedPositionFor(map: TraceMap, needle: SourceNeedle): GeneratedMapping | InvalidGeneratedMapping; | |
| 47 | /** | |
| 48 | * Finds all generated line/column positions of the provided source/line/column source position. | |
| 49 | */ | |
| 50 | export declare function allGeneratedPositionsFor(map: TraceMap, needle: SourceNeedle): GeneratedMapping[]; | |
| 51 | /** | |
| 52 | * Iterates each mapping in generated position order. | |
| 53 | */ | |
| 54 | export declare function eachMapping(map: TraceMap, cb: (mapping: EachMapping) => void): void; | |
| 55 | /** | |
| 56 | * Retrieves the source content for a particular source, if its found. Returns null if not. | |
| 57 | */ | |
| 58 | export declare function sourceContentFor(map: TraceMap, source: string): string | null; | |
| 59 | /** | |
| 60 | * Determines if the source is marked to ignore by the source map. | |
| 61 | */ | |
| 62 | export declare function isIgnored(map: TraceMap, source: string): boolean; | |
| 63 | /** | |
| 64 | * A helper that skips sorting of the input map's mappings array, which can be expensive for larger | |
| 65 | * maps. | |
| 66 | */ | |
| 67 | export declare function presortedDecodedMap(map: DecodedSourceMap, mapUrl?: string): TraceMap; | |
| 68 | /** | |
| 69 | * Returns a sourcemap object (with decoded mappings) suitable for passing to a library that expects | |
| 70 | * a sourcemap, or to JSON.stringify. | |
| 71 | */ | |
| 72 | export declare function decodedMap(map: TraceMap): Omit<DecodedSourceMap, 'mappings'> & { | |
| 73 | mappings: readonly SourceMapSegment[][]; | |
| 74 | }; | |
| 75 | /** | |
| 76 | * Returns a sourcemap object (with encoded mappings) suitable for passing to a library that expects | |
| 77 | * a sourcemap, or to JSON.stringify. | |
| 78 | */ | |
| 79 | export declare function encodedMap(map: TraceMap): EncodedSourceMap; | |
| 80 | //# sourceMappingURL=trace-mapping.d.ts.map |
| … | … | @@ -0,0 +1 @@ |
| 1 | {"version":3,"file":"trace-mapping.d.ts","sourceRoot":"","sources":["../src/trace-mapping.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAAE,gBAAgB,EAAkB,MAAM,qBAAqB,CAAC;AAC5E,OAAO,KAAK,EACV,WAAW,EACX,gBAAgB,EAChB,gBAAgB,EAChB,sBAAsB,EACtB,eAAe,EACf,uBAAuB,EACvB,gBAAgB,EAChB,cAAc,EACd,MAAM,EACN,YAAY,EACZ,SAAS,EACT,WAAW,EAIX,EAAE,EACH,MAAM,SAAS,CAAC;AAIjB,YAAY,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,YAAY,EACV,SAAS,EACT,gBAAgB,EAChB,gBAAgB,EAChB,OAAO,EACP,kBAAkB,EAClB,WAAW,EACX,IAAI,EACJ,WAAW,EACX,gBAAgB,EAChB,uBAAuB,EACvB,sBAAsB,EACtB,MAAM,EACN,eAAe,EACf,eAAe,IAAI,OAAO,EAC1B,uBAAuB,EACvB,cAAc,EACd,YAAY,EACZ,MAAM,EACN,sBAAsB,EACtB,sBAAsB,EACtB,wBAAwB,EACxB,aAAa,GACd,MAAM,SAAS,CAAC;AAajB,eAAO,MAAM,iBAAiB,KAAK,CAAC;AACpC,eAAO,MAAM,oBAAoB,IAAI,CAAC;AAEtC,OAAO,EAAE,UAAU,EAAE,UAAU,IAAI,MAAM,EAAE,MAAM,eAAe,CAAC;AAEjE,qBAAa,QAAS,YAAW,SAAS;IAChC,OAAO,EAAE,WAAW,CAAC,SAAS,CAAC,CAAC;IAChC,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC1B,KAAK,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IAC5B,UAAU,EAAE,WAAW,CAAC,YAAY,CAAC,CAAC;IACtC,OAAO,EAAE,WAAW,CAAC,SAAS,CAAC,CAAC;IAChC,cAAc,EAAE,WAAW,CAAC,gBAAgB,CAAC,CAAC;IAC9C,UAAU,EAAE,WAAW,CAAC,YAAY,CAAC,CAAC;IAEtC,eAAe,EAAE,MAAM,EAAE,CAAC;IAClC,QAAgB,QAAQ,CAAqB;IAE7C,QAAgB,QAAQ,CAAmC;IAC3D,QAAgB,YAAY,CAAY;IAExC,QAAgB,UAAU,CAAuB;IACjD,QAAgB,cAAc,CAA0B;gBAE5C,GAAG,EAAE,EAAE,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI;CAmC5D;AAUD;;GAEG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,QAAQ,GAAG,gBAAgB,CAAC,UAAU,CAAC,CAE3E;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,QAAQ,GAAG,QAAQ,CAAC,gBAAgB,CAAC,UAAU,CAAC,CAAC,CAErF;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,GAAG,EAAE,QAAQ,EACb,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,GACb,QAAQ,CAAC,gBAAgB,CAAC,GAAG,IAAI,CAiBnC;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,QAAQ,EACb,MAAM,EAAE,MAAM,GACb,eAAe,GAAG,sBAAsB,CAiC1C;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,QAAQ,EACb,MAAM,EAAE,YAAY,GACnB,gBAAgB,GAAG,uBAAuB,CAG5C;AAED;;GAEG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,GAAG,gBAAgB,EAAE,CAIhG;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,IAAI,GAAG,IAAI,CAgCnF;AASD;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAK7E;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAKhE;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,gBAAgB,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,QAAQ,CAIpF;AAED;;;GAGG;AACH,wBAAgB,UAAU,CACxB,GAAG,EAAE,QAAQ,GACZ,IAAI,CAAC,gBAAgB,EAAE,UAAU,CAAC,GAAG;IAAE,QAAQ,EAAE,SAAS,gBAAgB,EAAE,EAAE,CAAA;CAAE,CAElF;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,QAAQ,GAAG,gBAAgB,CAE1D"} |
| … | … | @@ -0,0 +1,80 @@ |
| 1 | import type { SourceMapSegment } from './sourcemap-segment.mts'; | |
| 2 | import type { SourceMapV3, DecodedSourceMap, EncodedSourceMap, InvalidOriginalMapping, OriginalMapping, InvalidGeneratedMapping, GeneratedMapping, SourceMapInput, Needle, SourceNeedle, SourceMap, EachMapping, Ro } from './types.mts'; | |
| 3 | export type { SourceMapSegment } from './sourcemap-segment.mts'; | |
| 4 | export type { SourceMap, DecodedSourceMap, EncodedSourceMap, Section, SectionedSourceMap, SourceMapV3, Bias, EachMapping, GeneratedMapping, InvalidGeneratedMapping, InvalidOriginalMapping, Needle, OriginalMapping, OriginalMapping as Mapping, SectionedSourceMapInput, SourceMapInput, SourceNeedle, XInput, EncodedSourceMapXInput, DecodedSourceMapXInput, SectionedSourceMapXInput, SectionXInput, } from './types.mts'; | |
| 5 | export declare const LEAST_UPPER_BOUND = -1; | |
| 6 | export declare const GREATEST_LOWER_BOUND = 1; | |
| 7 | export { FlattenMap, FlattenMap as AnyMap } from './flatten-map.mts'; | |
| 8 | export declare class TraceMap implements SourceMap { | |
| 9 | version: SourceMapV3['version']; | |
| 10 | file: SourceMapV3['file']; | |
| 11 | names: SourceMapV3['names']; | |
| 12 | sourceRoot: SourceMapV3['sourceRoot']; | |
| 13 | sources: SourceMapV3['sources']; | |
| 14 | sourcesContent: SourceMapV3['sourcesContent']; | |
| 15 | ignoreList: SourceMapV3['ignoreList']; | |
| 16 | resolvedSources: string[]; | |
| 17 | private _encoded; | |
| 18 | private _decoded; | |
| 19 | private _decodedMemo; | |
| 20 | private _bySources; | |
| 21 | private _bySourceMemos; | |
| 22 | constructor(map: Ro<SourceMapInput>, mapUrl?: string | null); | |
| 23 | } | |
| 24 | /** | |
| 25 | * Returns the encoded (VLQ string) form of the SourceMap's mappings field. | |
| 26 | */ | |
| 27 | export declare function encodedMappings(map: TraceMap): EncodedSourceMap['mappings']; | |
| 28 | /** | |
| 29 | * Returns the decoded (array of lines of segments) form of the SourceMap's mappings field. | |
| 30 | */ | |
| 31 | export declare function decodedMappings(map: TraceMap): Readonly<DecodedSourceMap['mappings']>; | |
| 32 | /** | |
| 33 | * A low-level API to find the segment associated with a generated line/column (think, from a | |
| 34 | * stack trace). Line and column here are 0-based, unlike `originalPositionFor`. | |
| 35 | */ | |
| 36 | export declare function traceSegment(map: TraceMap, line: number, column: number): Readonly<SourceMapSegment> | null; | |
| 37 | /** | |
| 38 | * A higher-level API to find the source/line/column associated with a generated line/column | |
| 39 | * (think, from a stack trace). Line is 1-based, but column is 0-based, due to legacy behavior in | |
| 40 | * `source-map` library. | |
| 41 | */ | |
| 42 | export declare function originalPositionFor(map: TraceMap, needle: Needle): OriginalMapping | InvalidOriginalMapping; | |
| 43 | /** | |
| 44 | * Finds the generated line/column position of the provided source/line/column source position. | |
| 45 | */ | |
| 46 | export declare function generatedPositionFor(map: TraceMap, needle: SourceNeedle): GeneratedMapping | InvalidGeneratedMapping; | |
| 47 | /** | |
| 48 | * Finds all generated line/column positions of the provided source/line/column source position. | |
| 49 | */ | |
| 50 | export declare function allGeneratedPositionsFor(map: TraceMap, needle: SourceNeedle): GeneratedMapping[]; | |
| 51 | /** | |
| 52 | * Iterates each mapping in generated position order. | |
| 53 | */ | |
| 54 | export declare function eachMapping(map: TraceMap, cb: (mapping: EachMapping) => void): void; | |
| 55 | /** | |
| 56 | * Retrieves the source content for a particular source, if its found. Returns null if not. | |
| 57 | */ | |
| 58 | export declare function sourceContentFor(map: TraceMap, source: string): string | null; | |
| 59 | /** | |
| 60 | * Determines if the source is marked to ignore by the source map. | |
| 61 | */ | |
| 62 | export declare function isIgnored(map: TraceMap, source: string): boolean; | |
| 63 | /** | |
| 64 | * A helper that skips sorting of the input map's mappings array, which can be expensive for larger | |
| 65 | * maps. | |
| 66 | */ | |
| 67 | export declare function presortedDecodedMap(map: DecodedSourceMap, mapUrl?: string): TraceMap; | |
| 68 | /** | |
| 69 | * Returns a sourcemap object (with decoded mappings) suitable for passing to a library that expects | |
| 70 | * a sourcemap, or to JSON.stringify. | |
| 71 | */ | |
| 72 | export declare function decodedMap(map: TraceMap): Omit<DecodedSourceMap, 'mappings'> & { | |
| 73 | mappings: readonly SourceMapSegment[][]; | |
| 74 | }; | |
| 75 | /** | |
| 76 | * Returns a sourcemap object (with encoded mappings) suitable for passing to a library that expects | |
| 77 | * a sourcemap, or to JSON.stringify. | |
| 78 | */ | |
| 79 | export declare function encodedMap(map: TraceMap): EncodedSourceMap; | |
| 80 | //# sourceMappingURL=trace-mapping.d.ts.map |
| … | … | @@ -0,0 +1 @@ |
| 1 | {"version":3,"file":"trace-mapping.d.ts","sourceRoot":"","sources":["../src/trace-mapping.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAAE,gBAAgB,EAAkB,MAAM,qBAAqB,CAAC;AAC5E,OAAO,KAAK,EACV,WAAW,EACX,gBAAgB,EAChB,gBAAgB,EAChB,sBAAsB,EACtB,eAAe,EACf,uBAAuB,EACvB,gBAAgB,EAChB,cAAc,EACd,MAAM,EACN,YAAY,EACZ,SAAS,EACT,WAAW,EAIX,EAAE,EACH,MAAM,SAAS,CAAC;AAIjB,YAAY,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,YAAY,EACV,SAAS,EACT,gBAAgB,EAChB,gBAAgB,EAChB,OAAO,EACP,kBAAkB,EAClB,WAAW,EACX,IAAI,EACJ,WAAW,EACX,gBAAgB,EAChB,uBAAuB,EACvB,sBAAsB,EACtB,MAAM,EACN,eAAe,EACf,eAAe,IAAI,OAAO,EAC1B,uBAAuB,EACvB,cAAc,EACd,YAAY,EACZ,MAAM,EACN,sBAAsB,EACtB,sBAAsB,EACtB,wBAAwB,EACxB,aAAa,GACd,MAAM,SAAS,CAAC;AAajB,eAAO,MAAM,iBAAiB,KAAK,CAAC;AACpC,eAAO,MAAM,oBAAoB,IAAI,CAAC;AAEtC,OAAO,EAAE,UAAU,EAAE,UAAU,IAAI,MAAM,EAAE,MAAM,eAAe,CAAC;AAEjE,qBAAa,QAAS,YAAW,SAAS;IAChC,OAAO,EAAE,WAAW,CAAC,SAAS,CAAC,CAAC;IAChC,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC1B,KAAK,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IAC5B,UAAU,EAAE,WAAW,CAAC,YAAY,CAAC,CAAC;IACtC,OAAO,EAAE,WAAW,CAAC,SAAS,CAAC,CAAC;IAChC,cAAc,EAAE,WAAW,CAAC,gBAAgB,CAAC,CAAC;IAC9C,UAAU,EAAE,WAAW,CAAC,YAAY,CAAC,CAAC;IAEtC,eAAe,EAAE,MAAM,EAAE,CAAC;IAClC,QAAgB,QAAQ,CAAqB;IAE7C,QAAgB,QAAQ,CAAmC;IAC3D,QAAgB,YAAY,CAAY;IAExC,QAAgB,UAAU,CAAuB;IACjD,QAAgB,cAAc,CAA0B;gBAE5C,GAAG,EAAE,EAAE,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI;CAmC5D;AAUD;;GAEG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,QAAQ,GAAG,gBAAgB,CAAC,UAAU,CAAC,CAE3E;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,QAAQ,GAAG,QAAQ,CAAC,gBAAgB,CAAC,UAAU,CAAC,CAAC,CAErF;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,GAAG,EAAE,QAAQ,EACb,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,GACb,QAAQ,CAAC,gBAAgB,CAAC,GAAG,IAAI,CAiBnC;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,QAAQ,EACb,MAAM,EAAE,MAAM,GACb,eAAe,GAAG,sBAAsB,CAiC1C;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,QAAQ,EACb,MAAM,EAAE,YAAY,GACnB,gBAAgB,GAAG,uBAAuB,CAG5C;AAED;;GAEG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,GAAG,gBAAgB,EAAE,CAIhG;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,IAAI,GAAG,IAAI,CAgCnF;AASD;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAK7E;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAKhE;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,gBAAgB,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,QAAQ,CAIpF;AAED;;;GAGG;AACH,wBAAgB,UAAU,CACxB,GAAG,EAAE,QAAQ,GACZ,IAAI,CAAC,gBAAgB,EAAE,UAAU,CAAC,GAAG;IAAE,QAAQ,EAAE,SAAS,gBAAgB,EAAE,EAAE,CAAA;CAAE,CAElF;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,QAAQ,GAAG,gBAAgB,CAE1D"} |
| … | … | @@ -0,0 +1,107 @@ |
| 1 | import type { SourceMapSegment } from './sourcemap-segment.cts'; | |
| 2 | import type { GREATEST_LOWER_BOUND, LEAST_UPPER_BOUND, TraceMap } from './trace-mapping.cts'; | |
| 3 | export interface SourceMapV3 { | |
| 4 | file?: string | null; | |
| 5 | names: string[]; | |
| 6 | sourceRoot?: string; | |
| 7 | sources: (string | null)[]; | |
| 8 | sourcesContent?: (string | null)[]; | |
| 9 | version: 3; | |
| 10 | ignoreList?: number[]; | |
| 11 | } | |
| 12 | export interface EncodedSourceMap extends SourceMapV3 { | |
| 13 | mappings: string; | |
| 14 | } | |
| 15 | export interface DecodedSourceMap extends SourceMapV3 { | |
| 16 | mappings: SourceMapSegment[][]; | |
| 17 | } | |
| 18 | export interface Section { | |
| 19 | offset: { | |
| 20 | line: number; | |
| 21 | column: number; | |
| 22 | }; | |
| 23 | map: EncodedSourceMap | DecodedSourceMap | SectionedSourceMap; | |
| 24 | } | |
| 25 | export interface SectionedSourceMap { | |
| 26 | file?: string | null; | |
| 27 | sections: Section[]; | |
| 28 | version: 3; | |
| 29 | } | |
| 30 | export type OriginalMapping = { | |
| 31 | source: string | null; | |
| 32 | line: number; | |
| 33 | column: number; | |
| 34 | name: string | null; | |
| 35 | }; | |
| 36 | export type InvalidOriginalMapping = { | |
| 37 | source: null; | |
| 38 | line: null; | |
| 39 | column: null; | |
| 40 | name: null; | |
| 41 | }; | |
| 42 | export type GeneratedMapping = { | |
| 43 | line: number; | |
| 44 | column: number; | |
| 45 | }; | |
| 46 | export type InvalidGeneratedMapping = { | |
| 47 | line: null; | |
| 48 | column: null; | |
| 49 | }; | |
| 50 | export type Bias = typeof GREATEST_LOWER_BOUND | typeof LEAST_UPPER_BOUND; | |
| 51 | export type XInput = { | |
| 52 | x_google_ignoreList?: SourceMapV3['ignoreList']; | |
| 53 | }; | |
| 54 | export type EncodedSourceMapXInput = EncodedSourceMap & XInput; | |
| 55 | export type DecodedSourceMapXInput = DecodedSourceMap & XInput; | |
| 56 | export type SectionedSourceMapXInput = Omit<SectionedSourceMap, 'sections'> & { | |
| 57 | sections: SectionXInput[]; | |
| 58 | }; | |
| 59 | export type SectionXInput = Omit<Section, 'map'> & { | |
| 60 | map: SectionedSourceMapInput; | |
| 61 | }; | |
| 62 | export type SourceMapInput = string | EncodedSourceMapXInput | DecodedSourceMapXInput | TraceMap; | |
| 63 | export type SectionedSourceMapInput = SourceMapInput | SectionedSourceMapXInput; | |
| 64 | export type Needle = { | |
| 65 | line: number; | |
| 66 | column: number; | |
| 67 | bias?: Bias; | |
| 68 | }; | |
| 69 | export type SourceNeedle = { | |
| 70 | source: string; | |
| 71 | line: number; | |
| 72 | column: number; | |
| 73 | bias?: Bias; | |
| 74 | }; | |
| 75 | export type EachMapping = { | |
| 76 | generatedLine: number; | |
| 77 | generatedColumn: number; | |
| 78 | source: null; | |
| 79 | originalLine: null; | |
| 80 | originalColumn: null; | |
| 81 | name: null; | |
| 82 | } | { | |
| 83 | generatedLine: number; | |
| 84 | generatedColumn: number; | |
| 85 | source: string | null; | |
| 86 | originalLine: number; | |
| 87 | originalColumn: number; | |
| 88 | name: string | null; | |
| 89 | }; | |
| 90 | export declare abstract class SourceMap { | |
| 91 | version: SourceMapV3['version']; | |
| 92 | file: SourceMapV3['file']; | |
| 93 | names: SourceMapV3['names']; | |
| 94 | sourceRoot: SourceMapV3['sourceRoot']; | |
| 95 | sources: SourceMapV3['sources']; | |
| 96 | sourcesContent: SourceMapV3['sourcesContent']; | |
| 97 | resolvedSources: SourceMapV3['sources']; | |
| 98 | ignoreList: SourceMapV3['ignoreList']; | |
| 99 | } | |
| 100 | export type Ro<T> = T extends Array<infer V> ? V[] | Readonly<V[]> | RoArray<V> | Readonly<RoArray<V>> : T extends object ? T | Readonly<T> | RoObject<T> | Readonly<RoObject<T>> : T; | |
| 101 | type RoArray<T> = Ro<T>[]; | |
| 102 | type RoObject<T> = { | |
| 103 | [K in keyof T]: T[K] | Ro<T[K]>; | |
| 104 | }; | |
| 105 | export declare function parse<T>(map: T): Exclude<T, string>; | |
| 106 | export {}; | |
| 107 | //# sourceMappingURL=types.d.ts.map |
| … | … | @@ -0,0 +1 @@ |
| 1 | {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,KAAK,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAEzF,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC;IAC3B,cAAc,CAAC,EAAE,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC;IACnC,OAAO,EAAE,CAAC,CAAC;IACX,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACnD,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACnD,QAAQ,EAAE,gBAAgB,EAAE,EAAE,CAAC;CAChC;AAED,MAAM,WAAW,OAAO;IACtB,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACzC,GAAG,EAAE,gBAAgB,GAAG,gBAAgB,GAAG,kBAAkB,CAAC;CAC/D;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,OAAO,EAAE,CAAC,CAAC;CACZ;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,MAAM,EAAE,IAAI,CAAC;IACb,IAAI,EAAE,IAAI,CAAC;IACX,MAAM,EAAE,IAAI,CAAC;IACb,IAAI,EAAE,IAAI,CAAC;CACZ,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AACF,MAAM,MAAM,uBAAuB,GAAG;IACpC,IAAI,EAAE,IAAI,CAAC;IACX,MAAM,EAAE,IAAI,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,IAAI,GAAG,OAAO,oBAAoB,GAAG,OAAO,iBAAiB,CAAC;AAE1E,MAAM,MAAM,MAAM,GAAG;IAAE,mBAAmB,CAAC,EAAE,WAAW,CAAC,YAAY,CAAC,CAAA;CAAE,CAAC;AACzE,MAAM,MAAM,sBAAsB,GAAG,gBAAgB,GAAG,MAAM,CAAC;AAC/D,MAAM,MAAM,sBAAsB,GAAG,gBAAgB,GAAG,MAAM,CAAC;AAC/D,MAAM,MAAM,wBAAwB,GAAG,IAAI,CAAC,kBAAkB,EAAE,UAAU,CAAC,GAAG;IAC5E,QAAQ,EAAE,aAAa,EAAE,CAAC;CAC3B,CAAC;AACF,MAAM,MAAM,aAAa,GAAG,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,GAAG;IACjD,GAAG,EAAE,uBAAuB,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,sBAAsB,GAAG,sBAAsB,GAAG,QAAQ,CAAC;AACjG,MAAM,MAAM,uBAAuB,GAAG,cAAc,GAAG,wBAAwB,CAAC;AAEhF,MAAM,MAAM,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,IAAI,CAAA;CAAE,CAAC;AACnE,MAAM,MAAM,YAAY,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,IAAI,CAAA;CAAE,CAAC;AAEzF,MAAM,MAAM,WAAW,GACnB;IACE,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,IAAI,CAAC;IACb,YAAY,EAAE,IAAI,CAAC;IACnB,cAAc,EAAE,IAAI,CAAC;IACrB,IAAI,EAAE,IAAI,CAAC;CACZ,GACD;IACE,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB,CAAC;AAEN,8BAAsB,SAAS;IACrB,OAAO,EAAE,WAAW,CAAC,SAAS,CAAC,CAAC;IAChC,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC1B,KAAK,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IAC5B,UAAU,EAAE,WAAW,CAAC,YAAY,CAAC,CAAC;IACtC,OAAO,EAAE,WAAW,CAAC,SAAS,CAAC,CAAC;IAChC,cAAc,EAAE,WAAW,CAAC,gBAAgB,CAAC,CAAC;IAC9C,eAAe,EAAE,WAAW,CAAC,SAAS,CAAC,CAAC;IACxC,UAAU,EAAE,WAAW,CAAC,YAAY,CAAC,CAAC;CAC/C;AAED,MAAM,MAAM,EAAE,CAAC,CAAC,IACd,CAAC,SAAS,KAAK,CAAC,MAAM,CAAC,CAAC,GACpB,CAAC,EAAE,GAAG,QAAQ,CAAC,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,GACvD,CAAC,SAAS,MAAM,GACd,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,GACrD,CAAC,CAAC;AACV,KAAK,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;AAC1B,KAAK,QAAQ,CAAC,CAAC,IAAI;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAAE,CAAC;AAEvD,wBAAgB,KAAK,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,EAAE,MAAM,CAAC,CAEnD"} |
| … | … | @@ -0,0 +1,107 @@ |
| 1 | import type { SourceMapSegment } from './sourcemap-segment.mts'; | |
| 2 | import type { GREATEST_LOWER_BOUND, LEAST_UPPER_BOUND, TraceMap } from './trace-mapping.mts'; | |
| 3 | export interface SourceMapV3 { | |
| 4 | file?: string | null; | |
| 5 | names: string[]; | |
| 6 | sourceRoot?: string; | |
| 7 | sources: (string | null)[]; | |
| 8 | sourcesContent?: (string | null)[]; | |
| 9 | version: 3; | |
| 10 | ignoreList?: number[]; | |
| 11 | } | |
| 12 | export interface EncodedSourceMap extends SourceMapV3 { | |
| 13 | mappings: string; | |
| 14 | } | |
| 15 | export interface DecodedSourceMap extends SourceMapV3 { | |
| 16 | mappings: SourceMapSegment[][]; | |
| 17 | } | |
| 18 | export interface Section { | |
| 19 | offset: { | |
| 20 | line: number; | |
| 21 | column: number; | |
| 22 | }; | |
| 23 | map: EncodedSourceMap | DecodedSourceMap | SectionedSourceMap; | |
| 24 | } | |
| 25 | export interface SectionedSourceMap { | |
| 26 | file?: string | null; | |
| 27 | sections: Section[]; | |
| 28 | version: 3; | |
| 29 | } | |
| 30 | export type OriginalMapping = { | |
| 31 | source: string | null; | |
| 32 | line: number; | |
| 33 | column: number; | |
| 34 | name: string | null; | |
| 35 | }; | |
| 36 | export type InvalidOriginalMapping = { | |
| 37 | source: null; | |
| 38 | line: null; | |
| 39 | column: null; | |
| 40 | name: null; | |
| 41 | }; | |
| 42 | export type GeneratedMapping = { | |
| 43 | line: number; | |
| 44 | column: number; | |
| 45 | }; | |
| 46 | export type InvalidGeneratedMapping = { | |
| 47 | line: null; | |
| 48 | column: null; | |
| 49 | }; | |
| 50 | export type Bias = typeof GREATEST_LOWER_BOUND | typeof LEAST_UPPER_BOUND; | |
| 51 | export type XInput = { | |
| 52 | x_google_ignoreList?: SourceMapV3['ignoreList']; | |
| 53 | }; | |
| 54 | export type EncodedSourceMapXInput = EncodedSourceMap & XInput; | |
| 55 | export type DecodedSourceMapXInput = DecodedSourceMap & XInput; | |
| 56 | export type SectionedSourceMapXInput = Omit<SectionedSourceMap, 'sections'> & { | |
| 57 | sections: SectionXInput[]; | |
| 58 | }; | |
| 59 | export type SectionXInput = Omit<Section, 'map'> & { | |
| 60 | map: SectionedSourceMapInput; | |
| 61 | }; | |
| 62 | export type SourceMapInput = string | EncodedSourceMapXInput | DecodedSourceMapXInput | TraceMap; | |
| 63 | export type SectionedSourceMapInput = SourceMapInput | SectionedSourceMapXInput; | |
| 64 | export type Needle = { | |
| 65 | line: number; | |
| 66 | column: number; | |
| 67 | bias?: Bias; | |
| 68 | }; | |
| 69 | export type SourceNeedle = { | |
| 70 | source: string; | |
| 71 | line: number; | |
| 72 | column: number; | |
| 73 | bias?: Bias; | |
| 74 | }; | |
| 75 | export type EachMapping = { | |
| 76 | generatedLine: number; | |
| 77 | generatedColumn: number; | |
| 78 | source: null; | |
| 79 | originalLine: null; | |
| 80 | originalColumn: null; | |
| 81 | name: null; | |
| 82 | } | { | |
| 83 | generatedLine: number; | |
| 84 | generatedColumn: number; | |
| 85 | source: string | null; | |
| 86 | originalLine: number; | |
| 87 | originalColumn: number; | |
| 88 | name: string | null; | |
| 89 | }; | |
| 90 | export declare abstract class SourceMap { | |
| 91 | version: SourceMapV3['version']; | |
| 92 | file: SourceMapV3['file']; | |
| 93 | names: SourceMapV3['names']; | |
| 94 | sourceRoot: SourceMapV3['sourceRoot']; | |
| 95 | sources: SourceMapV3['sources']; | |
| 96 | sourcesContent: SourceMapV3['sourcesContent']; | |
| 97 | resolvedSources: SourceMapV3['sources']; | |
| 98 | ignoreList: SourceMapV3['ignoreList']; | |
| 99 | } | |
| 100 | export type Ro<T> = T extends Array<infer V> ? V[] | Readonly<V[]> | RoArray<V> | Readonly<RoArray<V>> : T extends object ? T | Readonly<T> | RoObject<T> | Readonly<RoObject<T>> : T; | |
| 101 | type RoArray<T> = Ro<T>[]; | |
| 102 | type RoObject<T> = { | |
| 103 | [K in keyof T]: T[K] | Ro<T[K]>; | |
| 104 | }; | |
| 105 | export declare function parse<T>(map: T): Exclude<T, string>; | |
| 106 | export {}; | |
| 107 | //# sourceMappingURL=types.d.ts.map |
| … | … | @@ -0,0 +1 @@ |
| 1 | {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,KAAK,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAEzF,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC;IAC3B,cAAc,CAAC,EAAE,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC;IACnC,OAAO,EAAE,CAAC,CAAC;IACX,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACnD,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACnD,QAAQ,EAAE,gBAAgB,EAAE,EAAE,CAAC;CAChC;AAED,MAAM,WAAW,OAAO;IACtB,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACzC,GAAG,EAAE,gBAAgB,GAAG,gBAAgB,GAAG,kBAAkB,CAAC;CAC/D;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,OAAO,EAAE,CAAC,CAAC;CACZ;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,MAAM,EAAE,IAAI,CAAC;IACb,IAAI,EAAE,IAAI,CAAC;IACX,MAAM,EAAE,IAAI,CAAC;IACb,IAAI,EAAE,IAAI,CAAC;CACZ,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AACF,MAAM,MAAM,uBAAuB,GAAG;IACpC,IAAI,EAAE,IAAI,CAAC;IACX,MAAM,EAAE,IAAI,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,IAAI,GAAG,OAAO,oBAAoB,GAAG,OAAO,iBAAiB,CAAC;AAE1E,MAAM,MAAM,MAAM,GAAG;IAAE,mBAAmB,CAAC,EAAE,WAAW,CAAC,YAAY,CAAC,CAAA;CAAE,CAAC;AACzE,MAAM,MAAM,sBAAsB,GAAG,gBAAgB,GAAG,MAAM,CAAC;AAC/D,MAAM,MAAM,sBAAsB,GAAG,gBAAgB,GAAG,MAAM,CAAC;AAC/D,MAAM,MAAM,wBAAwB,GAAG,IAAI,CAAC,kBAAkB,EAAE,UAAU,CAAC,GAAG;IAC5E,QAAQ,EAAE,aAAa,EAAE,CAAC;CAC3B,CAAC;AACF,MAAM,MAAM,aAAa,GAAG,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,GAAG;IACjD,GAAG,EAAE,uBAAuB,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,sBAAsB,GAAG,sBAAsB,GAAG,QAAQ,CAAC;AACjG,MAAM,MAAM,uBAAuB,GAAG,cAAc,GAAG,wBAAwB,CAAC;AAEhF,MAAM,MAAM,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,IAAI,CAAA;CAAE,CAAC;AACnE,MAAM,MAAM,YAAY,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,IAAI,CAAA;CAAE,CAAC;AAEzF,MAAM,MAAM,WAAW,GACnB;IACE,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,IAAI,CAAC;IACb,YAAY,EAAE,IAAI,CAAC;IACnB,cAAc,EAAE,IAAI,CAAC;IACrB,IAAI,EAAE,IAAI,CAAC;CACZ,GACD;IACE,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB,CAAC;AAEN,8BAAsB,SAAS;IACrB,OAAO,EAAE,WAAW,CAAC,SAAS,CAAC,CAAC;IAChC,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC1B,KAAK,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IAC5B,UAAU,EAAE,WAAW,CAAC,YAAY,CAAC,CAAC;IACtC,OAAO,EAAE,WAAW,CAAC,SAAS,CAAC,CAAC;IAChC,cAAc,EAAE,WAAW,CAAC,gBAAgB,CAAC,CAAC;IAC9C,eAAe,EAAE,WAAW,CAAC,SAAS,CAAC,CAAC;IACxC,UAAU,EAAE,WAAW,CAAC,YAAY,CAAC,CAAC;CAC/C;AAED,MAAM,MAAM,EAAE,CAAC,CAAC,IACd,CAAC,SAAS,KAAK,CAAC,MAAM,CAAC,CAAC,GACpB,CAAC,EAAE,GAAG,QAAQ,CAAC,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,GACvD,CAAC,SAAS,MAAM,GACd,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,GACrD,CAAC,CAAC;AACV,KAAK,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;AAC1B,KAAK,QAAQ,CAAC,CAAC,IAAI;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAAE,CAAC;AAEvD,wBAAgB,KAAK,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,EAAE,MAAM,CAAC,CAEnD"} |
| … | … | @@ -0,0 +1,21 @@ |
| 1 | MIT License | |
| 2 | ||
| 3 | Copyright (c) Microsoft Corporation. | |
| 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 |
| … | … | @@ -0,0 +1,15 @@ |
| 1 | # Installation | |
| 2 | > `npm install --save @types/istanbul-lib-coverage` | |
| 3 | ||
| 4 | # Summary | |
| 5 | This package contains type definitions for istanbul-lib-coverage (https://istanbul.js.org). | |
| 6 | ||
| 7 | # Details | |
| 8 | Files were exported from https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/istanbul-lib-coverage. | |
| 9 | ||
| 10 | ### Additional Details | |
| 11 | * Last updated: Tue, 07 Nov 2023 03:09:37 GMT | |
| 12 | * Dependencies: none | |
| 13 | ||
| 14 | # Credits | |
| 15 | These definitions were written by [Jason Cheatham](https://github.com/jason0x43). |
| … | … | @@ -0,0 +1,111 @@ |
| 1 | export interface CoverageSummaryData { | |
| 2 | lines: Totals; | |
| 3 | statements: Totals; | |
| 4 | branches: Totals; | |
| 5 | functions: Totals; | |
| 6 | } | |
| 7 | ||
| 8 | export class CoverageSummary { | |
| 9 | constructor(data: CoverageSummary | CoverageSummaryData); | |
| 10 | merge(obj: CoverageSummary): CoverageSummary; | |
| 11 | toJSON(): CoverageSummaryData; | |
| 12 | isEmpty(): boolean; | |
| 13 | data: CoverageSummaryData; | |
| 14 | lines: Totals; | |
| 15 | statements: Totals; | |
| 16 | branches: Totals; | |
| 17 | functions: Totals; | |
| 18 | } | |
| 19 | ||
| 20 | export interface CoverageMapData { | |
| 21 | [key: string]: FileCoverage | FileCoverageData; | |
| 22 | } | |
| 23 | ||
| 24 | export class CoverageMap { | |
| 25 | constructor(data: CoverageMapData | CoverageMap); | |
| 26 | addFileCoverage(pathOrObject: string | FileCoverage | FileCoverageData): void; | |
| 27 | files(): string[]; | |
| 28 | fileCoverageFor(filename: string): FileCoverage; | |
| 29 | filter(callback: (key: string) => boolean): void; | |
| 30 | getCoverageSummary(): CoverageSummary; | |
| 31 | merge(data: CoverageMapData | CoverageMap): void; | |
| 32 | toJSON(): CoverageMapData; | |
| 33 | data: CoverageMapData; | |
| 34 | } | |
| 35 | ||
| 36 | export interface Location { | |
| 37 | line: number; | |
| 38 | column: number; | |
| 39 | } | |
| 40 | ||
| 41 | export interface Range { | |
| 42 | start: Location; | |
| 43 | end: Location; | |
| 44 | } | |
| 45 | ||
| 46 | export interface BranchMapping { | |
| 47 | loc: Range; | |
| 48 | type: string; | |
| 49 | locations: Range[]; | |
| 50 | line: number; | |
| 51 | } | |
| 52 | ||
| 53 | export interface FunctionMapping { | |
| 54 | name: string; | |
| 55 | decl: Range; | |
| 56 | loc: Range; | |
| 57 | line: number; | |
| 58 | } | |
| 59 | ||
| 60 | export interface FileCoverageData { | |
| 61 | path: string; | |
| 62 | statementMap: { [key: string]: Range }; | |
| 63 | fnMap: { [key: string]: FunctionMapping }; | |
| 64 | branchMap: { [key: string]: BranchMapping }; | |
| 65 | s: { [key: string]: number }; | |
| 66 | f: { [key: string]: number }; | |
| 67 | b: { [key: string]: number[] }; | |
| 68 | } | |
| 69 | ||
| 70 | export interface Totals { | |
| 71 | total: number; | |
| 72 | covered: number; | |
| 73 | skipped: number; | |
| 74 | pct: number; | |
| 75 | } | |
| 76 | ||
| 77 | export interface Coverage { | |
| 78 | covered: number; | |
| 79 | total: number; | |
| 80 | coverage: number; | |
| 81 | } | |
| 82 | ||
| 83 | export class FileCoverage implements FileCoverageData { | |
| 84 | constructor(data: string | FileCoverage | FileCoverageData); | |
| 85 | merge(other: FileCoverageData): void; | |
| 86 | getBranchCoverageByLine(): { [line: number]: Coverage }; | |
| 87 | getLineCoverage(): { [line: number]: number }; | |
| 88 | getUncoveredLines(): number[]; | |
| 89 | resetHits(): void; | |
| 90 | computeBranchTotals(): Totals; | |
| 91 | computeSimpleTotals(): Totals; | |
| 92 | toSummary(): CoverageSummary; | |
| 93 | toJSON(): object; | |
| 94 | ||
| 95 | data: FileCoverageData; | |
| 96 | path: string; | |
| 97 | statementMap: { [key: string]: Range }; | |
| 98 | fnMap: { [key: string]: FunctionMapping }; | |
| 99 | branchMap: { [key: string]: BranchMapping }; | |
| 100 | s: { [key: string]: number }; | |
| 101 | f: { [key: string]: number }; | |
| 102 | b: { [key: string]: number[] }; | |
| 103 | } | |
| 104 | ||
| 105 | export const classes: { | |
| 106 | FileCoverage: FileCoverage; | |
| 107 | }; | |
| 108 | ||
| 109 | export function createCoverageMap(data?: CoverageMap | CoverageMapData): CoverageMap; | |
| 110 | export function createCoverageSummary(obj?: CoverageSummary | CoverageSummaryData): CoverageSummary; | |
| 111 | export function createFileCoverage(pathOrObject: string | FileCoverage | FileCoverageData): FileCoverage; |
| … | … | @@ -0,0 +1,25 @@ |
| 1 | { | |
| 2 | "name": "@types/istanbul-lib-coverage", | |
| 3 | "version": "2.0.6", | |
| 4 | "description": "TypeScript definitions for istanbul-lib-coverage", | |
| 5 | "homepage": "https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/istanbul-lib-coverage", | |
| 6 | "license": "MIT", | |
| 7 | "contributors": [ | |
| 8 | { | |
| 9 | "name": "Jason Cheatham", | |
| 10 | "githubUsername": "jason0x43", | |
| 11 | "url": "https://github.com/jason0x43" | |
| 12 | } | |
| 13 | ], | |
| 14 | "main": "", | |
| 15 | "types": "index.d.ts", | |
| 16 | "repository": { | |
| 17 | "type": "git", | |
| 18 | "url": "https://github.com/DefinitelyTyped/DefinitelyTyped.git", | |
| 19 | "directory": "types/istanbul-lib-coverage" | |
| 20 | }, | |
| 21 | "scripts": {}, | |
| 22 | "dependencies": {}, | |
| 23 | "typesPublisherContentHash": "36c823c8b3f66dab91254b0f7299de71768ad8836bfbfcaa062409dd86fbbd61", | |
| 24 | "typeScriptVersion": "4.5" | |
| 25 | } |
| … | … | @@ -0,0 +1,201 @@ |
| 1 | Apache License | |
| 2 | Version 2.0, January 2004 | |
| 3 | http://www.apache.org/licenses/ | |
| 4 | ||
| 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | |
| 6 | ||
| 7 | 1. Definitions. | |
| 8 | ||
| 9 | "License" shall mean the terms and conditions for use, reproduction, | |
| 10 | and distribution as defined by Sections 1 through 9 of this document. | |
| 11 | ||
| 12 | "Licensor" shall mean the copyright owner or entity authorized by | |
| 13 | the copyright owner that is granting the License. | |
| 14 | ||
| 15 | "Legal Entity" shall mean the union of the acting entity and all | |
| 16 | other entities that control, are controlled by, or are under common | |
| 17 | control with that entity. For the purposes of this definition, | |
| 18 | "control" means (i) the power, direct or indirect, to cause the | |
| 19 | direction or management of such entity, whether by contract or | |
| 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the | |
| 21 | outstanding shares, or (iii) beneficial ownership of such entity. | |
| 22 | ||
| 23 | "You" (or "Your") shall mean an individual or Legal Entity | |
| 24 | exercising permissions granted by this License. | |
| 25 | ||
| 26 | "Source" form shall mean the preferred form for making modifications, | |
| 27 | including but not limited to software source code, documentation | |
| 28 | source, and configuration files. | |
| 29 | ||
| 30 | "Object" form shall mean any form resulting from mechanical | |
| 31 | transformation or translation of a Source form, including but | |
| 32 | not limited to compiled object code, generated documentation, | |
| 33 | and conversions to other media types. | |
| 34 | ||
| 35 | "Work" shall mean the work of authorship, whether in Source or | |
| 36 | Object form, made available under the License, as indicated by a | |
| 37 | copyright notice that is included in or attached to the work | |
| 38 | (an example is provided in the Appendix below). | |
| 39 | ||
| 40 | "Derivative Works" shall mean any work, whether in Source or Object | |
| 41 | form, that is based on (or derived from) the Work and for which the | |
| 42 | editorial revisions, annotations, elaborations, or other modifications | |
| 43 | represent, as a whole, an original work of authorship. For the purposes | |
| 44 | of this License, Derivative Works shall not include works that remain | |
| 45 | separable from, or merely link (or bind by name) to the interfaces of, | |
| 46 | the Work and Derivative Works thereof. | |
| 47 | ||
| 48 | "Contribution" shall mean any work of authorship, including | |
| 49 | the original version of the Work and any modifications or additions | |
| 50 | to that Work or Derivative Works thereof, that is intentionally | |
| 51 | submitted to Licensor for inclusion in the Work by the copyright owner | |
| 52 | or by an individual or Legal Entity authorized to submit on behalf of | |
| 53 | the copyright owner. For the purposes of this definition, "submitted" | |
| 54 | means any form of electronic, verbal, or written communication sent | |
| 55 | to the Licensor or its representatives, including but not limited to | |
| 56 | communication on electronic mailing lists, source code control systems, | |
| 57 | and issue tracking systems that are managed by, or on behalf of, the | |
| 58 | Licensor for the purpose of discussing and improving the Work, but | |
| 59 | excluding communication that is conspicuously marked or otherwise | |
| 60 | designated in writing by the copyright owner as "Not a Contribution." | |
| 61 | ||
| 62 | "Contributor" shall mean Licensor and any individual or Legal Entity | |
| 63 | on behalf of whom a Contribution has been received by Licensor and | |
| 64 | subsequently incorporated within the Work. | |
| 65 | ||
| 66 | 2. Grant of Copyright License. Subject to the terms and conditions of | |
| 67 | this License, each Contributor hereby grants to You a perpetual, | |
| 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 69 | copyright license to reproduce, prepare Derivative Works of, | |
| 70 | publicly display, publicly perform, sublicense, and distribute the | |
| 71 | Work and such Derivative Works in Source or Object form. | |
| 72 | ||
| 73 | 3. Grant of Patent License. Subject to the terms and conditions of | |
| 74 | this License, each Contributor hereby grants to You a perpetual, | |
| 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 76 | (except as stated in this section) patent license to make, have made, | |
| 77 | use, offer to sell, sell, import, and otherwise transfer the Work, | |
| 78 | where such license applies only to those patent claims licensable | |
| 79 | by such Contributor that are necessarily infringed by their | |
| 80 | Contribution(s) alone or by combination of their Contribution(s) | |
| 81 | with the Work to which such Contribution(s) was submitted. If You | |
| 82 | institute patent litigation against any entity (including a | |
| 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work | |
| 84 | or a Contribution incorporated within the Work constitutes direct | |
| 85 | or contributory patent infringement, then any patent licenses | |
| 86 | granted to You under this License for that Work shall terminate | |
| 87 | as of the date such litigation is filed. | |
| 88 | ||
| 89 | 4. Redistribution. You may reproduce and distribute copies of the | |
| 90 | Work or Derivative Works thereof in any medium, with or without | |
| 91 | modifications, and in Source or Object form, provided that You | |
| 92 | meet the following conditions: | |
| 93 | ||
| 94 | (a) You must give any other recipients of the Work or | |
| 95 | Derivative Works a copy of this License; and | |
| 96 | ||
| 97 | (b) You must cause any modified files to carry prominent notices | |
| 98 | stating that You changed the files; and | |
| 99 | ||
| 100 | (c) You must retain, in the Source form of any Derivative Works | |
| 101 | that You distribute, all copyright, patent, trademark, and | |
| 102 | attribution notices from the Source form of the Work, | |
| 103 | excluding those notices that do not pertain to any part of | |
| 104 | the Derivative Works; and | |
| 105 | ||
| 106 | (d) If the Work includes a "NOTICE" text file as part of its | |
| 107 | distribution, then any Derivative Works that You distribute must | |
| 108 | include a readable copy of the attribution notices contained | |
| 109 | within such NOTICE file, excluding those notices that do not | |
| 110 | pertain to any part of the Derivative Works, in at least one | |
| 111 | of the following places: within a NOTICE text file distributed | |
| 112 | as part of the Derivative Works; within the Source form or | |
| 113 | documentation, if provided along with the Derivative Works; or, | |
| 114 | within a display generated by the Derivative Works, if and | |
| 115 | wherever such third-party notices normally appear. The contents | |
| 116 | of the NOTICE file are for informational purposes only and | |
| 117 | do not modify the License. You may add Your own attribution | |
| 118 | notices within Derivative Works that You distribute, alongside | |
| 119 | or as an addendum to the NOTICE text from the Work, provided | |
| 120 | that such additional attribution notices cannot be construed | |
| 121 | as modifying the License. | |
| 122 | ||
| 123 | You may add Your own copyright statement to Your modifications and | |
| 124 | may provide additional or different license terms and conditions | |
| 125 | for use, reproduction, or distribution of Your modifications, or | |
| 126 | for any such Derivative Works as a whole, provided Your use, | |
| 127 | reproduction, and distribution of the Work otherwise complies with | |
| 128 | the conditions stated in this License. | |
| 129 | ||
| 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, | |
| 131 | any Contribution intentionally submitted for inclusion in the Work | |
| 132 | by You to the Licensor shall be under the terms and conditions of | |
| 133 | this License, without any additional terms or conditions. | |
| 134 | Notwithstanding the above, nothing herein shall supersede or modify | |
| 135 | the terms of any separate license agreement you may have executed | |
| 136 | with Licensor regarding such Contributions. | |
| 137 | ||
| 138 | 6. Trademarks. This License does not grant permission to use the trade | |
| 139 | names, trademarks, service marks, or product names of the Licensor, | |
| 140 | except as required for reasonable and customary use in describing the | |
| 141 | origin of the Work and reproducing the content of the NOTICE file. | |
| 142 | ||
| 143 | 7. Disclaimer of Warranty. Unless required by applicable law or | |
| 144 | agreed to in writing, Licensor provides the Work (and each | |
| 145 | Contributor provides its Contributions) on an "AS IS" BASIS, | |
| 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |
| 147 | implied, including, without limitation, any warranties or conditions | |
| 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | |
| 149 | PARTICULAR PURPOSE. You are solely responsible for determining the | |
| 150 | appropriateness of using or redistributing the Work and assume any | |
| 151 | risks associated with Your exercise of permissions under this License. | |
| 152 | ||
| 153 | 8. Limitation of Liability. In no event and under no legal theory, | |
| 154 | whether in tort (including negligence), contract, or otherwise, | |
| 155 | unless required by applicable law (such as deliberate and grossly | |
| 156 | negligent acts) or agreed to in writing, shall any Contributor be | |
| 157 | liable to You for damages, including any direct, indirect, special, | |
| 158 | incidental, or consequential damages of any character arising as a | |
| 159 | result of this License or out of the use or inability to use the | |
| 160 | Work (including but not limited to damages for loss of goodwill, | |
| 161 | work stoppage, computer failure or malfunction, or any and all | |
| 162 | other commercial damages or losses), even if such Contributor | |
| 163 | has been advised of the possibility of such damages. | |
| 164 | ||
| 165 | 9. Accepting Warranty or Additional Liability. While redistributing | |
| 166 | the Work or Derivative Works thereof, You may choose to offer, | |
| 167 | and charge a fee for, acceptance of support, warranty, indemnity, | |
| 168 | or other liability obligations and/or rights consistent with this | |
| 169 | License. However, in accepting such obligations, You may act only | |
| 170 | on Your own behalf and on Your sole responsibility, not on behalf | |
| 171 | of any other Contributor, and only if You agree to indemnify, | |
| 172 | defend, and hold each Contributor harmless for any liability | |
| 173 | incurred by, or claims asserted against, such Contributor by reason | |
| 174 | of your accepting any such warranty or additional liability. | |
| 175 | ||
| 176 | END OF TERMS AND CONDITIONS | |
| 177 | ||
| 178 | APPENDIX: How to apply the Apache License to your work. | |
| 179 | ||
| 180 | To apply the Apache License to your work, attach the following | |
| 181 | boilerplate notice, with the fields enclosed by brackets "[]" | |
| 182 | replaced with your own identifying information. (Don't include | |
| 183 | the brackets!) The text should be enclosed in the appropriate | |
| 184 | comment syntax for the file format. We also recommend that a | |
| 185 | file or class name and description of purpose be included on the | |
| 186 | same "printed page" as the copyright notice for easier | |
| 187 | identification within third-party archives. | |
| 188 | ||
| 189 | Copyright [yyyy] [name of copyright owner] | |
| 190 | ||
| 191 | Licensed under the Apache License, Version 2.0 (the "License"); | |
| 192 | you may not use this file except in compliance with the License. | |
| 193 | You may obtain a copy of the License at | |
| 194 | ||
| 195 | http://www.apache.org/licenses/LICENSE-2.0 | |
| 196 | ||
| 197 | Unless required by applicable law or agreed to in writing, software | |
| 198 | distributed under the License is distributed on an "AS IS" BASIS, | |
| 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 200 | See the License for the specific language governing permissions and | |
| 201 | limitations under the License. |
| … | … | @@ -0,0 +1,88 @@ |
| 1 | # adaptive-timeout | |
| 2 | ||
| 3 | Adaptive timeout calculation with EWMA-based tracking and fallback strategies. | |
| 4 | ||
| 5 | ## Install | |
| 6 | ||
| 7 | ``` | |
| 8 | npm install adaptive-timeout | |
| 9 | ``` | |
| 10 | ||
| 11 | ## Usage | |
| 12 | ||
| 13 | ```js | |
| 14 | const AdaptiveTimeout = require('adaptive-timeout') | |
| 15 | ||
| 16 | const timeouts = new AdaptiveTimeout() | |
| 17 | ||
| 18 | // Record a sample when you get a response | |
| 19 | timeouts.put('10.0.0.1:8080', 150) | |
| 20 | ||
| 21 | // Get timeout for next request (attempt 1, 2, 3...) | |
| 22 | timeouts.get('10.0.0.1:8080', 1) // adaptive based on recorded samples | |
| 23 | timeouts.get('unknown-peer', 1) // uses fallback strategy | |
| 24 | ``` | |
| 25 | ||
| 26 | ## API | |
| 27 | ||
| 28 | #### `const timeouts = new AdaptiveTimeout([options])` | |
| 29 | ||
| 30 | Create a new instance. | |
| 31 | ||
| 32 | Options: | |
| 33 | ||
| 34 | - `maxSize` - max cache entries (default: `65536`) | |
| 35 | - `maxAge` - entry TTL in ms (default: `600000` / 10 minutes) | |
| 36 | - `fallback` - timeout sequence for unknown keys (default: `[500, 750, 1000, 1500, 2000]`) | |
| 37 | - `min` - minimum timeout for known keys (default: `300`) | |
| 38 | - `max` - maximum timeout for known keys (default: `5000`) | |
| 39 | - `jitter` - random jitter range in ms (default: `256`) | |
| 40 | ||
| 41 | #### `timeouts.put(key, value)` | |
| 42 | ||
| 43 | Record a sample. Updates the exponentially weighted moving average. | |
| 44 | ||
| 45 | Returns `{ avg, variance }`. | |
| 46 | ||
| 47 | #### `timeouts.get(key, [attempt])` | |
| 48 | ||
| 49 | Get a timeout value. | |
| 50 | ||
| 51 | For known keys: calculates adaptive timeout with linear backoff based on attempt number. | |
| 52 | ||
| 53 | For unknown keys: returns value from fallback sequence based on attempt number. | |
| 54 | ||
| 55 | #### `timeouts.getValue(key)` | |
| 56 | ||
| 57 | Get raw stats for a key. Returns `{ avg, variance }` or `null`. | |
| 58 | ||
| 59 | #### `timeouts.has(key)` | |
| 60 | ||
| 61 | Check if a key exists. | |
| 62 | ||
| 63 | #### `timeouts.delete(key)` | |
| 64 | ||
| 65 | Remove a key. | |
| 66 | ||
| 67 | #### `timeouts.clear()` | |
| 68 | ||
| 69 | Remove all entries. | |
| 70 | ||
| 71 | #### `timeouts.size` | |
| 72 | ||
| 73 | Number of entries. | |
| 74 | ||
| 75 | ## How it works | |
| 76 | ||
| 77 | Uses TCP-style EWMA (exponentially weighted moving average) to track values: | |
| 78 | ||
| 79 | - `avg` blends 87.5% old + 12.5% new sample | |
| 80 | - `variance` blends 75% old + 25% new deviation | |
| 81 | ||
| 82 | Timeout for known keys: `(avg + 2 * variance) * attempt + jitter` | |
| 83 | ||
| 84 | Unknown keys use an aggressive fallback sequence to probe quickly. | |
| 85 | ||
| 86 | ## License | |
| 87 | ||
| 88 | Apache 2.0 |
| … | … | @@ -0,0 +1,58 @@ |
| 1 | declare module '@holepunchto/adaptive-timeout' { | |
| 2 | interface AdaptiveTimeoutOptions { | |
| 3 | maxSize?: number | |
| 4 | maxAge?: number | |
| 5 | fallback?: number[] | |
| 6 | min?: number | |
| 7 | max?: number | |
| 8 | jitter?: number | |
| 9 | } | |
| 10 | ||
| 11 | interface Stats { | |
| 12 | avg: number | |
| 13 | variance: number | |
| 14 | } | |
| 15 | ||
| 16 | declare class AdaptiveTimeout { | |
| 17 | constructor(opts?: AdaptiveTimeoutOptions) | |
| 18 | ||
| 19 | getValue(key: string): Stats | null | |
| 20 | put(key: string, value: number): Stats | |
| 21 | get(key: string, attempt?: number): number | |
| 22 | has(key: string): boolean | |
| 23 | delete(key: string): boolean | |
| 24 | clear(): void | |
| 25 | ||
| 26 | // Default - aggressive ramp | |
| 27 | static TimeoutAggressive: string[] | |
| 28 | ||
| 29 | // Linear - steady increase | |
| 30 | static TimeoutLinear: string[] | |
| 31 | ||
| 32 | // Exponential - slow start, rapid backoff | |
| 33 | static TimeoutExponential: string[] | |
| 34 | ||
| 35 | // Gentle - conservative, patient | |
| 36 | static TimeoutGentle: string[] | |
| 37 | ||
| 38 | // Fast - rapid fire retries | |
| 39 | static TimeoutFast: string[] | |
| 40 | ||
| 41 | // U-shape - long, short, long | |
| 42 | static TimeoutUShape: string[] | |
| 43 | ||
| 44 | // Inverse U - short, long, short | |
| 45 | static TimeoutInverseU: string[] | |
| 46 | ||
| 47 | // Sawtooth - alternating fast/slow | |
| 48 | static TimeoutSawtooth: string[] | |
| 49 | ||
| 50 | // Plateau - quick ramp then steady | |
| 51 | static TimeoutPlateau: string[] | |
| 52 | ||
| 53 | // Logarithmic - diminishing increases | |
| 54 | static TimeoutLogarithmic: string[] | |
| 55 | } | |
| 56 | ||
| 57 | export = AdaptiveTimeout | |
| 58 | } |
| … | … | @@ -0,0 +1,93 @@ |
| 1 | const Cache = require('xache') | |
| 2 | ||
| 3 | module.exports = class AdaptiveTimeout { | |
| 4 | constructor(opts = {}) { | |
| 5 | this._cache = new Cache({ | |
| 6 | maxSize: opts.maxSize || 65536, | |
| 7 | maxAge: opts.maxAge || 10 * 60 * 1000 // 10 minutes | |
| 8 | }) | |
| 9 | this._fallback = opts.fallback || AdaptiveTimeout.TimeoutExponential | |
| 10 | this._min = opts.min ?? 300 | |
| 11 | this._max = opts.max || 4000 | |
| 12 | this._jitter = opts.jitter ?? 256 | |
| 13 | } | |
| 14 | ||
| 15 | // Default - aggressive ramp | |
| 16 | static TimeoutAggressive = [500, 750, 1000, 1500, 2000] | |
| 17 | ||
| 18 | // Linear - steady increase | |
| 19 | static TimeoutLinear = [500, 1000, 1500, 2000, 2500] | |
| 20 | ||
| 21 | // Exponential - slow start, rapid backoff | |
| 22 | static TimeoutExponential = [250, 500, 1000, 2000, 4000] | |
| 23 | ||
| 24 | // Gentle - conservative, patient | |
| 25 | static TimeoutGentle = [1000, 1250, 1500, 1750, 2000] | |
| 26 | ||
| 27 | // Fast - rapid fire retries | |
| 28 | static TimeoutFast = [200, 400, 600, 800, 1000] | |
| 29 | ||
| 30 | // U-shape - long, short, long | |
| 31 | static TimeoutUShape = [1500, 750, 500, 750, 1500] | |
| 32 | ||
| 33 | // Inverse U - short, long, short | |
| 34 | static TimeoutInverseU = [500, 1000, 1500, 1000, 500] | |
| 35 | ||
| 36 | // Sawtooth - alternating fast/slow | |
| 37 | static TimeoutSawtooth = [500, 1500, 500, 1500, 500] | |
| 38 | ||
| 39 | // Plateau - quick ramp then steady | |
| 40 | static TimeoutPlateau = [500, 1000, 2000, 2000, 2000] | |
| 41 | ||
| 42 | // Logarithmic - diminishing increases | |
| 43 | static TimeoutLogarithmic = [500, 1000, 1300, 1500, 1600] | |
| 44 | ||
| 45 | getValue(key) { | |
| 46 | return this._cache.get(key) | |
| 47 | } | |
| 48 | ||
| 49 | put(key, value) { | |
| 50 | let p = this._cache.get(key) | |
| 51 | ||
| 52 | if (!p) { | |
| 53 | p = { avg: value, variance: value >> 1 } | |
| 54 | } else { | |
| 55 | // blend 75% old + 25% new deviation | |
| 56 | p.variance += (Math.abs(p.avg - value) - p.variance) >> 2 | |
| 57 | // blend 87.5% old + 12.5% new sample | |
| 58 | p.avg += (value - p.avg) >> 3 | |
| 59 | } | |
| 60 | ||
| 61 | this._cache.set(key, p) | |
| 62 | ||
| 63 | return p | |
| 64 | } | |
| 65 | ||
| 66 | get(key, attempt = 1) { | |
| 67 | const p = this._cache.get(key) | |
| 68 | const jitter = (Math.random() * this._jitter) | 0 | |
| 69 | ||
| 70 | if (p) { | |
| 71 | // known - use adaptive with linear backoff | |
| 72 | const base = p.avg + (p.variance << 1) | |
| 73 | const backoff = base * attempt | |
| 74 | return Math.min(Math.max(backoff + jitter, this._min), this._max) | |
| 75 | } else { | |
| 76 | // unknown - use aggressive fallback | |
| 77 | const base = this._fallback[Math.min(attempt - 1, this._fallback.length - 1)] | |
| 78 | return base + jitter | |
| 79 | } | |
| 80 | } | |
| 81 | ||
| 82 | has(key) { | |
| 83 | return this._cache.has(key) | |
| 84 | } | |
| 85 | ||
| 86 | delete(key) { | |
| 87 | return this._cache.delete(key) | |
| 88 | } | |
| 89 | ||
| 90 | clear() { | |
| 91 | this._cache.clear() | |
| 92 | } | |
| 93 | } |
| … | … | @@ -0,0 +1,42 @@ |
| 1 | { | |
| 2 | "name": "adaptive-timeout", | |
| 3 | "version": "1.0.1", | |
| 4 | "description": "adaptive-timeout", | |
| 5 | "main": "index.js", | |
| 6 | "exports": { | |
| 7 | "./package": "./package.json", | |
| 8 | ".": { | |
| 9 | "types": "./index.d.ts", | |
| 10 | "default": "./index.js" | |
| 11 | } | |
| 12 | }, | |
| 13 | "files": [ | |
| 14 | "package.json", | |
| 15 | "index.js", | |
| 16 | "index.d.ts" | |
| 17 | ], | |
| 18 | "devDependencies": { | |
| 19 | "brittle": "^3.19.0", | |
| 20 | "lunte": "^1.2.0", | |
| 21 | "prettier": "^3.6.2", | |
| 22 | "prettier-config-holepunch": "^2.0.0" | |
| 23 | }, | |
| 24 | "scripts": { | |
| 25 | "format": "prettier . --write", | |
| 26 | "test": "prettier . --check && lunte && brittle-bare test/index.js", | |
| 27 | "lint": "lunte" | |
| 28 | }, | |
| 29 | "repository": { | |
| 30 | "type": "git", | |
| 31 | "url": "https://github.com/holepunchto/adaptive-timeout.git" | |
| 32 | }, | |
| 33 | "author": "Holepunch Inc", | |
| 34 | "license": "Apache-2.0", | |
| 35 | "bugs": { | |
| 36 | "url": "https://github.com/holepunchto/adaptive-timeout/issues" | |
| 37 | }, | |
| 38 | "homepage": "https://github.com/holepunchto/adaptive-timeout", | |
| 39 | "dependencies": { | |
| 40 | "xache": "^1.2.1" | |
| 41 | } | |
| 42 | } |
| … | … | @@ -0,0 +1,201 @@ |
| 1 | Apache License | |
| 2 | Version 2.0, January 2004 | |
| 3 | http://www.apache.org/licenses/ | |
| 4 | ||
| 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | |
| 6 | ||
| 7 | 1. Definitions. | |
| 8 | ||
| 9 | "License" shall mean the terms and conditions for use, reproduction, | |
| 10 | and distribution as defined by Sections 1 through 9 of this document. | |
| 11 | ||
| 12 | "Licensor" shall mean the copyright owner or entity authorized by | |
| 13 | the copyright owner that is granting the License. | |
| 14 | ||
| 15 | "Legal Entity" shall mean the union of the acting entity and all | |
| 16 | other entities that control, are controlled by, or are under common | |
| 17 | control with that entity. For the purposes of this definition, | |
| 18 | "control" means (i) the power, direct or indirect, to cause the | |
| 19 | direction or management of such entity, whether by contract or | |
| 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the | |
| 21 | outstanding shares, or (iii) beneficial ownership of such entity. | |
| 22 | ||
| 23 | "You" (or "Your") shall mean an individual or Legal Entity | |
| 24 | exercising permissions granted by this License. | |
| 25 | ||
| 26 | "Source" form shall mean the preferred form for making modifications, | |
| 27 | including but not limited to software source code, documentation | |
| 28 | source, and configuration files. | |
| 29 | ||
| 30 | "Object" form shall mean any form resulting from mechanical | |
| 31 | transformation or translation of a Source form, including but | |
| 32 | not limited to compiled object code, generated documentation, | |
| 33 | and conversions to other media types. | |
| 34 | ||
| 35 | "Work" shall mean the work of authorship, whether in Source or | |
| 36 | Object form, made available under the License, as indicated by a | |
| 37 | copyright notice that is included in or attached to the work | |
| 38 | (an example is provided in the Appendix below). | |
| 39 | ||
| 40 | "Derivative Works" shall mean any work, whether in Source or Object | |
| 41 | form, that is based on (or derived from) the Work and for which the | |
| 42 | editorial revisions, annotations, elaborations, or other modifications | |
| 43 | represent, as a whole, an original work of authorship. For the purposes | |
| 44 | of this License, Derivative Works shall not include works that remain | |
| 45 | separable from, or merely link (or bind by name) to the interfaces of, | |
| 46 | the Work and Derivative Works thereof. | |
| 47 | ||
| 48 | "Contribution" shall mean any work of authorship, including | |
| 49 | the original version of the Work and any modifications or additions | |
| 50 | to that Work or Derivative Works thereof, that is intentionally | |
| 51 | submitted to Licensor for inclusion in the Work by the copyright owner | |
| 52 | or by an individual or Legal Entity authorized to submit on behalf of | |
| 53 | the copyright owner. For the purposes of this definition, "submitted" | |
| 54 | means any form of electronic, verbal, or written communication sent | |
| 55 | to the Licensor or its representatives, including but not limited to | |
| 56 | communication on electronic mailing lists, source code control systems, | |
| 57 | and issue tracking systems that are managed by, or on behalf of, the | |
| 58 | Licensor for the purpose of discussing and improving the Work, but | |
| 59 | excluding communication that is conspicuously marked or otherwise | |
| 60 | designated in writing by the copyright owner as "Not a Contribution." | |
| 61 | ||
| 62 | "Contributor" shall mean Licensor and any individual or Legal Entity | |
| 63 | on behalf of whom a Contribution has been received by Licensor and | |
| 64 | subsequently incorporated within the Work. | |
| 65 | ||
| 66 | 2. Grant of Copyright License. Subject to the terms and conditions of | |
| 67 | this License, each Contributor hereby grants to You a perpetual, | |
| 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 69 | copyright license to reproduce, prepare Derivative Works of, | |
| 70 | publicly display, publicly perform, sublicense, and distribute the | |
| 71 | Work and such Derivative Works in Source or Object form. | |
| 72 | ||
| 73 | 3. Grant of Patent License. Subject to the terms and conditions of | |
| 74 | this License, each Contributor hereby grants to You a perpetual, | |
| 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 76 | (except as stated in this section) patent license to make, have made, | |
| 77 | use, offer to sell, sell, import, and otherwise transfer the Work, | |
| 78 | where such license applies only to those patent claims licensable | |
| 79 | by such Contributor that are necessarily infringed by their | |
| 80 | Contribution(s) alone or by combination of their Contribution(s) | |
| 81 | with the Work to which such Contribution(s) was submitted. If You | |
| 82 | institute patent litigation against any entity (including a | |
| 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work | |
| 84 | or a Contribution incorporated within the Work constitutes direct | |
| 85 | or contributory patent infringement, then any patent licenses | |
| 86 | granted to You under this License for that Work shall terminate | |
| 87 | as of the date such litigation is filed. | |
| 88 | ||
| 89 | 4. Redistribution. You may reproduce and distribute copies of the | |
| 90 | Work or Derivative Works thereof in any medium, with or without | |
| 91 | modifications, and in Source or Object form, provided that You | |
| 92 | meet the following conditions: | |
| 93 | ||
| 94 | (a) You must give any other recipients of the Work or | |
| 95 | Derivative Works a copy of this License; and | |
| 96 | ||
| 97 | (b) You must cause any modified files to carry prominent notices | |
| 98 | stating that You changed the files; and | |
| 99 | ||
| 100 | (c) You must retain, in the Source form of any Derivative Works | |
| 101 | that You distribute, all copyright, patent, trademark, and | |
| 102 | attribution notices from the Source form of the Work, | |
| 103 | excluding those notices that do not pertain to any part of | |
| 104 | the Derivative Works; and | |
| 105 | ||
| 106 | (d) If the Work includes a "NOTICE" text file as part of its | |
| 107 | distribution, then any Derivative Works that You distribute must | |
| 108 | include a readable copy of the attribution notices contained | |
| 109 | within such NOTICE file, excluding those notices that do not | |
| 110 | pertain to any part of the Derivative Works, in at least one | |
| 111 | of the following places: within a NOTICE text file distributed | |
| 112 | as part of the Derivative Works; within the Source form or | |
| 113 | documentation, if provided along with the Derivative Works; or, | |
| 114 | within a display generated by the Derivative Works, if and | |
| 115 | wherever such third-party notices normally appear. The contents | |
| 116 | of the NOTICE file are for informational purposes only and | |
| 117 | do not modify the License. You may add Your own attribution | |
| 118 | notices within Derivative Works that You distribute, alongside | |
| 119 | or as an addendum to the NOTICE text from the Work, provided | |
| 120 | that such additional attribution notices cannot be construed | |
| 121 | as modifying the License. | |
| 122 | ||
| 123 | You may add Your own copyright statement to Your modifications and | |
| 124 | may provide additional or different license terms and conditions | |
| 125 | for use, reproduction, or distribution of Your modifications, or | |
| 126 | for any such Derivative Works as a whole, provided Your use, | |
| 127 | reproduction, and distribution of the Work otherwise complies with | |
| 128 | the conditions stated in this License. | |
| 129 | ||
| 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, | |
| 131 | any Contribution intentionally submitted for inclusion in the Work | |
| 132 | by You to the Licensor shall be under the terms and conditions of | |
| 133 | this License, without any additional terms or conditions. | |
| 134 | Notwithstanding the above, nothing herein shall supersede or modify | |
| 135 | the terms of any separate license agreement you may have executed | |
| 136 | with Licensor regarding such Contributions. | |
| 137 | ||
| 138 | 6. Trademarks. This License does not grant permission to use the trade | |
| 139 | names, trademarks, service marks, or product names of the Licensor, | |
| 140 | except as required for reasonable and customary use in describing the | |
| 141 | origin of the Work and reproducing the content of the NOTICE file. | |
| 142 | ||
| 143 | 7. Disclaimer of Warranty. Unless required by applicable law or | |
| 144 | agreed to in writing, Licensor provides the Work (and each | |
| 145 | Contributor provides its Contributions) on an "AS IS" BASIS, | |
| 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |
| 147 | implied, including, without limitation, any warranties or conditions | |
| 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | |
| 149 | PARTICULAR PURPOSE. You are solely responsible for determining the | |
| 150 | appropriateness of using or redistributing the Work and assume any | |
| 151 | risks associated with Your exercise of permissions under this License. | |
| 152 | ||
| 153 | 8. Limitation of Liability. In no event and under no legal theory, | |
| 154 | whether in tort (including negligence), contract, or otherwise, | |
| 155 | unless required by applicable law (such as deliberate and grossly | |
| 156 | negligent acts) or agreed to in writing, shall any Contributor be | |
| 157 | liable to You for damages, including any direct, indirect, special, | |
| 158 | incidental, or consequential damages of any character arising as a | |
| 159 | result of this License or out of the use or inability to use the | |
| 160 | Work (including but not limited to damages for loss of goodwill, | |
| 161 | work stoppage, computer failure or malfunction, or any and all | |
| 162 | other commercial damages or losses), even if such Contributor | |
| 163 | has been advised of the possibility of such damages. | |
| 164 | ||
| 165 | 9. Accepting Warranty or Additional Liability. While redistributing | |
| 166 | the Work or Derivative Works thereof, You may choose to offer, | |
| 167 | and charge a fee for, acceptance of support, warranty, indemnity, | |
| 168 | or other liability obligations and/or rights consistent with this | |
| 169 | License. However, in accepting such obligations, You may act only | |
| 170 | on Your own behalf and on Your sole responsibility, not on behalf | |
| 171 | of any other Contributor, and only if You agree to indemnify, | |
| 172 | defend, and hold each Contributor harmless for any liability | |
| 173 | incurred by, or claims asserted against, such Contributor by reason | |
| 174 | of your accepting any such warranty or additional liability. | |
| 175 | ||
| 176 | END OF TERMS AND CONDITIONS | |
| 177 | ||
| 178 | APPENDIX: How to apply the Apache License to your work. | |
| 179 | ||
| 180 | To apply the Apache License to your work, attach the following | |
| 181 | boilerplate notice, with the fields enclosed by brackets "[]" | |
| 182 | replaced with your own identifying information. (Don't include | |
| 183 | the brackets!) The text should be enclosed in the appropriate | |
| 184 | comment syntax for the file format. We also recommend that a | |
| 185 | file or class name and description of purpose be included on the | |
| 186 | same "printed page" as the copyright notice for easier | |
| 187 | identification within third-party archives. | |
| 188 | ||
| 189 | Copyright [yyyy] [name of copyright owner] | |
| 190 | ||
| 191 | Licensed under the Apache License, Version 2.0 (the "License"); | |
| 192 | you may not use this file except in compliance with the License. | |
| 193 | You may obtain a copy of the License at | |
| 194 | ||
| 195 | http://www.apache.org/licenses/LICENSE-2.0 | |
| 196 | ||
| 197 | Unless required by applicable law or agreed to in writing, software | |
| 198 | distributed under the License is distributed on an "AS IS" BASIS, | |
| 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 200 | See the License for the specific language governing permissions and | |
| 201 | limitations under the License. |
| … | … | @@ -0,0 +1,13 @@ |
| 1 | Copyright 2023 Holepunch Inc | |
| 2 | ||
| 3 | Licensed under the Apache License, Version 2.0 (the "License"); | |
| 4 | you may not use this file except in compliance with the License. | |
| 5 | You may obtain a copy of the License at | |
| 6 | ||
| 7 | http://www.apache.org/licenses/LICENSE-2.0 | |
| 8 | ||
| 9 | Unless required by applicable law or agreed to in writing, software | |
| 10 | distributed under the License is distributed on an "AS IS" BASIS, | |
| 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 12 | See the License for the specific language governing permissions and | |
| 13 | limitations under the License. |
| … | … | @@ -0,0 +1,19 @@ |
| 1 | # autobase-test-helpers | |
| 2 | ||
| 3 | Helpers when writing tests for an Autobased application | |
| 4 | ||
| 5 | ``` | |
| 6 | npm install autobase-test-helpers | |
| 7 | ``` | |
| 8 | ||
| 9 | ## Usage | |
| 10 | ||
| 11 | ``` js | |
| 12 | const { sync } = require('autobase-test-helpers') | |
| 13 | ||
| 14 | await sync(bases) // expects replication to have been set up | |
| 15 | ``` | |
| 16 | ||
| 17 | ## License | |
| 18 | ||
| 19 | MIT |
| … | … | @@ -0,0 +1,149 @@ |
| 1 | const b4a = require('b4a') | |
| 2 | ||
| 3 | module.exports = { | |
| 4 | eventFlush, | |
| 5 | replicate, | |
| 6 | sync, | |
| 7 | synced, | |
| 8 | replicateAndSync | |
| 9 | } | |
| 10 | ||
| 11 | function eventFlush () { | |
| 12 | return new Promise(resolve => setImmediate(resolve)) | |
| 13 | } | |
| 14 | ||
| 15 | async function replicateAndSync (bases, opts) { | |
| 16 | const done = replicate(bases) | |
| 17 | await sync(bases, opts) | |
| 18 | await done() | |
| 19 | } | |
| 20 | ||
| 21 | async function sync (bases, { checkHash = true } = {}) { | |
| 22 | for (const base of bases) { | |
| 23 | await base.update() | |
| 24 | } | |
| 25 | ||
| 26 | if (bases.length === 1) return | |
| 27 | ||
| 28 | return new Promise((resolve, reject) => { | |
| 29 | let done = false | |
| 30 | let checks = 0 | |
| 31 | ||
| 32 | for (const base of bases) { | |
| 33 | base.on('update', check) | |
| 34 | base.on('error', shutdown) | |
| 35 | } | |
| 36 | ||
| 37 | check() | |
| 38 | ||
| 39 | async function check () { | |
| 40 | checks++ | |
| 41 | for (const base of bases) { | |
| 42 | if (base.interrupted) return shutdown(new Error('base was interrupted, reason at base.interrupted')) | |
| 43 | } | |
| 44 | if (!(await same(checkHash))) return mbShutdown() | |
| 45 | for (const base of bases) { | |
| 46 | await base.update() | |
| 47 | } | |
| 48 | if (!(await same(checkHash))) return mbShutdown() | |
| 49 | shutdown() | |
| 50 | } | |
| 51 | ||
| 52 | function mbShutdown () { | |
| 53 | if (done) return shutdown() | |
| 54 | checks-- | |
| 55 | } | |
| 56 | ||
| 57 | async function same (checkHash) { | |
| 58 | return synced(bases) && await sameHash(bases, checkHash) | |
| 59 | } | |
| 60 | ||
| 61 | function shutdown (err) { | |
| 62 | checks-- | |
| 63 | done = true | |
| 64 | if (checks !== 0 && !err) return | |
| 65 | ||
| 66 | for (const base of bases) { | |
| 67 | base.off('update', check) | |
| 68 | base.off('error', shutdown) | |
| 69 | } | |
| 70 | ||
| 71 | if (err) reject(err) | |
| 72 | else resolve() | |
| 73 | } | |
| 74 | }) | |
| 75 | } | |
| 76 | ||
| 77 | async function sameHash (bases, check) { | |
| 78 | if (!check) return true | |
| 79 | const first = bases[0] | |
| 80 | const h1 = await first.hash() | |
| 81 | for (let i = 1; i < bases.length; i++) { | |
| 82 | if (bases[i].signedLength !== first.signedLength) return false | |
| 83 | const h2 = await bases[i].hash() | |
| 84 | if (!b4a.equals(h1, h2)) return false | |
| 85 | } | |
| 86 | return true | |
| 87 | } | |
| 88 | ||
| 89 | function synced (bases) { | |
| 90 | const first = bases[0] | |
| 91 | for (let i = 1; i < bases.length; i++) { | |
| 92 | if (!same(first, bases[i])) return false | |
| 93 | } | |
| 94 | return true | |
| 95 | } | |
| 96 | ||
| 97 | function same (a, b) { | |
| 98 | if (a.updating || b.updating) return false | |
| 99 | ||
| 100 | const h1 = a.heads() | |
| 101 | const h2 = b.heads() | |
| 102 | ||
| 103 | if (h1.length !== h2.length) return false | |
| 104 | ||
| 105 | for (let i = 0; i < h1.length; i++) { | |
| 106 | const h1i = h1[i] | |
| 107 | const h2i = h2[i] | |
| 108 | ||
| 109 | if (!b4a.equals(h1i.key, h2i.key)) return false | |
| 110 | if (h1i.length !== h2i.length) return false | |
| 111 | } | |
| 112 | ||
| 113 | return true | |
| 114 | } | |
| 115 | ||
| 116 | function replicate (bases) { | |
| 117 | const streams = [] | |
| 118 | const missing = bases.slice() | |
| 119 | ||
| 120 | while (missing.length) { | |
| 121 | const a = missing.pop() | |
| 122 | ||
| 123 | for (const b of missing) { | |
| 124 | const s1 = a.replicate(true) | |
| 125 | const s2 = b.replicate(false) | |
| 126 | ||
| 127 | s1.on('error', (err) => { | |
| 128 | if (err.code) console.log('autobase replicate error:', err.stack) | |
| 129 | }) | |
| 130 | s2.on('error', (err) => { | |
| 131 | if (err.code) console.log('autobase replicate error:', err.stack) | |
| 132 | }) | |
| 133 | ||
| 134 | s1.pipe(s2).pipe(s1) | |
| 135 | ||
| 136 | streams.push(s1) | |
| 137 | streams.push(s2) | |
| 138 | } | |
| 139 | } | |
| 140 | ||
| 141 | return close | |
| 142 | ||
| 143 | function close () { | |
| 144 | return Promise.all(streams.map(s => { | |
| 145 | s.destroy() | |
| 146 | return new Promise(resolve => s.on('close', resolve)) | |
| 147 | })) | |
| 148 | } | |
| 149 | } |
| … | … | @@ -0,0 +1,19 @@ |
| 1 | { | |
| 2 | "name": "autobase-test-helpers", | |
| 3 | "version": "3.1.0", | |
| 4 | "description": "Helpers when writing tests for an Autobased application", | |
| 5 | "main": "index.js", | |
| 6 | "dependencies": { | |
| 7 | "b4a": "^1.6.4" | |
| 8 | }, | |
| 9 | "repository": { | |
| 10 | "type": "git", | |
| 11 | "url": "git+https://github.com/holepunchto/autobase-test-helpers.git" | |
| 12 | }, | |
| 13 | "author": "Holepunch", | |
| 14 | "license": "Apache-2.0", | |
| 15 | "bugs": { | |
| 16 | "url": "https://github.com/holepunchto/autobase-test-helpers/issues" | |
| 17 | }, | |
| 18 | "homepage": "https://github.com/holepunchto/autobase-test-helpers" | |
| 19 | } |
| … | … | @@ -0,0 +1,201 @@ |
| 1 | Apache License | |
| 2 | Version 2.0, January 2004 | |
| 3 | http://www.apache.org/licenses/ | |
| 4 | ||
| 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | |
| 6 | ||
| 7 | 1. Definitions. | |
| 8 | ||
| 9 | "License" shall mean the terms and conditions for use, reproduction, | |
| 10 | and distribution as defined by Sections 1 through 9 of this document. | |
| 11 | ||
| 12 | "Licensor" shall mean the copyright owner or entity authorized by | |
| 13 | the copyright owner that is granting the License. | |
| 14 | ||
| 15 | "Legal Entity" shall mean the union of the acting entity and all | |
| 16 | other entities that control, are controlled by, or are under common | |
| 17 | control with that entity. For the purposes of this definition, | |
| 18 | "control" means (i) the power, direct or indirect, to cause the | |
| 19 | direction or management of such entity, whether by contract or | |
| 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the | |
| 21 | outstanding shares, or (iii) beneficial ownership of such entity. | |
| 22 | ||
| 23 | "You" (or "Your") shall mean an individual or Legal Entity | |
| 24 | exercising permissions granted by this License. | |
| 25 | ||
| 26 | "Source" form shall mean the preferred form for making modifications, | |
| 27 | including but not limited to software source code, documentation | |
| 28 | source, and configuration files. | |
| 29 | ||
| 30 | "Object" form shall mean any form resulting from mechanical | |
| 31 | transformation or translation of a Source form, including but | |
| 32 | not limited to compiled object code, generated documentation, | |
| 33 | and conversions to other media types. | |
| 34 | ||
| 35 | "Work" shall mean the work of authorship, whether in Source or | |
| 36 | Object form, made available under the License, as indicated by a | |
| 37 | copyright notice that is included in or attached to the work | |
| 38 | (an example is provided in the Appendix below). | |
| 39 | ||
| 40 | "Derivative Works" shall mean any work, whether in Source or Object | |
| 41 | form, that is based on (or derived from) the Work and for which the | |
| 42 | editorial revisions, annotations, elaborations, or other modifications | |
| 43 | represent, as a whole, an original work of authorship. For the purposes | |
| 44 | of this License, Derivative Works shall not include works that remain | |
| 45 | separable from, or merely link (or bind by name) to the interfaces of, | |
| 46 | the Work and Derivative Works thereof. | |
| 47 | ||
| 48 | "Contribution" shall mean any work of authorship, including | |
| 49 | the original version of the Work and any modifications or additions | |
| 50 | to that Work or Derivative Works thereof, that is intentionally | |
| 51 | submitted to Licensor for inclusion in the Work by the copyright owner | |
| 52 | or by an individual or Legal Entity authorized to submit on behalf of | |
| 53 | the copyright owner. For the purposes of this definition, "submitted" | |
| 54 | means any form of electronic, verbal, or written communication sent | |
| 55 | to the Licensor or its representatives, including but not limited to | |
| 56 | communication on electronic mailing lists, source code control systems, | |
| 57 | and issue tracking systems that are managed by, or on behalf of, the | |
| 58 | Licensor for the purpose of discussing and improving the Work, but | |
| 59 | excluding communication that is conspicuously marked or otherwise | |
| 60 | designated in writing by the copyright owner as "Not a Contribution." | |
| 61 | ||
| 62 | "Contributor" shall mean Licensor and any individual or Legal Entity | |
| 63 | on behalf of whom a Contribution has been received by Licensor and | |
| 64 | subsequently incorporated within the Work. | |
| 65 | ||
| 66 | 2. Grant of Copyright License. Subject to the terms and conditions of | |
| 67 | this License, each Contributor hereby grants to You a perpetual, | |
| 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 69 | copyright license to reproduce, prepare Derivative Works of, | |
| 70 | publicly display, publicly perform, sublicense, and distribute the | |
| 71 | Work and such Derivative Works in Source or Object form. | |
| 72 | ||
| 73 | 3. Grant of Patent License. Subject to the terms and conditions of | |
| 74 | this License, each Contributor hereby grants to You a perpetual, | |
| 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 76 | (except as stated in this section) patent license to make, have made, | |
| 77 | use, offer to sell, sell, import, and otherwise transfer the Work, | |
| 78 | where such license applies only to those patent claims licensable | |
| 79 | by such Contributor that are necessarily infringed by their | |
| 80 | Contribution(s) alone or by combination of their Contribution(s) | |
| 81 | with the Work to which such Contribution(s) was submitted. If You | |
| 82 | institute patent litigation against any entity (including a | |
| 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work | |
| 84 | or a Contribution incorporated within the Work constitutes direct | |
| 85 | or contributory patent infringement, then any patent licenses | |
| 86 | granted to You under this License for that Work shall terminate | |
| 87 | as of the date such litigation is filed. | |
| 88 | ||
| 89 | 4. Redistribution. You may reproduce and distribute copies of the | |
| 90 | Work or Derivative Works thereof in any medium, with or without | |
| 91 | modifications, and in Source or Object form, provided that You | |
| 92 | meet the following conditions: | |
| 93 | ||
| 94 | (a) You must give any other recipients of the Work or | |
| 95 | Derivative Works a copy of this License; and | |
| 96 | ||
| 97 | (b) You must cause any modified files to carry prominent notices | |
| 98 | stating that You changed the files; and | |
| 99 | ||
| 100 | (c) You must retain, in the Source form of any Derivative Works | |
| 101 | that You distribute, all copyright, patent, trademark, and | |
| 102 | attribution notices from the Source form of the Work, | |
| 103 | excluding those notices that do not pertain to any part of | |
| 104 | the Derivative Works; and | |
| 105 | ||
| 106 | (d) If the Work includes a "NOTICE" text file as part of its | |
| 107 | distribution, then any Derivative Works that You distribute must | |
| 108 | include a readable copy of the attribution notices contained | |
| 109 | within such NOTICE file, excluding those notices that do not | |
| 110 | pertain to any part of the Derivative Works, in at least one | |
| 111 | of the following places: within a NOTICE text file distributed | |
| 112 | as part of the Derivative Works; within the Source form or | |
| 113 | documentation, if provided along with the Derivative Works; or, | |
| 114 | within a display generated by the Derivative Works, if and | |
| 115 | wherever such third-party notices normally appear. The contents | |
| 116 | of the NOTICE file are for informational purposes only and | |
| 117 | do not modify the License. You may add Your own attribution | |
| 118 | notices within Derivative Works that You distribute, alongside | |
| 119 | or as an addendum to the NOTICE text from the Work, provided | |
| 120 | that such additional attribution notices cannot be construed | |
| 121 | as modifying the License. | |
| 122 | ||
| 123 | You may add Your own copyright statement to Your modifications and | |
| 124 | may provide additional or different license terms and conditions | |
| 125 | for use, reproduction, or distribution of Your modifications, or | |
| 126 | for any such Derivative Works as a whole, provided Your use, | |
| 127 | reproduction, and distribution of the Work otherwise complies with | |
| 128 | the conditions stated in this License. | |
| 129 | ||
| 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, | |
| 131 | any Contribution intentionally submitted for inclusion in the Work | |
| 132 | by You to the Licensor shall be under the terms and conditions of | |
| 133 | this License, without any additional terms or conditions. | |
| 134 | Notwithstanding the above, nothing herein shall supersede or modify | |
| 135 | the terms of any separate license agreement you may have executed | |
| 136 | with Licensor regarding such Contributions. | |
| 137 | ||
| 138 | 6. Trademarks. This License does not grant permission to use the trade | |
| 139 | names, trademarks, service marks, or product names of the Licensor, | |
| 140 | except as required for reasonable and customary use in describing the | |
| 141 | origin of the Work and reproducing the content of the NOTICE file. | |
| 142 | ||
| 143 | 7. Disclaimer of Warranty. Unless required by applicable law or | |
| 144 | agreed to in writing, Licensor provides the Work (and each | |
| 145 | Contributor provides its Contributions) on an "AS IS" BASIS, | |
| 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |
| 147 | implied, including, without limitation, any warranties or conditions | |
| 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | |
| 149 | PARTICULAR PURPOSE. You are solely responsible for determining the | |
| 150 | appropriateness of using or redistributing the Work and assume any | |
| 151 | risks associated with Your exercise of permissions under this License. | |
| 152 | ||
| 153 | 8. Limitation of Liability. In no event and under no legal theory, | |
| 154 | whether in tort (including negligence), contract, or otherwise, | |
| 155 | unless required by applicable law (such as deliberate and grossly | |
| 156 | negligent acts) or agreed to in writing, shall any Contributor be | |
| 157 | liable to You for damages, including any direct, indirect, special, | |
| 158 | incidental, or consequential damages of any character arising as a | |
| 159 | result of this License or out of the use or inability to use the | |
| 160 | Work (including but not limited to damages for loss of goodwill, | |
| 161 | work stoppage, computer failure or malfunction, or any and all | |
| 162 | other commercial damages or losses), even if such Contributor | |
| 163 | has been advised of the possibility of such damages. | |
| 164 | ||
| 165 | 9. Accepting Warranty or Additional Liability. While redistributing | |
| 166 | the Work or Derivative Works thereof, You may choose to offer, | |
| 167 | and charge a fee for, acceptance of support, warranty, indemnity, | |
| 168 | or other liability obligations and/or rights consistent with this | |
| 169 | License. However, in accepting such obligations, You may act only | |
| 170 | on Your own behalf and on Your sole responsibility, not on behalf | |
| 171 | of any other Contributor, and only if You agree to indemnify, | |
| 172 | defend, and hold each Contributor harmless for any liability | |
| 173 | incurred by, or claims asserted against, such Contributor by reason | |
| 174 | of your accepting any such warranty or additional liability. | |
| 175 | ||
| 176 | END OF TERMS AND CONDITIONS | |
| 177 | ||
| 178 | APPENDIX: How to apply the Apache License to your work. | |
| 179 | ||
| 180 | To apply the Apache License to your work, attach the following | |
| 181 | boilerplate notice, with the fields enclosed by brackets "[]" | |
| 182 | replaced with your own identifying information. (Don't include | |
| 183 | the brackets!) The text should be enclosed in the appropriate | |
| 184 | comment syntax for the file format. We also recommend that a | |
| 185 | file or class name and description of purpose be included on the | |
| 186 | same "printed page" as the copyright notice for easier | |
| 187 | identification within third-party archives. | |
| 188 | ||
| 189 | Copyright [yyyy] [name of copyright owner] | |
| 190 | ||
| 191 | Licensed under the Apache License, Version 2.0 (the "License"); | |
| 192 | you may not use this file except in compliance with the License. | |
| 193 | You may obtain a copy of the License at | |
| 194 | ||
| 195 | http://www.apache.org/licenses/LICENSE-2.0 | |
| 196 | ||
| 197 | Unless required by applicable law or agreed to in writing, software | |
| 198 | distributed under the License is distributed on an "AS IS" BASIS, | |
| 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 200 | See the License for the specific language governing permissions and | |
| 201 | limitations under the License. |
| … | … | @@ -0,0 +1,47 @@ |
| 1 | # bare-bundle | |
| 2 | ||
| 3 | Application bundle format for JavaScript, inspired by <https://github.com/electron/asar>. | |
| 4 | ||
| 5 | ``` | |
| 6 | npm i bare-bundle | |
| 7 | ``` | |
| 8 | ||
| 9 | ## Format | |
| 10 | ||
| 11 | ``` | |
| 12 | [#!hashbang] | |
| 13 | <header length><header><...files> | |
| 14 | ``` | |
| 15 | ||
| 16 | The header length is an integer literal denoting the total length of the header. The header itself is a JSON string literal of header length bytes and has the following format: | |
| 17 | ||
| 18 | ```js | |
| 19 | { | |
| 20 | "version": 0, | |
| 21 | "id": null | "<string>", | |
| 22 | "main": null | "<url>", | |
| 23 | "imports": { | |
| 24 | "<from>": "<to>" | |
| 25 | }, | |
| 26 | "resolutions": { | |
| 27 | "<url>": "<imports>" | |
| 28 | }, | |
| 29 | "addons": ["<url>"], | |
| 30 | "assets": ["<url>"], | |
| 31 | "files": { | |
| 32 | "<url>": { | |
| 33 | "offset": number, | |
| 34 | "length": number, | |
| 35 | "mode": number | |
| 36 | } | |
| 37 | } | |
| 38 | } | |
| 39 | ``` | |
| 40 | ||
| 41 | For each `<url>` in `files`, `offset` provides the byte offset to the file **after** the header and `length` provides the byte length of the file. | |
| 42 | ||
| 43 | The bundle may optionally be prefixed with a hashbang, `#!`, for use with script interpreters. The hashbang is ignored during parsing. | |
| 44 | ||
| 45 | ## License | |
| 46 | ||
| 47 | Apache-2.0 |
| … | … | @@ -0,0 +1,86 @@ |
| 1 | import Buffer from 'bare-buffer' | |
| 2 | import URL from 'bare-url' | |
| 3 | ||
| 4 | interface MemoryFileOptions { | |
| 5 | executable?: boolean | |
| 6 | mode?: number | |
| 7 | } | |
| 8 | ||
| 9 | interface MemoryFile { | |
| 10 | mode(): number | |
| 11 | read(): Buffer | |
| 12 | size(): number | |
| 13 | } | |
| 14 | ||
| 15 | declare class MemoryFile { | |
| 16 | constructor(data: string | Buffer, opts?: MemoryFileOptions) | |
| 17 | } | |
| 18 | ||
| 19 | interface BundleOptions { | |
| 20 | File?: typeof MemoryFile | |
| 21 | } | |
| 22 | ||
| 23 | type RecursiveStringObject = { [key: string]: string | RecursiveStringObject } | |
| 24 | ||
| 25 | interface BundleWriteOptions { | |
| 26 | addon?: boolean | |
| 27 | alias?: string | |
| 28 | asset?: boolean | |
| 29 | executable?: boolean | |
| 30 | imports?: RecursiveStringObject | |
| 31 | main?: boolean | |
| 32 | mode?: number | |
| 33 | } | |
| 34 | ||
| 35 | interface BundleMountOptions { | |
| 36 | conditions?: { [condition: string]: string | URL } | |
| 37 | } | |
| 38 | ||
| 39 | interface BundleToBufferOptions { | |
| 40 | indent?: number | |
| 41 | shared?: boolean | |
| 42 | } | |
| 43 | ||
| 44 | interface Bundle extends Iterable<[key: string, read: Buffer, mode: number]> { | |
| 45 | readonly files: Record<string, MemoryFile> | |
| 46 | readonly version: number | |
| 47 | ||
| 48 | addons: string[] | |
| 49 | assets: string[] | |
| 50 | id: string | null | |
| 51 | imports: RecursiveStringObject | |
| 52 | main: string | null | |
| 53 | resolutions: RecursiveStringObject | |
| 54 | ||
| 55 | empty(): boolean | |
| 56 | keys(): string[] | |
| 57 | exists(key: string): boolean | |
| 58 | mode(key: string): number | |
| 59 | read(key: string): Buffer | |
| 60 | write(key: string, data: string, opts?: BundleWriteOptions): this | |
| 61 | mount(root: string | URL, opts?: BundleMountOptions): Bundle | |
| 62 | unmount(root: string | URL, opts?: BundleMountOptions): Bundle | |
| 63 | toBuffer(opts?: BundleToBufferOptions): Buffer | |
| 64 | } | |
| 65 | ||
| 66 | declare class Bundle { | |
| 67 | static readonly version: number | |
| 68 | ||
| 69 | constructor(opts?: BundleOptions) | |
| 70 | } | |
| 71 | ||
| 72 | declare namespace Bundle { | |
| 73 | export { | |
| 74 | type MemoryFile, | |
| 75 | type MemoryFileOptions, | |
| 76 | type BundleOptions, | |
| 77 | type BundleWriteOptions, | |
| 78 | type BundleMountOptions, | |
| 79 | type BundleToBufferOptions | |
| 80 | } | |
| 81 | ||
| 82 | export function isBundle(value: unknown): value is Bundle | |
| 83 | export function from(value: string | Buffer | Bundle): Bundle | |
| 84 | } | |
| 85 | ||
| 86 | export = Bundle |
| … | … | @@ -0,0 +1,592 @@ |
| 1 | const errors = require('./lib/errors') | |
| 2 | ||
| 3 | const kind = Symbol.for('bare.bundle.kind') | |
| 4 | ||
| 5 | class MemoryFile { | |
| 6 | constructor(data, opts = {}) { | |
| 7 | const { executable = false, mode = executable ? 0o755 : 0o644 } = opts | |
| 8 | ||
| 9 | this._data = typeof data === 'string' ? Buffer.from(data) : data | |
| 10 | this._mode = mode | |
| 11 | } | |
| 12 | ||
| 13 | size() { | |
| 14 | return this._data.byteLength | |
| 15 | } | |
| 16 | ||
| 17 | mode() { | |
| 18 | return this._mode | |
| 19 | } | |
| 20 | ||
| 21 | read() { | |
| 22 | return this._data | |
| 23 | } | |
| 24 | ||
| 25 | inspect() { | |
| 26 | return { | |
| 27 | __proto__: { constructor: MemoryFile }, | |
| 28 | ||
| 29 | data: this._data, | |
| 30 | mode: this._mode.toString(8) | |
| 31 | } | |
| 32 | } | |
| 33 | ||
| 34 | [Symbol.for('bare.inspect')]() { | |
| 35 | return this.inspect() | |
| 36 | } | |
| 37 | ||
| 38 | [Symbol.for('nodejs.util.inspect.custom')]() { | |
| 39 | return this.inspect() | |
| 40 | } | |
| 41 | } | |
| 42 | ||
| 43 | module.exports = exports = class Bundle { | |
| 44 | static get [kind]() { | |
| 45 | return 0 // Compatibility version | |
| 46 | } | |
| 47 | ||
| 48 | static get version() { | |
| 49 | return 0 | |
| 50 | } | |
| 51 | ||
| 52 | constructor(opts = {}) { | |
| 53 | const { File = MemoryFile } = opts | |
| 54 | ||
| 55 | this._File = File | |
| 56 | this._id = null | |
| 57 | this._main = null | |
| 58 | this._imports = {} | |
| 59 | this._resolutions = {} | |
| 60 | this._addons = [] | |
| 61 | this._assets = [] | |
| 62 | this._files = new Map() | |
| 63 | } | |
| 64 | ||
| 65 | get [kind]() { | |
| 66 | return Bundle[kind] | |
| 67 | } | |
| 68 | ||
| 69 | get version() { | |
| 70 | return Bundle.version | |
| 71 | } | |
| 72 | ||
| 73 | get id() { | |
| 74 | return this._id | |
| 75 | } | |
| 76 | ||
| 77 | set id(value) { | |
| 78 | if (typeof value !== 'string' && value !== null) { | |
| 79 | throw new TypeError(`ID must be a string or null. Received type ${typeof value} (${value})`) | |
| 80 | } | |
| 81 | ||
| 82 | this._id = value | |
| 83 | } | |
| 84 | ||
| 85 | get main() { | |
| 86 | return this._main | |
| 87 | } | |
| 88 | ||
| 89 | set main(value) { | |
| 90 | if (typeof value !== 'string' && value !== null) { | |
| 91 | throw new TypeError(`Main must be a string or null. Received type ${typeof value} (${value})`) | |
| 92 | } | |
| 93 | ||
| 94 | this._main = value | |
| 95 | } | |
| 96 | ||
| 97 | get imports() { | |
| 98 | return this._imports | |
| 99 | } | |
| 100 | ||
| 101 | set imports(value) { | |
| 102 | this._imports = cloneImportsMap(value) | |
| 103 | } | |
| 104 | ||
| 105 | get resolutions() { | |
| 106 | return this._resolutions | |
| 107 | } | |
| 108 | ||
| 109 | set resolutions(value) { | |
| 110 | this._resolutions = cloneResolutionsMap(value) | |
| 111 | } | |
| 112 | ||
| 113 | get addons() { | |
| 114 | return this._addons | |
| 115 | } | |
| 116 | ||
| 117 | set addons(value) { | |
| 118 | this._addons = cloneFilesList(value, 'Addons') | |
| 119 | } | |
| 120 | ||
| 121 | get assets() { | |
| 122 | return this._assets | |
| 123 | } | |
| 124 | ||
| 125 | set assets(value) { | |
| 126 | this._assets = cloneFilesList(value, 'Assets') | |
| 127 | } | |
| 128 | ||
| 129 | get files() { | |
| 130 | return Object.fromEntries(this._files.entries()) | |
| 131 | } | |
| 132 | ||
| 133 | *[Symbol.iterator]() { | |
| 134 | for (const [key, file] of this._files) { | |
| 135 | yield [key, file.read(), file.mode()] | |
| 136 | } | |
| 137 | } | |
| 138 | ||
| 139 | empty() { | |
| 140 | return this._files.size === 0 | |
| 141 | } | |
| 142 | ||
| 143 | keys() { | |
| 144 | return this._files.keys() | |
| 145 | } | |
| 146 | ||
| 147 | exists(key) { | |
| 148 | return this._files.has(key) | |
| 149 | } | |
| 150 | ||
| 151 | size(key) { | |
| 152 | const file = this._files.get(key) || null | |
| 153 | if (file === null) return 0 | |
| 154 | return file.size() | |
| 155 | } | |
| 156 | ||
| 157 | mode(key) { | |
| 158 | const file = this._files.get(key) || null | |
| 159 | if (file === null) return 0 | |
| 160 | return file.mode() | |
| 161 | } | |
| 162 | ||
| 163 | read(key) { | |
| 164 | const file = this._files.get(key) || null | |
| 165 | if (file === null) return null | |
| 166 | return file.read() | |
| 167 | } | |
| 168 | ||
| 169 | write(key, data, opts = {}) { | |
| 170 | if (typeof key !== 'string') { | |
| 171 | throw new TypeError(`File path must be a string. Received type ${typeof key} (${key})`) | |
| 172 | } | |
| 173 | ||
| 174 | const { main = false, alias = null, imports = null, addon = false, asset = false } = opts | |
| 175 | ||
| 176 | this._files.set(key, new MemoryFile(data, opts)) | |
| 177 | ||
| 178 | if (main) this._main = key | |
| 179 | if (alias) this._imports[alias] = key | |
| 180 | if (imports) this._resolutions[key] = cloneImportsMap(imports) | |
| 181 | if (addon) this._addons.push(key) | |
| 182 | if (asset) this._assets.push(key) | |
| 183 | ||
| 184 | return this | |
| 185 | } | |
| 186 | ||
| 187 | mount(root, opts = {}) { | |
| 188 | const bundle = new Bundle() | |
| 189 | ||
| 190 | // Go through the private API properties as we're operating on already | |
| 191 | // validated values. | |
| 192 | ||
| 193 | bundle._File = this._File | |
| 194 | bundle._id = this._id | |
| 195 | ||
| 196 | if (this._main) bundle._main = mountSpecifier(this._main, root) | |
| 197 | ||
| 198 | bundle._imports = transformImportsMap(this._imports, root, null, opts, mountSpecifier) | |
| 199 | bundle._resolutions = transformResolutionsMap(this._resolutions, root, opts, mountSpecifier) | |
| 200 | ||
| 201 | for (const [key, file] of this._files) { | |
| 202 | bundle._files.set(mountSpecifier(key, root), file) | |
| 203 | } | |
| 204 | ||
| 205 | bundle._addons = transformFilesList(this._addons, root, mountSpecifier) | |
| 206 | bundle._assets = transformFilesList(this._assets, root, mountSpecifier) | |
| 207 | ||
| 208 | return bundle | |
| 209 | } | |
| 210 | ||
| 211 | unmount(root, opts = {}) { | |
| 212 | const bundle = new Bundle() | |
| 213 | ||
| 214 | // Go through the private API properties as we're operating on already | |
| 215 | // validated values. | |
| 216 | ||
| 217 | bundle._File = this._File | |
| 218 | bundle._id = this._id | |
| 219 | ||
| 220 | if (this._main) bundle._main = unmountSpecifier(this._main, root) | |
| 221 | ||
| 222 | bundle._imports = transformImportsMap(this._imports, root, null, opts, unmountSpecifier) | |
| 223 | bundle._resolutions = transformResolutionsMap(this._resolutions, root, opts, unmountSpecifier) | |
| 224 | ||
| 225 | for (const [key, file] of this._files) { | |
| 226 | bundle._files.set(unmountSpecifier(key, root), file) | |
| 227 | } | |
| 228 | ||
| 229 | bundle._addons = transformFilesList(this._addons, root, unmountSpecifier) | |
| 230 | bundle._assets = transformFilesList(this._assets, root, unmountSpecifier) | |
| 231 | ||
| 232 | return bundle | |
| 233 | } | |
| 234 | ||
| 235 | toBuffer(opts = {}) { | |
| 236 | const { indent = 0, shared = false } = opts | |
| 237 | ||
| 238 | const header = { | |
| 239 | version: Bundle.version, | |
| 240 | id: this._id, | |
| 241 | main: this._main, | |
| 242 | imports: cloneImportsMap(this._imports), | |
| 243 | resolutions: cloneResolutionsMap(this._resolutions), | |
| 244 | addons: cloneFilesList(this._addons, 'Addons'), | |
| 245 | assets: cloneFilesList(this._assets, 'Assets'), | |
| 246 | files: {} | |
| 247 | } | |
| 248 | ||
| 249 | const keys = [...this._files.keys()].sort() | |
| 250 | ||
| 251 | let offset = 0 | |
| 252 | ||
| 253 | for (const key of keys) { | |
| 254 | const length = this.size(key) | |
| 255 | ||
| 256 | header.files[key] = { offset, length, mode: this.mode(key) } | |
| 257 | offset += length | |
| 258 | } | |
| 259 | ||
| 260 | const json = Buffer.from(`\n${JSON.stringify(header, null, indent)}\n`) | |
| 261 | ||
| 262 | const length = Buffer.from(json.byteLength.toString(10)) | |
| 263 | ||
| 264 | const total = length.byteLength + json.byteLength + offset | |
| 265 | ||
| 266 | const storage = shared ? new SharedArrayBuffer(total) : new ArrayBuffer(total) | |
| 267 | ||
| 268 | const buffer = Buffer.from(storage) | |
| 269 | ||
| 270 | offset = 0 | |
| 271 | ||
| 272 | buffer.set(length, offset) | |
| 273 | offset += length.byteLength | |
| 274 | ||
| 275 | buffer.set(json, offset) | |
| 276 | offset += json.byteLength | |
| 277 | ||
| 278 | for (const key of keys) { | |
| 279 | buffer.set(this.read(key), offset) | |
| 280 | offset += this.size(key) | |
| 281 | } | |
| 282 | ||
| 283 | return buffer | |
| 284 | } | |
| 285 | ||
| 286 | inspect() { | |
| 287 | return { | |
| 288 | __proto__: { constructor: Bundle }, | |
| 289 | ||
| 290 | version: this.version, | |
| 291 | id: this.id, | |
| 292 | main: this.main, | |
| 293 | imports: this.imports, | |
| 294 | resolutions: this.resolutions, | |
| 295 | addons: this.addons, | |
| 296 | assets: this.assets, | |
| 297 | files: this.files | |
| 298 | } | |
| 299 | } | |
| 300 | ||
| 301 | [Symbol.for('bare.inspect')]() { | |
| 302 | return this.inspect() | |
| 303 | } | |
| 304 | ||
| 305 | [Symbol.for('nodejs.util.inspect.custom')]() { | |
| 306 | return this.inspect() | |
| 307 | } | |
| 308 | } | |
| 309 | ||
| 310 | const Bundle = exports | |
| 311 | ||
| 312 | exports.errors = errors | |
| 313 | ||
| 314 | exports.isBundle = function isBundle(value) { | |
| 315 | if (value instanceof Bundle) return true | |
| 316 | ||
| 317 | return typeof value === 'object' && value !== null && value[kind] === Bundle[kind] | |
| 318 | } | |
| 319 | ||
| 320 | exports.from = function from(value) { | |
| 321 | // from(string) | |
| 322 | if (typeof value === 'string') return fromString(value) | |
| 323 | ||
| 324 | // from(buffer) | |
| 325 | if (Buffer.isBuffer(value)) return fromBuffer(value) | |
| 326 | ||
| 327 | // from(bundle) | |
| 328 | return value | |
| 329 | } | |
| 330 | ||
| 331 | function fromString(string) { | |
| 332 | return fromBuffer(Buffer.from(string)) | |
| 333 | } | |
| 334 | ||
| 335 | function fromBuffer(buffer) { | |
| 336 | if (buffer[0] === 0x23 /* # */ && buffer[1] === 0x21 /* ! */) { | |
| 337 | let end = 2 | |
| 338 | ||
| 339 | while (buffer[end] !== 0xa /* \n */) end++ | |
| 340 | ||
| 341 | buffer = buffer.subarray(end + 1) | |
| 342 | } | |
| 343 | ||
| 344 | let end = 0 | |
| 345 | ||
| 346 | while (isDecimal(buffer[end])) end++ | |
| 347 | ||
| 348 | const len = parseInt(buffer.toString('utf8', 0, end), 10) | |
| 349 | ||
| 350 | let header | |
| 351 | try { | |
| 352 | header = JSON.parse(buffer.toString('utf8', end, end + len)) | |
| 353 | } catch (err) { | |
| 354 | throw errors.INVALID_BUNDLE_HEADER('Invalid bundle header', err) | |
| 355 | } | |
| 356 | ||
| 357 | const bundle = new Bundle() | |
| 358 | ||
| 359 | // Go through the public API setters to ensure that the header fields are | |
| 360 | // validated. | |
| 361 | ||
| 362 | if (header.id) bundle.id = header.id | |
| 363 | if (header.main) bundle.main = header.main | |
| 364 | if (header.imports) bundle.imports = header.imports | |
| 365 | if (header.resolutions) bundle.resolutions = header.resolutions | |
| 366 | if (header.addons) bundle.addons = header.addons | |
| 367 | if (header.assets) bundle.assets = header.assets | |
| 368 | ||
| 369 | let offset = end + len | |
| 370 | ||
| 371 | for (const [file, info] of Object.entries(header.files)) { | |
| 372 | bundle.write(file, buffer.subarray(offset, offset + info.length), { | |
| 373 | mode: info.mode || 0o644 | |
| 374 | }) | |
| 375 | ||
| 376 | offset += info.length | |
| 377 | } | |
| 378 | ||
| 379 | return bundle | |
| 380 | } | |
| 381 | ||
| 382 | function isDecimal(c) { | |
| 383 | return c >= 0x30 && c <= 0x39 | |
| 384 | } | |
| 385 | ||
| 386 | function compareKeys([a], [b]) { | |
| 387 | return a > b ? 1 : a < b ? -1 : 0 | |
| 388 | } | |
| 389 | ||
| 390 | function cloneImportsMap(value) { | |
| 391 | if (typeof value === 'object' && value !== null) { | |
| 392 | const imports = {} | |
| 393 | ||
| 394 | for (const entry of Object.entries(value).sort(compareKeys)) { | |
| 395 | imports[entry[0]] = cloneImportsMapEntry(entry[1]) | |
| 396 | } | |
| 397 | ||
| 398 | return imports | |
| 399 | } | |
| 400 | ||
| 401 | throw new TypeError(`Imports map must be an object. Received type ${typeof value} (${value})`) | |
| 402 | } | |
| 403 | ||
| 404 | function cloneImportsMapEntry(value) { | |
| 405 | if (typeof value === 'string') return value | |
| 406 | ||
| 407 | if (typeof value === 'object' && value !== null) { | |
| 408 | const imports = {} | |
| 409 | ||
| 410 | for (const entry of Object.entries(value)) { | |
| 411 | imports[entry[0]] = cloneImportsMapEntry(entry[1]) | |
| 412 | } | |
| 413 | ||
| 414 | return imports | |
| 415 | } | |
| 416 | ||
| 417 | throw new TypeError( | |
| 418 | `Imports map entry must be a string or object. Received type ${typeof value} (${value})` | |
| 419 | ) | |
| 420 | } | |
| 421 | ||
| 422 | function cloneResolutionsMap(value) { | |
| 423 | if (typeof value === 'object' && value !== null) { | |
| 424 | const resolutions = {} | |
| 425 | ||
| 426 | for (const entry of Object.entries(value).sort(compareKeys)) { | |
| 427 | resolutions[entry[0]] = cloneImportsMap(entry[1]) | |
| 428 | } | |
| 429 | ||
| 430 | return resolutions | |
| 431 | } | |
| 432 | ||
| 433 | throw new TypeError(`Resolutions map must be an object. Received type ${typeof value} (${value})`) | |
| 434 | } | |
| 435 | ||
| 436 | function cloneFilesList(value, name) { | |
| 437 | if (Array.isArray(value)) { | |
| 438 | const files = [] | |
| 439 | ||
| 440 | for (const entry of value) { | |
| 441 | if (typeof entry !== 'string') { | |
| 442 | throw new TypeError( | |
| 443 | `${name} entry must be a string. Received type ${typeof entry} (${entry})` | |
| 444 | ) | |
| 445 | } | |
| 446 | ||
| 447 | files.push(entry) | |
| 448 | } | |
| 449 | ||
| 450 | return files.sort() | |
| 451 | } | |
| 452 | ||
| 453 | throw new TypeError(`${name} list must be an array. Received type ${typeof value} (${value})`) | |
| 454 | } | |
| 455 | ||
| 456 | function transformImportsMap(value, root, conditionalRoot, opts, fn) { | |
| 457 | const { conditions = {} } = opts | |
| 458 | ||
| 459 | const imports = {} | |
| 460 | ||
| 461 | for (const entry of Object.entries(value)) { | |
| 462 | const condition = entry[0] | |
| 463 | ||
| 464 | imports[condition] = transformImportsMapEntry( | |
| 465 | entry[1], | |
| 466 | root, | |
| 467 | conditionalRoot || conditions[condition], | |
| 468 | opts, | |
| 469 | fn | |
| 470 | ) | |
| 471 | } | |
| 472 | ||
| 473 | return imports | |
| 474 | } | |
| 475 | ||
| 476 | function transformImportsMapEntry(value, root, conditionalRoot, opts, fn) { | |
| 477 | const { conditions = {} } = opts | |
| 478 | ||
| 479 | if (typeof value === 'string') { | |
| 480 | return fn(value, conditionalRoot || conditions.default || root) | |
| 481 | } | |
| 482 | ||
| 483 | return transformImportsMap(value, root, conditionalRoot, opts, fn) | |
| 484 | } | |
| 485 | ||
| 486 | function transformResolutionsMap(value, root, opts, fn) { | |
| 487 | const resolutions = {} | |
| 488 | ||
| 489 | for (const entry of Object.entries(value)) { | |
| 490 | resolutions[fn(entry[0], root)] = transformImportsMap(entry[1], root, null, opts, fn) | |
| 491 | } | |
| 492 | ||
| 493 | return resolutions | |
| 494 | } | |
| 495 | ||
| 496 | function transformFilesList(value, root, fn) { | |
| 497 | const files = [] | |
| 498 | ||
| 499 | for (const entry of value) { | |
| 500 | files.push(fn(entry, root)) | |
| 501 | } | |
| 502 | ||
| 503 | return files | |
| 504 | } | |
| 505 | ||
| 506 | function mountSpecifier(specifier, root) { | |
| 507 | if (startsWithWindowsDriveLetter(specifier)) { | |
| 508 | specifier = '/' + specifier | |
| 509 | } | |
| 510 | ||
| 511 | if (specifier[0] === '/' || specifier[0] === '\\') { | |
| 512 | specifier = '.' + specifier | |
| 513 | } | |
| 514 | ||
| 515 | if (specifier.startsWith('./') || specifier.startsWith('.\\')) { | |
| 516 | return new URL(specifier, root).href | |
| 517 | } | |
| 518 | ||
| 519 | return specifier | |
| 520 | } | |
| 521 | ||
| 522 | function unmountSpecifier(specifier, root) { | |
| 523 | specifier = new URL(specifier) | |
| 524 | ||
| 525 | if (typeof root === 'string') root = new URL(root) | |
| 526 | ||
| 527 | if ( | |
| 528 | specifier.protocol !== root.protocol || | |
| 529 | specifier.host !== root.host || | |
| 530 | specifier.port !== root.port | |
| 531 | ) { | |
| 532 | return specifier.href | |
| 533 | } | |
| 534 | ||
| 535 | const specifierPath = splitPath(specifier.pathname) | |
| 536 | const rootPath = splitPath(root.pathname) | |
| 537 | ||
| 538 | while (specifierPath.length > 0 && rootPath[0] === specifierPath[0]) { | |
| 539 | specifierPath.shift() | |
| 540 | rootPath.shift() | |
| 541 | } | |
| 542 | ||
| 543 | rootPath.fill('..') | |
| 544 | ||
| 545 | return '/' + rootPath.concat(specifierPath).join('/') | |
| 546 | } | |
| 547 | ||
| 548 | function splitPath(path) { | |
| 549 | const parts = path.split('/') | |
| 550 | ||
| 551 | if (!parts[0]) parts.shift() | |
| 552 | if (!parts[parts.length - 1]) parts.pop() | |
| 553 | ||
| 554 | return parts | |
| 555 | } | |
| 556 | ||
| 557 | // https://infra.spec.whatwg.org/#ascii-upper-alpha | |
| 558 | function isASCIIUpperAlpha(c) { | |
| 559 | return c >= 0x41 && c <= 0x5a | |
| 560 | } | |
| 561 | ||
| 562 | // https://infra.spec.whatwg.org/#ascii-lower-alpha | |
| 563 | function isASCIILowerAlpha(c) { | |
| 564 | return c >= 0x61 && c <= 0x7a | |
| 565 | } | |
| 566 | ||
| 567 | // https://infra.spec.whatwg.org/#ascii-alpha | |
| 568 | function isASCIIAlpha(c) { | |
| 569 | return isASCIIUpperAlpha(c) || isASCIILowerAlpha(c) | |
| 570 | } | |
| 571 | ||
| 572 | // https://url.spec.whatwg.org/#windows-drive-letter | |
| 573 | function isWindowsDriveLetter(input) { | |
| 574 | return ( | |
| 575 | input.length >= 2 && | |
| 576 | isASCIIAlpha(input.charCodeAt(0)) && | |
| 577 | (input.charCodeAt(1) === 0x3a || input.charCodeAt(1) === 0x7c) | |
| 578 | ) | |
| 579 | } | |
| 580 | ||
| 581 | // https://url.spec.whatwg.org/#start-with-a-windows-drive-letter | |
| 582 | function startsWithWindowsDriveLetter(input) { | |
| 583 | return ( | |
| 584 | input.length >= 2 && | |
| 585 | isWindowsDriveLetter(input) && | |
| 586 | (input.length === 2 || | |
| 587 | input.charCodeAt(2) === 0x2f || | |
| 588 | input.charCodeAt(2) === 0x5c || | |
| 589 | input.charCodeAt(2) === 0x3f || | |
| 590 | input.charCodeAt(2) === 0x23) | |
| 591 | ) | |
| 592 | } |
| … | … | @@ -0,0 +1,20 @@ |
| 1 | module.exports = class BundleError extends Error { | |
| 2 | constructor(msg, fn = BundleError, opts = {}) { | |
| 3 | const { cause, code = fn.name } = opts | |
| 4 | ||
| 5 | super(`${code}: ${msg}`, { cause }) | |
| 6 | this.code = code | |
| 7 | ||
| 8 | if (Error.captureStackTrace) { | |
| 9 | Error.captureStackTrace(this, fn) | |
| 10 | } | |
| 11 | } | |
| 12 | ||
| 13 | get name() { | |
| 14 | return 'BundleError' | |
| 15 | } | |
| 16 | ||
| 17 | static INVALID_BUNDLE_HEADER(msg, cause) { | |
| 18 | return new BundleError(msg, BundleError.INVALID_BUNDLE_HEADER, { cause }) | |
| 19 | } | |
| 20 | } |
| … | … | @@ -0,0 +1,49 @@ |
| 1 | { | |
| 2 | "name": "bare-bundle", | |
| 3 | "version": "1.10.0", | |
| 4 | "description": "Application bundle format for JavaScript", | |
| 5 | "exports": { | |
| 6 | ".": { | |
| 7 | "types": "./index.d.ts", | |
| 8 | "default": "./index.js" | |
| 9 | }, | |
| 10 | "./package": "./package.json" | |
| 11 | }, | |
| 12 | "files": [ | |
| 13 | "index.js", | |
| 14 | "index.d.ts", | |
| 15 | "lib" | |
| 16 | ], | |
| 17 | "scripts": { | |
| 18 | "test": "prettier . --check && bare test.js" | |
| 19 | }, | |
| 20 | "repository": { | |
| 21 | "type": "git", | |
| 22 | "url": "git+https://github.com/holepunchto/bare-bundle.git" | |
| 23 | }, | |
| 24 | "author": "Holepunch", | |
| 25 | "license": "Apache-2.0", | |
| 26 | "bugs": { | |
| 27 | "url": "https://github.com/holepunchto/bare-bundle/issues" | |
| 28 | }, | |
| 29 | "homepage": "https://github.com/holepunchto/bare-bundle#readme", | |
| 30 | "devDependencies": { | |
| 31 | "bare-buffer": "^3.0.2", | |
| 32 | "bare-url": "^2.1.3", | |
| 33 | "brittle": "^3.1.1", | |
| 34 | "prettier": "^3.4.2", | |
| 35 | "prettier-config-holepunch": "^2.0.0" | |
| 36 | }, | |
| 37 | "peerDependencies": { | |
| 38 | "bare-buffer": "*", | |
| 39 | "bare-url": "*" | |
| 40 | }, | |
| 41 | "peerDependenciesMeta": { | |
| 42 | "bare-buffer": { | |
| 43 | "optional": true | |
| 44 | }, | |
| 45 | "bare-url": { | |
| 46 | "optional": true | |
| 47 | } | |
| 48 | } | |
| 49 | } |
| … | … | @@ -0,0 +1,202 @@ |
| 1 | Apache License | |
| 2 | Version 2.0, January 2004 | |
| 3 | http://www.apache.org/licenses/ | |
| 4 | ||
| 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | |
| 6 | ||
| 7 | 1. Definitions. | |
| 8 | ||
| 9 | "License" shall mean the terms and conditions for use, reproduction, | |
| 10 | and distribution as defined by Sections 1 through 9 of this document. | |
| 11 | ||
| 12 | "Licensor" shall mean the copyright owner or entity authorized by | |
| 13 | the copyright owner that is granting the License. | |
| 14 | ||
| 15 | "Legal Entity" shall mean the union of the acting entity and all | |
| 16 | other entities that control, are controlled by, or are under common | |
| 17 | control with that entity. For the purposes of this definition, | |
| 18 | "control" means (i) the power, direct or indirect, to cause the | |
| 19 | direction or management of such entity, whether by contract or | |
| 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the | |
| 21 | outstanding shares, or (iii) beneficial ownership of such entity. | |
| 22 | ||
| 23 | "You" (or "Your") shall mean an individual or Legal Entity | |
| 24 | exercising permissions granted by this License. | |
| 25 | ||
| 26 | "Source" form shall mean the preferred form for making modifications, | |
| 27 | including but not limited to software source code, documentation | |
| 28 | source, and configuration files. | |
| 29 | ||
| 30 | "Object" form shall mean any form resulting from mechanical | |
| 31 | transformation or translation of a Source form, including but | |
| 32 | not limited to compiled object code, generated documentation, | |
| 33 | and conversions to other media types. | |
| 34 | ||
| 35 | "Work" shall mean the work of authorship, whether in Source or | |
| 36 | Object form, made available under the License, as indicated by a | |
| 37 | copyright notice that is included in or attached to the work | |
| 38 | (an example is provided in the Appendix below). | |
| 39 | ||
| 40 | "Derivative Works" shall mean any work, whether in Source or Object | |
| 41 | form, that is based on (or derived from) the Work and for which the | |
| 42 | editorial revisions, annotations, elaborations, or other modifications | |
| 43 | represent, as a whole, an original work of authorship. For the purposes | |
| 44 | of this License, Derivative Works shall not include works that remain | |
| 45 | separable from, or merely link (or bind by name) to the interfaces of, | |
| 46 | the Work and Derivative Works thereof. | |
| 47 | ||
| 48 | "Contribution" shall mean any work of authorship, including | |
| 49 | the original version of the Work and any modifications or additions | |
| 50 | to that Work or Derivative Works thereof, that is intentionally | |
| 51 | submitted to Licensor for inclusion in the Work by the copyright owner | |
| 52 | or by an individual or Legal Entity authorized to submit on behalf of | |
| 53 | the copyright owner. For the purposes of this definition, "submitted" | |
| 54 | means any form of electronic, verbal, or written communication sent | |
| 55 | to the Licensor or its representatives, including but not limited to | |
| 56 | communication on electronic mailing lists, source code control systems, | |
| 57 | and issue tracking systems that are managed by, or on behalf of, the | |
| 58 | Licensor for the purpose of discussing and improving the Work, but | |
| 59 | excluding communication that is conspicuously marked or otherwise | |
| 60 | designated in writing by the copyright owner as "Not a Contribution." | |
| 61 | ||
| 62 | "Contributor" shall mean Licensor and any individual or Legal Entity | |
| 63 | on behalf of whom a Contribution has been received by Licensor and | |
| 64 | subsequently incorporated within the Work. | |
| 65 | ||
| 66 | 2. Grant of Copyright License. Subject to the terms and conditions of | |
| 67 | this License, each Contributor hereby grants to You a perpetual, | |
| 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 69 | copyright license to reproduce, prepare Derivative Works of, | |
| 70 | publicly display, publicly perform, sublicense, and distribute the | |
| 71 | Work and such Derivative Works in Source or Object form. | |
| 72 | ||
| 73 | 3. Grant of Patent License. Subject to the terms and conditions of | |
| 74 | this License, each Contributor hereby grants to You a perpetual, | |
| 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 76 | (except as stated in this section) patent license to make, have made, | |
| 77 | use, offer to sell, sell, import, and otherwise transfer the Work, | |
| 78 | where such license applies only to those patent claims licensable | |
| 79 | by such Contributor that are necessarily infringed by their | |
| 80 | Contribution(s) alone or by combination of their Contribution(s) | |
| 81 | with the Work to which such Contribution(s) was submitted. If You | |
| 82 | institute patent litigation against any entity (including a | |
| 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work | |
| 84 | or a Contribution incorporated within the Work constitutes direct | |
| 85 | or contributory patent infringement, then any patent licenses | |
| 86 | granted to You under this License for that Work shall terminate | |
| 87 | as of the date such litigation is filed. | |
| 88 | ||
| 89 | 4. Redistribution. You may reproduce and distribute copies of the | |
| 90 | Work or Derivative Works thereof in any medium, with or without | |
| 91 | modifications, and in Source or Object form, provided that You | |
| 92 | meet the following conditions: | |
| 93 | ||
| 94 | (a) You must give any other recipients of the Work or | |
| 95 | Derivative Works a copy of this License; and | |
| 96 | ||
| 97 | (b) You must cause any modified files to carry prominent notices | |
| 98 | stating that You changed the files; and | |
| 99 | ||
| 100 | (c) You must retain, in the Source form of any Derivative Works | |
| 101 | that You distribute, all copyright, patent, trademark, and | |
| 102 | attribution notices from the Source form of the Work, | |
| 103 | excluding those notices that do not pertain to any part of | |
| 104 | the Derivative Works; and | |
| 105 | ||
| 106 | (d) If the Work includes a "NOTICE" text file as part of its | |
| 107 | distribution, then any Derivative Works that You distribute must | |
| 108 | include a readable copy of the attribution notices contained | |
| 109 | within such NOTICE file, excluding those notices that do not | |
| 110 | pertain to any part of the Derivative Works, in at least one | |
| 111 | of the following places: within a NOTICE text file distributed | |
| 112 | as part of the Derivative Works; within the Source form or | |
| 113 | documentation, if provided along with the Derivative Works; or, | |
| 114 | within a display generated by the Derivative Works, if and | |
| 115 | wherever such third-party notices normally appear. The contents | |
| 116 | of the NOTICE file are for informational purposes only and | |
| 117 | do not modify the License. You may add Your own attribution | |
| 118 | notices within Derivative Works that You distribute, alongside | |
| 119 | or as an addendum to the NOTICE text from the Work, provided | |
| 120 | that such additional attribution notices cannot be construed | |
| 121 | as modifying the License. | |
| 122 | ||
| 123 | You may add Your own copyright statement to Your modifications and | |
| 124 | may provide additional or different license terms and conditions | |
| 125 | for use, reproduction, or distribution of Your modifications, or | |
| 126 | for any such Derivative Works as a whole, provided Your use, | |
| 127 | reproduction, and distribution of the Work otherwise complies with | |
| 128 | the conditions stated in this License. | |
| 129 | ||
| 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, | |
| 131 | any Contribution intentionally submitted for inclusion in the Work | |
| 132 | by You to the Licensor shall be under the terms and conditions of | |
| 133 | this License, without any additional terms or conditions. | |
| 134 | Notwithstanding the above, nothing herein shall supersede or modify | |
| 135 | the terms of any separate license agreement you may have executed | |
| 136 | with Licensor regarding such Contributions. | |
| 137 | ||
| 138 | 6. Trademarks. This License does not grant permission to use the trade | |
| 139 | names, trademarks, service marks, or product names of the Licensor, | |
| 140 | except as required for reasonable and customary use in describing the | |
| 141 | origin of the Work and reproducing the content of the NOTICE file. | |
| 142 | ||
| 143 | 7. Disclaimer of Warranty. Unless required by applicable law or | |
| 144 | agreed to in writing, Licensor provides the Work (and each | |
| 145 | Contributor provides its Contributions) on an "AS IS" BASIS, | |
| 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |
| 147 | implied, including, without limitation, any warranties or conditions | |
| 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | |
| 149 | PARTICULAR PURPOSE. You are solely responsible for determining the | |
| 150 | appropriateness of using or redistributing the Work and assume any | |
| 151 | risks associated with Your exercise of permissions under this License. | |
| 152 | ||
| 153 | 8. Limitation of Liability. In no event and under no legal theory, | |
| 154 | whether in tort (including negligence), contract, or otherwise, | |
| 155 | unless required by applicable law (such as deliberate and grossly | |
| 156 | negligent acts) or agreed to in writing, shall any Contributor be | |
| 157 | liable to You for damages, including any direct, indirect, special, | |
| 158 | incidental, or consequential damages of any character arising as a | |
| 159 | result of this License or out of the use or inability to use the | |
| 160 | Work (including but not limited to damages for loss of goodwill, | |
| 161 | work stoppage, computer failure or malfunction, or any and all | |
| 162 | other commercial damages or losses), even if such Contributor | |
| 163 | has been advised of the possibility of such damages. | |
| 164 | ||
| 165 | 9. Accepting Warranty or Additional Liability. While redistributing | |
| 166 | the Work or Derivative Works thereof, You may choose to offer, | |
| 167 | and charge a fee for, acceptance of support, warranty, indemnity, | |
| 168 | or other liability obligations and/or rights consistent with this | |
| 169 | License. However, in accepting such obligations, You may act only | |
| 170 | on Your own behalf and on Your sole responsibility, not on behalf | |
| 171 | of any other Contributor, and only if You agree to indemnify, | |
| 172 | defend, and hold each Contributor harmless for any liability | |
| 173 | incurred by, or claims asserted against, such Contributor by reason | |
| 174 | of your accepting any such warranty or additional liability. | |
| 175 | ||
| 176 | END OF TERMS AND CONDITIONS | |
| 177 | ||
| 178 | APPENDIX: How to apply the Apache License to your work. | |
| 179 | ||
| 180 | To apply the Apache License to your work, attach the following | |
| 181 | boilerplate notice, with the fields enclosed by brackets "[]" | |
| 182 | replaced with your own identifying information. (Don't include | |
| 183 | the brackets!) The text should be enclosed in the appropriate | |
| 184 | comment syntax for the file format. We also recommend that a | |
| 185 | file or class name and description of purpose be included on the | |
| 186 | same "printed page" as the copyright notice for easier | |
| 187 | identification within third-party archives. | |
| 188 | ||
| 189 | Copyright [yyyy] [name of copyright owner] | |
| 190 | ||
| 191 | Licensed under the Apache License, Version 2.0 (the "License"); | |
| 192 | you may not use this file except in compliance with the License. | |
| 193 | You may obtain a copy of the License at | |
| 194 | ||
| 195 | http://www.apache.org/licenses/LICENSE-2.0 | |
| 196 | ||
| 197 | Unless required by applicable law or agreed to in writing, software | |
| 198 | distributed under the License is distributed on an "AS IS" BASIS, | |
| 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 200 | See the License for the specific language governing permissions and | |
| 201 | limitations under the License. | |
| 202 |
| … | … | @@ -0,0 +1,38 @@ |
| 1 | # bare-cov | |
| 2 | ||
| 3 | Run tests with coverage | |
| 4 | ||
| 5 | ### Installation | |
| 6 | ||
| 7 | ``` | |
| 8 | npm i bare-cov | |
| 9 | ``` | |
| 10 | ||
| 11 | ### Usage | |
| 12 | ||
| 13 | ```js | |
| 14 | // Record coverage until current process ends and then generate reports | |
| 15 | require('bare-cov')(options) | |
| 16 | ``` | |
| 17 | ||
| 18 | ### Options | |
| 19 | ||
| 20 | #### reporters | |
| 21 | ||
| 22 | Coverage reporter(s) to use (default: `['text', 'json']`) | |
| 23 | ||
| 24 | #### reporterOptions | |
| 25 | ||
| 26 | Options to pass to each reporter keyed by reporter (default: `{}`) | |
| 27 | ||
| 28 | #### dir | |
| 29 | ||
| 30 | Directory to write coverage reports to (default: `coverage`) | |
| 31 | ||
| 32 | #### skipRawDump | |
| 33 | ||
| 34 | Skip saving of raw v8 coverage data to disk (default: `false`) | |
| 35 | ||
| 36 | # License | |
| 37 | ||
| 38 | Apache-2.0 |
| … | … | @@ -0,0 +1,45 @@ |
| 1 | 'use strict' | |
| 2 | ||
| 3 | const { isBare } = require('which-runtime') | |
| 4 | const { Session } = require('inspector') | |
| 5 | const fs = require('fs') | |
| 6 | const path = require('path') | |
| 7 | const Transformer = require('./lib/transformer') | |
| 8 | const process = require('process') | |
| 9 | ||
| 10 | module.exports = async function setupCoverage(opts = {}) { | |
| 11 | const cwd = process.cwd() | |
| 12 | const dir = path.resolve(opts.dir ?? 'coverage') | |
| 13 | const session = new Session() | |
| 14 | session.connect() | |
| 15 | ||
| 16 | const sessionPost = (...args) => | |
| 17 | new Promise((resolve, reject) => | |
| 18 | session.post(...args, (err, result) => (err ? reject(err) : resolve(result))) | |
| 19 | ) | |
| 20 | ||
| 21 | await sessionPost('Profiler.enable') | |
| 22 | await sessionPost('Profiler.startPreciseCoverage', { | |
| 23 | callCount: true, | |
| 24 | detailed: true | |
| 25 | }) | |
| 26 | ||
| 27 | process.once('beforeExit', async () => { | |
| 28 | const v8Report = await sessionPost('Profiler.takePreciseCoverage') | |
| 29 | isBare ? session.destroy() : session.disconnect() | |
| 30 | ||
| 31 | if (opts.skipRawDump !== true) { | |
| 32 | if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }) | |
| 33 | fs.writeFileSync(path.join(dir, 'v8-coverage.json'), JSON.stringify(v8Report)) | |
| 34 | } | |
| 35 | ||
| 36 | const reporters = Array.isArray(opts.reporters) ? opts.reporters : ['text', 'json'] | |
| 37 | ||
| 38 | const transformer = new Transformer({ ...opts, cwd }) | |
| 39 | const coverageMap = await transformer.transformToCoverageMap(v8Report) | |
| 40 | if (reporters.includes('json')) { | |
| 41 | fs.writeFileSync(path.join(dir, 'coverage-final.json'), JSON.stringify(coverageMap)) | |
| 42 | } | |
| 43 | if (reporters.includes('text')) transformer.report(coverageMap) | |
| 44 | }) | |
| 45 | } |
| … | … | @@ -0,0 +1,118 @@ |
| 1 | // Based on https://github.com/istanbuljs/istanbuljs/blob/main/packages/istanbul-lib-coverage/lib/file-coverage.js | |
| 2 | const isLineColumn = (o) => typeof o?.line === 'number' && typeof o?.column === 'number' | |
| 3 | const isLoc = (o) => isLineColumn(o?.start) && isLineColumn(o.end) | |
| 4 | const getLoc = (o) => (isLoc(o) ? o : isLoc(o.loc) ? o.loc : null) | |
| 5 | ||
| 6 | function findNearestContainer(coverageRecord, targetRecordMap) { | |
| 7 | const loc = getLoc(coverageRecord) | |
| 8 | if (!loc) return null | |
| 9 | ||
| 10 | let container = { nearest: null, distance: null, key: null } | |
| 11 | for (const [key, [, targetCoverageRecord]] of Object.entries(targetRecordMap)) { | |
| 12 | const tLoc = getLoc(targetCoverageRecord) | |
| 13 | if (!tLoc) continue | |
| 14 | ||
| 15 | const distance = [ | |
| 16 | loc.start.line - tLoc.start.line, | |
| 17 | loc.start.column - tLoc.start.column, | |
| 18 | tLoc.end.line - loc.end.line, | |
| 19 | tLoc.end.column - loc.end.column | |
| 20 | ] | |
| 21 | if ( | |
| 22 | distance[0] < 0 || | |
| 23 | distance[2] < 0 || | |
| 24 | (distance[0] === 0 && distance[1] < 0) || | |
| 25 | (distance[2] === 0 && distance[3] < 0) | |
| 26 | ) | |
| 27 | continue | |
| 28 | if (container.nearest === null) { | |
| 29 | container = { nearest: targetCoverageRecord, distance, key } | |
| 30 | continue | |
| 31 | } | |
| 32 | ||
| 33 | const closerBefore = | |
| 34 | distance[0] < container.distance[0] || | |
| 35 | (distance[0] === 0 && distance[1] < container.distance[1]) | |
| 36 | const closerAfter = | |
| 37 | distance[2] < container.distance[2] || | |
| 38 | (distance[2] === 0 && distance[3] < container.distance[3]) | |
| 39 | if (closerBefore || closerAfter) container = { nearest: targetCoverageRecord, distance, key } | |
| 40 | } | |
| 41 | return container.key | |
| 42 | } | |
| 43 | ||
| 44 | function addContainedHits(hits, coverageRecord, sourceRecord) { | |
| 45 | const container = findNearestContainer(coverageRecord, sourceRecord) | |
| 46 | return container ? addHits(hits, sourceRecord[container][0]) : hits | |
| 47 | } | |
| 48 | ||
| 49 | function addHits(a, b) { | |
| 50 | if (typeof a === 'number' && typeof b === 'number') return a + b | |
| 51 | ||
| 52 | if (Array.isArray(a) && Array.isArray(b)) { | |
| 53 | if (a.length >= b.length) return a.map((hits, i) => hits + (b[i] || 0)) | |
| 54 | return b.map((hits, i) => hits + (a[i] || 0)) | |
| 55 | } | |
| 56 | ||
| 57 | return null | |
| 58 | } | |
| 59 | ||
| 60 | function toHitMap(hits, map, toKey) { | |
| 61 | return Object.entries(hits).reduce((acc, [id, value]) => { | |
| 62 | const coverageRecord = map[id] | |
| 63 | if (coverageRecord) acc[toKey(coverageRecord)] = [value, coverageRecord] | |
| 64 | return acc | |
| 65 | }, {}) | |
| 66 | } | |
| 67 | ||
| 68 | function mergeHitMaps(a, b) { | |
| 69 | const mergedRecords = {} | |
| 70 | for (const [key, [hits, record]] of Object.entries(a)) { | |
| 71 | const matchingValue = b[key] | |
| 72 | const newHits = matchingValue | |
| 73 | ? addHits(hits, matchingValue[0]) | |
| 74 | : addContainedHits(hits, record, b) | |
| 75 | mergedRecords[key] = [newHits, record] | |
| 76 | } | |
| 77 | ||
| 78 | for (const [key, [hits, record]] of Object.entries(b)) { | |
| 79 | if (mergedRecords[key]) continue | |
| 80 | const newHits = addContainedHits(hits, record, a) | |
| 81 | mergedRecords[key] = [newHits, record] | |
| 82 | } | |
| 83 | ||
| 84 | return Object.values(mergedRecords).reduce( | |
| 85 | (acc, [hits, record], index) => { | |
| 86 | acc.hits[index] = hits | |
| 87 | acc.map[index] = record | |
| 88 | return acc | |
| 89 | }, | |
| 90 | { hits: {}, map: {} } | |
| 91 | ) | |
| 92 | } | |
| 93 | ||
| 94 | function mergeCoverages(a, b) { | |
| 95 | const merged = { ...a } | |
| 96 | ||
| 97 | const toKey = ({ start, end }) => `${start.line}|${start.column}|${end.line}|${end.column}` | |
| 98 | ;({ hits: merged.s, map: merged.statementMap } = mergeHitMaps( | |
| 99 | toHitMap(a.s, a.statementMap, toKey), | |
| 100 | toHitMap(b.s, b.statementMap, toKey) | |
| 101 | )) | |
| 102 | ||
| 103 | const toKeyFn = ({ loc }) => toKey(loc) | |
| 104 | ;({ hits: merged.f, map: merged.fnMap } = mergeHitMaps( | |
| 105 | toHitMap(a.f, a.fnMap, toKeyFn), | |
| 106 | toHitMap(b.f, b.fnMap, toKeyFn) | |
| 107 | )) | |
| 108 | ||
| 109 | const toKeyBranch = ({ locations }) => toKey(locations[0]) | |
| 110 | ;({ hits: merged.b, map: merged.branchMap } = mergeHitMaps( | |
| 111 | toHitMap(a.b, a.branchMap, toKeyBranch), | |
| 112 | toHitMap(b.b, b.branchMap, toKeyBranch) | |
| 113 | )) | |
| 114 | ||
| 115 | return merged | |
| 116 | } | |
| 117 | ||
| 118 | module.exports = mergeCoverages |
| … | … | @@ -0,0 +1,215 @@ |
| 1 | const path = require('path') | |
| 2 | const process = require('process') | |
| 3 | ||
| 4 | // TODO: colorize output if available | |
| 5 | ||
| 6 | const PERCENT_WIDTH = 9 | |
| 7 | const DIR_INDENT = 1 | |
| 8 | const FILE_INDENT = 2 | |
| 9 | const TERMINAL_WIDTH = process?.stdout?.columns ?? 80 | |
| 10 | const STATIC_WIDTH = PERCENT_WIDTH * 4 + 6 // 4 percent columns + 5 separators and 1 branch col space | |
| 11 | const MIN_NAME_WIDTH = 5 | |
| 12 | const MIN_UNCOVERED_WIDTH = 17 | |
| 13 | ||
| 14 | function findCommonPath(paths) { | |
| 15 | const getCommon = (a, b) => { | |
| 16 | let i = 0 | |
| 17 | while (i < a.length && i < b.length && a[i] === b[i]) i++ | |
| 18 | return a.slice(0, i) | |
| 19 | } | |
| 20 | ||
| 21 | if (!paths || paths.length === 0) return [] | |
| 22 | ||
| 23 | const splitPaths = paths.map((p) => p.split(path.sep)) | |
| 24 | return splitPaths.slice(1).reduce((acc, path) => { | |
| 25 | return getCommon(acc, path) | |
| 26 | }, splitPaths[0]) | |
| 27 | } | |
| 28 | ||
| 29 | function truncate(str, width, right = false, delimiter = null) { | |
| 30 | if (str.length <= width) return str | |
| 31 | let truncateAt = right ? str.length - width + 1 : width - 1 | |
| 32 | if (delimiter) { | |
| 33 | const nextSeparatorIndex = right | |
| 34 | ? str.indexOf(delimiter, truncateAt) | |
| 35 | : str.lastIndexOf(delimiter, truncateAt - 1) | |
| 36 | if (nextSeparatorIndex === -1) return '…' | |
| 37 | truncateAt = right ? nextSeparatorIndex : nextSeparatorIndex + 1 | |
| 38 | } | |
| 39 | return right ? '…' + str.slice(truncateAt) : str.slice(0, truncateAt) + '…' | |
| 40 | } | |
| 41 | ||
| 42 | function toPercent(covered, total) { | |
| 43 | if (total === 0 || covered === total) return '100' | |
| 44 | if (covered === 0) return '0' | |
| 45 | return ((covered / total) * 100).toFixed(2) | |
| 46 | } | |
| 47 | ||
| 48 | function ntimes(n, str) { | |
| 49 | return Array(n).fill(str).join('') | |
| 50 | } | |
| 51 | ||
| 52 | function pad(str, width, right) { | |
| 53 | if (str.length >= width) return str | |
| 54 | const fill = ntimes(width - str.length, ' ') | |
| 55 | return right ? str + fill : fill + str | |
| 56 | } | |
| 57 | ||
| 58 | function rowSeparator(nameWidth, uncoveredWidth) { | |
| 59 | const name = ntimes(nameWidth, '-') | |
| 60 | const uncovered = ntimes(uncoveredWidth, '-') | |
| 61 | const percent = ntimes(PERCENT_WIDTH, '-') | |
| 62 | const branch = ntimes(PERCENT_WIDTH + 1, '-') | |
| 63 | return [name, percent, branch, percent, percent, uncovered].join('|') | |
| 64 | } | |
| 65 | ||
| 66 | function printHeader(nameWidth, uncoveredWidth) { | |
| 67 | const name = pad('File', nameWidth, true) | |
| 68 | const unc = pad(truncate('Uncovered Line #s', uncoveredWidth - 1), uncoveredWidth - 1, true) | |
| 69 | ||
| 70 | console.log(`${name}| % Stmts | % Branch | % Funcs | % Lines | ${unc}`) | |
| 71 | } | |
| 72 | ||
| 73 | function printRow(fileSummary, fileName, nameWidth, uncoveredWidth) { | |
| 74 | const statements = toPercent(fileSummary.covered.statements, fileSummary.total.statements) | |
| 75 | const branches = toPercent(fileSummary.covered.branches, fileSummary.total.branches) | |
| 76 | const functions = toPercent(fileSummary.covered.functions, fileSummary.total.functions) | |
| 77 | const lines = toPercent(fileSummary.covered.lines, fileSummary.total.lines) | |
| 78 | ||
| 79 | const uncoveredLines = fileSummary?.uncovered?.normalizedLines | |
| 80 | ? fileSummary.uncovered.normalizedLines | |
| 81 | : ' ' | |
| 82 | ||
| 83 | console.log( | |
| 84 | [ | |
| 85 | pad(` ${truncate(fileName, nameWidth - 2)} `, nameWidth, true), | |
| 86 | pad(` ${statements} `, PERCENT_WIDTH), | |
| 87 | pad(` ${branches} `, PERCENT_WIDTH + 1), | |
| 88 | pad(` ${functions} `, PERCENT_WIDTH), | |
| 89 | pad(` ${lines} `, PERCENT_WIDTH), | |
| 90 | pad(` ${truncate(uncoveredLines, uncoveredWidth - 2, true, ',')} `, uncoveredWidth, true) | |
| 91 | ].join('|') | |
| 92 | ) | |
| 93 | } | |
| 94 | ||
| 95 | function deriveWidths(groupedSummaries) { | |
| 96 | let nameWidth = MIN_NAME_WIDTH | |
| 97 | let uncoveredWidth = MIN_UNCOVERED_WIDTH | |
| 98 | ||
| 99 | for (const [dirName, dirSummary] of Object.entries(groupedSummaries)) { | |
| 100 | if (dirName.length + DIR_INDENT > nameWidth) nameWidth = dirName.length + DIR_INDENT | |
| 101 | ||
| 102 | for (const [fileName, fileSummary] of Object.entries(dirSummary.files)) { | |
| 103 | const normalizedLines = fileSummary.uncovered?.normalizedLines | |
| 104 | if (fileName.length + FILE_INDENT > nameWidth) nameWidth = fileName.length + FILE_INDENT | |
| 105 | if (normalizedLines && normalizedLines.length > uncoveredWidth) | |
| 106 | uncoveredWidth = normalizedLines.length | |
| 107 | } | |
| 108 | } | |
| 109 | ||
| 110 | // Add left/right space padding | |
| 111 | nameWidth += 2 | |
| 112 | uncoveredWidth += 2 | |
| 113 | ||
| 114 | if (nameWidth + uncoveredWidth + STATIC_WIDTH > TERMINAL_WIDTH) { | |
| 115 | const availableWidth = TERMINAL_WIDTH - STATIC_WIDTH | |
| 116 | nameWidth = Math.floor(availableWidth / 2) | |
| 117 | uncoveredWidth = availableWidth - nameWidth | |
| 118 | } | |
| 119 | ||
| 120 | return { nameWidth, uncoveredWidth } | |
| 121 | } | |
| 122 | ||
| 123 | function normalizeUncoveredLines(uncoveredLines) { | |
| 124 | if (!uncoveredLines || uncoveredLines.length === 0) return '' | |
| 125 | ||
| 126 | const lines = uncoveredLines.map((line) => Number(line)).sort((a, b) => a - b) | |
| 127 | const ranges = [] | |
| 128 | let start = lines[0] | |
| 129 | let end = start | |
| 130 | ||
| 131 | for (let i = 1; i < lines.length; i++) { | |
| 132 | if (lines[i] === end + 1) { | |
| 133 | end = lines[i] | |
| 134 | continue | |
| 135 | } | |
| 136 | ||
| 137 | ranges.push(start === end ? `${start}` : `${start}-${end}`) | |
| 138 | start = lines[i] | |
| 139 | end = start | |
| 140 | } | |
| 141 | ranges.push(start === end ? `${start}` : `${start}-${end}`) | |
| 142 | ||
| 143 | return ranges.join(',') | |
| 144 | } | |
| 145 | ||
| 146 | function addCoverages(a, b) { | |
| 147 | a.covered.statements += b.covered.statements | |
| 148 | a.covered.branches += b.covered.branches | |
| 149 | a.covered.functions += b.covered.functions | |
| 150 | a.covered.lines += b.covered.lines | |
| 151 | ||
| 152 | a.total.statements += b.total.statements | |
| 153 | a.total.branches += b.total.branches | |
| 154 | a.total.functions += b.total.functions | |
| 155 | a.total.lines += b.total.lines | |
| 156 | } | |
| 157 | ||
| 158 | function groupByDirectory(fileSummaries) { | |
| 159 | const commonPath = findCommonPath(Object.keys(fileSummaries)).join(path.sep) | |
| 160 | const commonDir = path.basename(commonPath) | |
| 161 | ||
| 162 | const directories = {} | |
| 163 | for (const [filePath, summary] of Object.entries(fileSummaries)) { | |
| 164 | const relativePath = path.relative(commonPath, filePath).replace(/^\.\//g, '') | |
| 165 | const pathName = path.join(commonDir, relativePath) | |
| 166 | const directory = path.dirname(pathName) | |
| 167 | const file = path.basename(pathName) | |
| 168 | if (!directories[directory]) { | |
| 169 | directories[directory] = { | |
| 170 | files: {}, | |
| 171 | coverage: { | |
| 172 | total: { statements: 0, branches: 0, functions: 0, lines: 0 }, | |
| 173 | covered: { statements: 0, branches: 0, functions: 0, lines: 0 } | |
| 174 | } | |
| 175 | } | |
| 176 | } | |
| 177 | ||
| 178 | directories[directory].files[file] = summary | |
| 179 | ||
| 180 | addCoverages(directories[directory].coverage, summary) | |
| 181 | } | |
| 182 | ||
| 183 | return directories | |
| 184 | } | |
| 185 | ||
| 186 | function printBody(grouped, nameWidth, uncoveredWidth) { | |
| 187 | for (const [dirName, dirSummary] of Object.entries(grouped)) { | |
| 188 | printRow(dirSummary.coverage, `${ntimes(DIR_INDENT, ' ')}${dirName}`, nameWidth, uncoveredWidth) | |
| 189 | for (const [fileName, fileSummary] of Object.entries(dirSummary.files)) { | |
| 190 | printRow(fileSummary, `${ntimes(FILE_INDENT, ' ')}${fileName}`, nameWidth, uncoveredWidth) | |
| 191 | } | |
| 192 | } | |
| 193 | } | |
| 194 | ||
| 195 | module.exports = function reportCoverage({ summary, fileSummaries }) { | |
| 196 | Object.values(fileSummaries).forEach((fileSummary) => { | |
| 197 | fileSummary.uncovered.normalizedLines = normalizeUncoveredLines(fileSummary.uncovered.lines) | |
| 198 | }) | |
| 199 | ||
| 200 | const grouped = groupByDirectory(fileSummaries) | |
| 201 | ||
| 202 | const { nameWidth, uncoveredWidth } = deriveWidths(grouped) | |
| 203 | ||
| 204 | const separator = rowSeparator(nameWidth, uncoveredWidth) | |
| 205 | const printSeparator = () => console.log(separator) | |
| 206 | ||
| 207 | printSeparator() | |
| 208 | printHeader(nameWidth, uncoveredWidth) | |
| 209 | printSeparator() | |
| 210 | ||
| 211 | printRow(summary, 'All files', nameWidth, uncoveredWidth) | |
| 212 | printBody(grouped, nameWidth, uncoveredWidth) | |
| 213 | ||
| 214 | printSeparator() | |
| 215 | } |
| … | … | @@ -0,0 +1,75 @@ |
| 1 | function summarizeFileCoverage(file) { | |
| 2 | const summary = { | |
| 3 | total: { statements: 0, branches: 0, functions: 0, lines: 0 }, | |
| 4 | covered: { statements: 0, branches: 0, functions: 0, lines: 0 }, | |
| 5 | uncovered: { lines: [] } | |
| 6 | } | |
| 7 | ||
| 8 | if (file.s) { | |
| 9 | const statements = Object.values(file.s) | |
| 10 | summary.total.statements = statements.length | |
| 11 | summary.covered.statements = statements.filter((hitCount) => hitCount > 0).length | |
| 12 | } | |
| 13 | ||
| 14 | if (file.b) { | |
| 15 | for (const branch of Object.values(file.b)) { | |
| 16 | summary.total.branches += branch.length | |
| 17 | summary.covered.branches += branch.filter((hitCount) => hitCount > 0).length | |
| 18 | } | |
| 19 | } | |
| 20 | ||
| 21 | if (file.f) { | |
| 22 | const functions = Object.values(file.f) | |
| 23 | summary.total.functions = functions.length | |
| 24 | summary.covered.functions = functions.filter((hitCount) => hitCount > 0).length | |
| 25 | } | |
| 26 | ||
| 27 | let lineCoverage = file.l | |
| 28 | if (!lineCoverage && file.s && file.statementMap) { | |
| 29 | lineCoverage = {} | |
| 30 | for (const [statementId, hitCount] of Object.entries(file.s)) { | |
| 31 | const statementInfo = file.statementMap[statementId] | |
| 32 | if (!statementInfo) continue | |
| 33 | ||
| 34 | const { line } = statementInfo.start | |
| 35 | const lastCount = lineCoverage[line] | |
| 36 | if (lastCount === undefined || lastCount < hitCount) lineCoverage[line] = hitCount | |
| 37 | } | |
| 38 | } | |
| 39 | ||
| 40 | const lines = Object.entries(lineCoverage) | |
| 41 | summary.total.lines = lines.length | |
| 42 | for (const [line, hitCount] of lines) { | |
| 43 | if (hitCount > 0) summary.covered.lines++ | |
| 44 | else summary.uncovered.lines.push(line) | |
| 45 | } | |
| 46 | ||
| 47 | return summary | |
| 48 | } | |
| 49 | ||
| 50 | module.exports = function summarizeCoverage(coverageData) { | |
| 51 | const summary = { | |
| 52 | total: { statements: 0, branches: 0, functions: 0, lines: 0 }, | |
| 53 | covered: { statements: 0, branches: 0, functions: 0, lines: 0 } | |
| 54 | } | |
| 55 | ||
| 56 | const fileSummaries = {} | |
| 57 | ||
| 58 | for (const [filename, file] of Object.entries(coverageData)) { | |
| 59 | const fileSummary = (fileSummaries[filename] = summarizeFileCoverage(file)) | |
| 60 | ||
| 61 | summary.total.statements += fileSummary.total.statements | |
| 62 | summary.covered.statements += fileSummary.covered.statements | |
| 63 | summary.total.branches += fileSummary.total.branches | |
| 64 | summary.covered.branches += fileSummary.covered.branches | |
| 65 | summary.total.functions += fileSummary.total.functions | |
| 66 | summary.covered.functions += fileSummary.covered.functions | |
| 67 | summary.total.lines += fileSummary.total.lines | |
| 68 | summary.covered.lines += fileSummary.covered.lines | |
| 69 | } | |
| 70 | ||
| 71 | return { | |
| 72 | summary, | |
| 73 | fileSummaries | |
| 74 | } | |
| 75 | } |
| … | … | @@ -0,0 +1,112 @@ |
| 1 | const { isWindows } = require('which-runtime') | |
| 2 | const path = require('path') | |
| 3 | const process = require('process') | |
| 4 | const picomatch = require('picomatch') | |
| 5 | ||
| 6 | const DEFAULT_EXCLUDES = [ | |
| 7 | 'coverage/**', | |
| 8 | 'packages/*/test{,s}/**', | |
| 9 | '**/*.d.ts', | |
| 10 | 'test{,s}/**', | |
| 11 | 'test{,-*}.{js,cjs,mjs,ts,tsx,jsx}', | |
| 12 | '**/*{.,-}test.{js,cjs,mjs,ts,tsx,jsx}', | |
| 13 | '**/__tests__/**', | |
| 14 | '**/{ava,babel,nyc}.config.{js,cjs,mjs}', | |
| 15 | '**/jest.config.{js,cjs,mjs,ts}', | |
| 16 | '**/{karma,rollup,webpack}.config.js', | |
| 17 | '**/.{eslint,mocha}rc.{js,cjs}' | |
| 18 | ] | |
| 19 | ||
| 20 | const DEFAULT_EXTENSIONS = ['.js', '.cjs', '.mjs', '.ts', '.tsx', '.jsx'] | |
| 21 | ||
| 22 | function isOutsideDir(dir, filename) { | |
| 23 | return isWindows | |
| 24 | ? !path.resolve(dir, filename).startsWith(path.resolve(dir) + path.sep) | |
| 25 | : /^\.\./.test(path.relative(dir, filename)) | |
| 26 | } | |
| 27 | ||
| 28 | // Based off https://github.com/istanbuljs/test-exclude/blob/master/index.js with glob-related code removed and minimatch replaced with picomatch | |
| 29 | class TestExclude { | |
| 30 | constructor(opts = {}) { | |
| 31 | this.cwd = opts.cwd ?? process.cwd() | |
| 32 | this.include = opts.include ?? [] | |
| 33 | this.exclude = opts.exclude ?? DEFAULT_EXCLUDES | |
| 34 | this.extension = opts.extension ?? DEFAULT_EXTENSIONS | |
| 35 | this.excludeNodeModules = opts.excludeNodeModules || true | |
| 36 | this.relativePath = opts.relativePath || true | |
| 37 | ||
| 38 | if (typeof this.include === 'string') this.include = [this.include] | |
| 39 | if (typeof this.exclude === 'string') this.exclude = [this.exclude] | |
| 40 | ||
| 41 | if (typeof this.extension === 'string') this.extension = [this.extension] | |
| 42 | else if (this.extension.length === 0) this.extension = false | |
| 43 | ||
| 44 | if (this.include && this.include.length > 0) | |
| 45 | this.include = prepGlobPatterns([].concat(this.include)) | |
| 46 | else this.include = false | |
| 47 | ||
| 48 | if (this.excludeNodeModules && !this.exclude.includes('**/node_modules/**')) | |
| 49 | this.exclude = this.exclude.concat('**/node_modules/**') | |
| 50 | ||
| 51 | this.exclude = prepGlobPatterns([].concat(this.exclude)) | |
| 52 | ||
| 53 | this.handleNegation() | |
| 54 | } | |
| 55 | ||
| 56 | /* handle the special case of negative globs | |
| 57 | * (!**foo/bar); we create a new this.excludeNegated set | |
| 58 | * of rules, which is applied after excludes and we | |
| 59 | * move excluded include rules into this.excludes. | |
| 60 | */ | |
| 61 | handleNegation() { | |
| 62 | const noNeg = (e) => e.charAt(0) !== '!' | |
| 63 | const onlyNeg = (e) => e.charAt(0) === '!' | |
| 64 | const stripNeg = (e) => e.slice(1) | |
| 65 | ||
| 66 | if (Array.isArray(this.include)) { | |
| 67 | const includeNegated = this.include.filter(onlyNeg).map(stripNeg) | |
| 68 | this.exclude.push(...prepGlobPatterns(includeNegated)) | |
| 69 | this.include = this.include.filter(noNeg) | |
| 70 | } | |
| 71 | ||
| 72 | this.excludeNegated = this.exclude.filter(onlyNeg).map(stripNeg) | |
| 73 | this.exclude = this.exclude.filter(noNeg) | |
| 74 | this.excludeNegated = prepGlobPatterns(this.excludeNegated) | |
| 75 | } | |
| 76 | ||
| 77 | shouldInstrument(filename, relFile) { | |
| 78 | if (this.extension && !this.extension.some((ext) => filename.endsWith(ext))) return false | |
| 79 | ||
| 80 | let pathToCheck = filename | |
| 81 | ||
| 82 | if (this.relativePath) { | |
| 83 | relFile = relFile || path.relative(this.cwd, filename) | |
| 84 | ||
| 85 | // Don't instrument files that are outside of the current working directory. | |
| 86 | if (isOutsideDir(this.cwd, filename)) return false | |
| 87 | ||
| 88 | pathToCheck = relFile.replace(/^\.[\\/]/, '') // remove leading './' or '.\'. | |
| 89 | } | |
| 90 | ||
| 91 | const dot = { dot: true, windows: isWindows } | |
| 92 | const matches = (pattern) => picomatch.isMatch(pathToCheck, pattern, dot) | |
| 93 | return ( | |
| 94 | (!this.include || this.include.some(matches)) && | |
| 95 | (!this.exclude.some(matches) || this.excludeNegated.some(matches)) | |
| 96 | ) | |
| 97 | } | |
| 98 | } | |
| 99 | ||
| 100 | function prepGlobPatterns(patterns) { | |
| 101 | return patterns.reduce((result, pattern) => { | |
| 102 | // Allow gitignore style of directory exclusion | |
| 103 | if (!/\/\*\*$/.test(pattern)) result = result.concat(pattern.replace(/\/$/, '') + '/**') | |
| 104 | ||
| 105 | // Any rules of the form **/foo.js, should also match foo.js. | |
| 106 | if (/^\*\*\//.test(pattern)) result = result.concat(pattern.replace(/^\*\*\//, '')) | |
| 107 | ||
| 108 | return result.concat(pattern) | |
| 109 | }, []) | |
| 110 | } | |
| 111 | ||
| 112 | module.exports = TestExclude |
| … | … | @@ -0,0 +1,66 @@ |
| 1 | 'use strict' | |
| 2 | ||
| 3 | const { isAbsolute } = require('path') | |
| 4 | const { fileURLToPath } = require('url') | |
| 5 | const v8ToIstanbul = require('bare-v8-to-istanbul') | |
| 6 | const TestExclude = require('./test-exclude') | |
| 7 | const summarize = require('./summarize') | |
| 8 | const report = require('./report') | |
| 9 | const merge = require('./merge') | |
| 10 | ||
| 11 | class Transformer { | |
| 12 | constructor(opts = {}) { | |
| 13 | this.includeRelative = opts.includeRelative ?? false | |
| 14 | this.exclude = new TestExclude({ cwd: opts.cwd }) | |
| 15 | this.includedUrlCache = new Map() | |
| 16 | } | |
| 17 | ||
| 18 | normalizeUrl(v8ReportResult) { | |
| 19 | if (/^node:/.test(v8ReportResult.url)) { | |
| 20 | v8ReportResult.url = `${v8ReportResult.url.replace(/^node:/, '')}.js` | |
| 21 | } | |
| 22 | ||
| 23 | if (/^file:\/\//.test(v8ReportResult.url)) { | |
| 24 | v8ReportResult.url = fileURLToPath(v8ReportResult.url) | |
| 25 | } | |
| 26 | ||
| 27 | return v8ReportResult | |
| 28 | } | |
| 29 | ||
| 30 | isResultUrlIncluded(url) { | |
| 31 | const cacheResult = this.includedUrlCache.get(url) | |
| 32 | if (cacheResult !== undefined) return cacheResult | |
| 33 | ||
| 34 | const result = (this.includeRelative || isAbsolute(url)) && this.exclude.shouldInstrument(url) | |
| 35 | this.includedUrlCache.set(url, result) | |
| 36 | return result | |
| 37 | } | |
| 38 | ||
| 39 | async transformToCoverageMap(rawV8Report) { | |
| 40 | const v8Report = { | |
| 41 | result: rawV8Report.result | |
| 42 | .map((v8ReportResult) => this.normalizeUrl(v8ReportResult)) | |
| 43 | .filter((v8ReportResult) => this.isResultUrlIncluded(v8ReportResult.url)) | |
| 44 | } | |
| 45 | ||
| 46 | const coverages = {} | |
| 47 | for (const v8ReportResult of v8Report.result) { | |
| 48 | const converter = v8ToIstanbul(v8ReportResult.url) | |
| 49 | await converter.load() | |
| 50 | converter.applyCoverage(v8ReportResult.functions) | |
| 51 | const converted = converter.toIstanbul() | |
| 52 | ||
| 53 | for (const [path, coverage] of Object.entries(converted)) { | |
| 54 | coverages[path] = coverages[path] ? merge(coverages[path], coverage) : coverage | |
| 55 | } | |
| 56 | } | |
| 57 | ||
| 58 | return coverages | |
| 59 | } | |
| 60 | ||
| 61 | report(coverage) { | |
| 62 | report(summarize(coverage)) | |
| 63 | } | |
| 64 | } | |
| 65 | ||
| 66 | module.exports = Transformer |
| … | … | @@ -0,0 +1,68 @@ |
| 1 | { | |
| 2 | "name": "bare-cov", | |
| 3 | "version": "1.2.1", | |
| 4 | "description": "Generate coverage reports using Bare and Node.js", | |
| 5 | "main": "index.js", | |
| 6 | "scripts": { | |
| 7 | "test": "node test", | |
| 8 | "test:bare": "bare test", | |
| 9 | "format": "prettier --write .", | |
| 10 | "format:check": "prettier --check ." | |
| 11 | }, | |
| 12 | "repository": { | |
| 13 | "type": "git", | |
| 14 | "url": "git+https://github.com/holepunchto/bare-cov.git" | |
| 15 | }, | |
| 16 | "keywords": [ | |
| 17 | "bare", | |
| 18 | "coverage", | |
| 19 | "node", | |
| 20 | "pear" | |
| 21 | ], | |
| 22 | "author": "Holepunch", | |
| 23 | "license": "Apache-2.0", | |
| 24 | "bugs": { | |
| 25 | "url": "https://github.com/holepunchto/bare-cov/issues" | |
| 26 | }, | |
| 27 | "homepage": "https://github.com/holepunchto/bare-cov#readme", | |
| 28 | "imports": { | |
| 29 | "fs": { | |
| 30 | "bare": "bare-fs" | |
| 31 | }, | |
| 32 | "path": { | |
| 33 | "bare": "bare-path" | |
| 34 | }, | |
| 35 | "process": { | |
| 36 | "bare": "bare-process" | |
| 37 | }, | |
| 38 | "url": { | |
| 39 | "bare": "bare-url" | |
| 40 | }, | |
| 41 | "inspector": { | |
| 42 | "bare": "bare-inspector" | |
| 43 | }, | |
| 44 | "child_process": { | |
| 45 | "bare": "bare-subprocess" | |
| 46 | } | |
| 47 | }, | |
| 48 | "dependencies": { | |
| 49 | "bare-fs": "^4.1.2", | |
| 50 | "bare-inspector": "^4.0.1", | |
| 51 | "bare-path": "^3.0.0", | |
| 52 | "bare-process": "^4.2.1", | |
| 53 | "bare-url": "^2.1.5", | |
| 54 | "bare-v8-to-istanbul": "^1.0.2", | |
| 55 | "picomatch": "^4.0.2", | |
| 56 | "which-runtime": "^1.2.1" | |
| 57 | }, | |
| 58 | "devDependencies": { | |
| 59 | "bare-subprocess": "^5.0.3", | |
| 60 | "brittle": "^3.13.1", | |
| 61 | "prettier": "^3.6.2", | |
| 62 | "prettier-config-holepunch": "^2.0.0" | |
| 63 | }, | |
| 64 | "files": [ | |
| 65 | "index.js", | |
| 66 | "lib" | |
| 67 | ] | |
| 68 | } |
| … | … | @@ -0,0 +1,35 @@ |
| 1 | cmake_minimum_required(VERSION 4.0) | |
| 2 | ||
| 3 | find_package(cmake-bare REQUIRED PATHS node_modules/cmake-bare) | |
| 4 | find_package(cmake-fetch REQUIRED PATHS node_modules/cmake-fetch) | |
| 5 | ||
| 6 | project(bare_crypto C CXX) | |
| 7 | ||
| 8 | bare_target(target) | |
| 9 | ||
| 10 | if(target MATCHES "win32") | |
| 11 | add_definitions(-DWIN32_LEAN_AND_MEAN) | |
| 12 | endif() | |
| 13 | ||
| 14 | if(target MATCHES "win32-x64") | |
| 15 | enable_language(ASM_NASM) | |
| 16 | else() | |
| 17 | enable_language(ASM) | |
| 18 | endif() | |
| 19 | ||
| 20 | fetch_package("github:google/boringssl#0.20260211.0") | |
| 21 | ||
| 22 | add_bare_module(bare_crypto) | |
| 23 | ||
| 24 | target_sources( | |
| 25 | ${bare_crypto} | |
| 26 | PRIVATE | |
| 27 | binding.c | |
| 28 | ) | |
| 29 | ||
| 30 | target_link_libraries( | |
| 31 | ${bare_crypto} | |
| 32 | PUBLIC | |
| 33 | ssl | |
| 34 | decrepit | |
| 35 | ) |
| … | … | @@ -0,0 +1,201 @@ |
| 1 | Apache License | |
| 2 | Version 2.0, January 2004 | |
| 3 | http://www.apache.org/licenses/ | |
| 4 | ||
| 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | |
| 6 | ||
| 7 | 1. Definitions. | |
| 8 | ||
| 9 | "License" shall mean the terms and conditions for use, reproduction, | |
| 10 | and distribution as defined by Sections 1 through 9 of this document. | |
| 11 | ||
| 12 | "Licensor" shall mean the copyright owner or entity authorized by | |
| 13 | the copyright owner that is granting the License. | |
| 14 | ||
| 15 | "Legal Entity" shall mean the union of the acting entity and all | |
| 16 | other entities that control, are controlled by, or are under common | |
| 17 | control with that entity. For the purposes of this definition, | |
| 18 | "control" means (i) the power, direct or indirect, to cause the | |
| 19 | direction or management of such entity, whether by contract or | |
| 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the | |
| 21 | outstanding shares, or (iii) beneficial ownership of such entity. | |
| 22 | ||
| 23 | "You" (or "Your") shall mean an individual or Legal Entity | |
| 24 | exercising permissions granted by this License. | |
| 25 | ||
| 26 | "Source" form shall mean the preferred form for making modifications, | |
| 27 | including but not limited to software source code, documentation | |
| 28 | source, and configuration files. | |
| 29 | ||
| 30 | "Object" form shall mean any form resulting from mechanical | |
| 31 | transformation or translation of a Source form, including but | |
| 32 | not limited to compiled object code, generated documentation, | |
| 33 | and conversions to other media types. | |
| 34 | ||
| 35 | "Work" shall mean the work of authorship, whether in Source or | |
| 36 | Object form, made available under the License, as indicated by a | |
| 37 | copyright notice that is included in or attached to the work | |
| 38 | (an example is provided in the Appendix below). | |
| 39 | ||
| 40 | "Derivative Works" shall mean any work, whether in Source or Object | |
| 41 | form, that is based on (or derived from) the Work and for which the | |
| 42 | editorial revisions, annotations, elaborations, or other modifications | |
| 43 | represent, as a whole, an original work of authorship. For the purposes | |
| 44 | of this License, Derivative Works shall not include works that remain | |
| 45 | separable from, or merely link (or bind by name) to the interfaces of, | |
| 46 | the Work and Derivative Works thereof. | |
| 47 | ||
| 48 | "Contribution" shall mean any work of authorship, including | |
| 49 | the original version of the Work and any modifications or additions | |
| 50 | to that Work or Derivative Works thereof, that is intentionally | |
| 51 | submitted to Licensor for inclusion in the Work by the copyright owner | |
| 52 | or by an individual or Legal Entity authorized to submit on behalf of | |
| 53 | the copyright owner. For the purposes of this definition, "submitted" | |
| 54 | means any form of electronic, verbal, or written communication sent | |
| 55 | to the Licensor or its representatives, including but not limited to | |
| 56 | communication on electronic mailing lists, source code control systems, | |
| 57 | and issue tracking systems that are managed by, or on behalf of, the | |
| 58 | Licensor for the purpose of discussing and improving the Work, but | |
| 59 | excluding communication that is conspicuously marked or otherwise | |
| 60 | designated in writing by the copyright owner as "Not a Contribution." | |
| 61 | ||
| 62 | "Contributor" shall mean Licensor and any individual or Legal Entity | |
| 63 | on behalf of whom a Contribution has been received by Licensor and | |
| 64 | subsequently incorporated within the Work. | |
| 65 | ||
| 66 | 2. Grant of Copyright License. Subject to the terms and conditions of | |
| 67 | this License, each Contributor hereby grants to You a perpetual, | |
| 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 69 | copyright license to reproduce, prepare Derivative Works of, | |
| 70 | publicly display, publicly perform, sublicense, and distribute the | |
| 71 | Work and such Derivative Works in Source or Object form. | |
| 72 | ||
| 73 | 3. Grant of Patent License. Subject to the terms and conditions of | |
| 74 | this License, each Contributor hereby grants to You a perpetual, | |
| 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 76 | (except as stated in this section) patent license to make, have made, | |
| 77 | use, offer to sell, sell, import, and otherwise transfer the Work, | |
| 78 | where such license applies only to those patent claims licensable | |
| 79 | by such Contributor that are necessarily infringed by their | |
| 80 | Contribution(s) alone or by combination of their Contribution(s) | |
| 81 | with the Work to which such Contribution(s) was submitted. If You | |
| 82 | institute patent litigation against any entity (including a | |
| 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work | |
| 84 | or a Contribution incorporated within the Work constitutes direct | |
| 85 | or contributory patent infringement, then any patent licenses | |
| 86 | granted to You under this License for that Work shall terminate | |
| 87 | as of the date such litigation is filed. | |
| 88 | ||
| 89 | 4. Redistribution. You may reproduce and distribute copies of the | |
| 90 | Work or Derivative Works thereof in any medium, with or without | |
| 91 | modifications, and in Source or Object form, provided that You | |
| 92 | meet the following conditions: | |
| 93 | ||
| 94 | (a) You must give any other recipients of the Work or | |
| 95 | Derivative Works a copy of this License; and | |
| 96 | ||
| 97 | (b) You must cause any modified files to carry prominent notices | |
| 98 | stating that You changed the files; and | |
| 99 | ||
| 100 | (c) You must retain, in the Source form of any Derivative Works | |
| 101 | that You distribute, all copyright, patent, trademark, and | |
| 102 | attribution notices from the Source form of the Work, | |
| 103 | excluding those notices that do not pertain to any part of | |
| 104 | the Derivative Works; and | |
| 105 | ||
| 106 | (d) If the Work includes a "NOTICE" text file as part of its | |
| 107 | distribution, then any Derivative Works that You distribute must | |
| 108 | include a readable copy of the attribution notices contained | |
| 109 | within such NOTICE file, excluding those notices that do not | |
| 110 | pertain to any part of the Derivative Works, in at least one | |
| 111 | of the following places: within a NOTICE text file distributed | |
| 112 | as part of the Derivative Works; within the Source form or | |
| 113 | documentation, if provided along with the Derivative Works; or, | |
| 114 | within a display generated by the Derivative Works, if and | |
| 115 | wherever such third-party notices normally appear. The contents | |
| 116 | of the NOTICE file are for informational purposes only and | |
| 117 | do not modify the License. You may add Your own attribution | |
| 118 | notices within Derivative Works that You distribute, alongside | |
| 119 | or as an addendum to the NOTICE text from the Work, provided | |
| 120 | that such additional attribution notices cannot be construed | |
| 121 | as modifying the License. | |
| 122 | ||
| 123 | You may add Your own copyright statement to Your modifications and | |
| 124 | may provide additional or different license terms and conditions | |
| 125 | for use, reproduction, or distribution of Your modifications, or | |
| 126 | for any such Derivative Works as a whole, provided Your use, | |
| 127 | reproduction, and distribution of the Work otherwise complies with | |
| 128 | the conditions stated in this License. | |
| 129 | ||
| 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, | |
| 131 | any Contribution intentionally submitted for inclusion in the Work | |
| 132 | by You to the Licensor shall be under the terms and conditions of | |
| 133 | this License, without any additional terms or conditions. | |
| 134 | Notwithstanding the above, nothing herein shall supersede or modify | |
| 135 | the terms of any separate license agreement you may have executed | |
| 136 | with Licensor regarding such Contributions. | |
| 137 | ||
| 138 | 6. Trademarks. This License does not grant permission to use the trade | |
| 139 | names, trademarks, service marks, or product names of the Licensor, | |
| 140 | except as required for reasonable and customary use in describing the | |
| 141 | origin of the Work and reproducing the content of the NOTICE file. | |
| 142 | ||
| 143 | 7. Disclaimer of Warranty. Unless required by applicable law or | |
| 144 | agreed to in writing, Licensor provides the Work (and each | |
| 145 | Contributor provides its Contributions) on an "AS IS" BASIS, | |
| 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |
| 147 | implied, including, without limitation, any warranties or conditions | |
| 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | |
| 149 | PARTICULAR PURPOSE. You are solely responsible for determining the | |
| 150 | appropriateness of using or redistributing the Work and assume any | |
| 151 | risks associated with Your exercise of permissions under this License. | |
| 152 | ||
| 153 | 8. Limitation of Liability. In no event and under no legal theory, | |
| 154 | whether in tort (including negligence), contract, or otherwise, | |
| 155 | unless required by applicable law (such as deliberate and grossly | |
| 156 | negligent acts) or agreed to in writing, shall any Contributor be | |
| 157 | liable to You for damages, including any direct, indirect, special, | |
| 158 | incidental, or consequential damages of any character arising as a | |
| 159 | result of this License or out of the use or inability to use the | |
| 160 | Work (including but not limited to damages for loss of goodwill, | |
| 161 | work stoppage, computer failure or malfunction, or any and all | |
| 162 | other commercial damages or losses), even if such Contributor | |
| 163 | has been advised of the possibility of such damages. | |
| 164 | ||
| 165 | 9. Accepting Warranty or Additional Liability. While redistributing | |
| 166 | the Work or Derivative Works thereof, You may choose to offer, | |
| 167 | and charge a fee for, acceptance of support, warranty, indemnity, | |
| 168 | or other liability obligations and/or rights consistent with this | |
| 169 | License. However, in accepting such obligations, You may act only | |
| 170 | on Your own behalf and on Your sole responsibility, not on behalf | |
| 171 | of any other Contributor, and only if You agree to indemnify, | |
| 172 | defend, and hold each Contributor harmless for any liability | |
| 173 | incurred by, or claims asserted against, such Contributor by reason | |
| 174 | of your accepting any such warranty or additional liability. | |
| 175 | ||
| 176 | END OF TERMS AND CONDITIONS | |
| 177 | ||
| 178 | APPENDIX: How to apply the Apache License to your work. | |
| 179 | ||
| 180 | To apply the Apache License to your work, attach the following | |
| 181 | boilerplate notice, with the fields enclosed by brackets "[]" | |
| 182 | replaced with your own identifying information. (Don't include | |
| 183 | the brackets!) The text should be enclosed in the appropriate | |
| 184 | comment syntax for the file format. We also recommend that a | |
| 185 | file or class name and description of purpose be included on the | |
| 186 | same "printed page" as the copyright notice for easier | |
| 187 | identification within third-party archives. | |
| 188 | ||
| 189 | Copyright [yyyy] [name of copyright owner] | |
| 190 | ||
| 191 | Licensed under the Apache License, Version 2.0 (the "License"); | |
| 192 | you may not use this file except in compliance with the License. | |
| 193 | You may obtain a copy of the License at | |
| 194 | ||
| 195 | http://www.apache.org/licenses/LICENSE-2.0 | |
| 196 | ||
| 197 | Unless required by applicable law or agreed to in writing, software | |
| 198 | distributed under the License is distributed on an "AS IS" BASIS, | |
| 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 200 | See the License for the specific language governing permissions and | |
| 201 | limitations under the License. |
| … | … | @@ -0,0 +1,98 @@ |
| 1 | # bare-crypto | |
| 2 | ||
| 3 | Cryptographic primitives for JavaScript. | |
| 4 | ||
| 5 | ``` | |
| 6 | npm i bare-crypto | |
| 7 | ``` | |
| 8 | ||
| 9 | ## Usage | |
| 10 | ||
| 11 | ```js | |
| 12 | const crypto = require('bare-crypto') | |
| 13 | ||
| 14 | const hash = crypto.createHash('sha256') | |
| 15 | ||
| 16 | hash.update('Hello, world!') | |
| 17 | ||
| 18 | const digest = hash.digest('hex') | |
| 19 | ||
| 20 | console.log(digest) | |
| 21 | ``` | |
| 22 | ||
| 23 | ## API | |
| 24 | ||
| 25 | #### `const hash = createHash(algorithm[, options])` | |
| 26 | ||
| 27 | Create a new `Hash` instance with the specified algorithm and options. The options are passed to [`new Transform()`](https://github.com/mafintosh/streamx#ts--new-streamtransformoptions). | |
| 28 | ||
| 29 | #### `const hmac = createHmac(algorithm, key[, options])` | |
| 30 | ||
| 31 | Create a new `Hmac` instance with the specified algorithm, key, and options. The options are passed to [`new Transform()`](https://github.com/mafintosh/streamx#ts--new-streamtransformoptions). | |
| 32 | ||
| 33 | #### `const cipher = createCipheriv(algorithm, key, iv[, options])` | |
| 34 | ||
| 35 | Create a new `Cipheriv` instance using the specified algorithm, key, and initialization vector (`iv`). The options are passed to [`new Transform()`](https://github.com/mafintosh/streamx#ts--new-streamtransformoptions). | |
| 36 | ||
| 37 | #### `const decipher = createDecipheriv(algorithm, key, iv[, options])` | |
| 38 | ||
| 39 | Create a new `Decipheriv` instance using the specified algorithm, key, and initialization vector (`iv`). The options are passed to [`new Transform()`](https://github.com/mafintosh/streamx#ts--new-streamtransformoptions). | |
| 40 | ||
| 41 | #### `const buffer = randomBytes(size)` | |
| 42 | ||
| 43 | Generate cryptographically secure random bytes. | |
| 44 | ||
| 45 | #### `randomBytes(size, callback)` | |
| 46 | ||
| 47 | Generate cryptographically secure random bytes. The callback signature is `callback(err, buffer)`. | |
| 48 | ||
| 49 | #### `buffer = randomFill(buffer[, offset][, size])` | |
| 50 | ||
| 51 | Fill a buffer with cryptographically secure random bytes. | |
| 52 | ||
| 53 | #### `randomFill(buffer[, offset][, size], callback)` | |
| 54 | ||
| 55 | Fill a buffer with cryptographically secure random bytes. The callback signature is `callback(err, buffer)` | |
| 56 | ||
| 57 | #### `const buffer = pbkdf2(password, salt, iterations, keylen, digest)` | |
| 58 | ||
| 59 | Derive a key from a password and salt using the specified digest algorithm and number of iterations. | |
| 60 | ||
| 61 | #### `pbkdf2(password, salt, iterations, keylen, digest, callback)` | |
| 62 | ||
| 63 | Derive a key from a password and salt using the specified digest algorithm and number of iterations. The callback signature is `callback(err, buffer)`. | |
| 64 | ||
| 65 | #### `constants.hash` | |
| 66 | ||
| 67 | The supported hash algorithms. | |
| 68 | ||
| 69 | | Constant | Description | | |
| 70 | | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | |
| 71 | | `MD5` | A widely-used 128-bit hash function, now considered insecure due to vulnerabilities to collision attacks. Still fast but not recommended for security-sensitive purposes. | | |
| 72 | | `SHA1` | A 160-bit hash function, stronger than MD5 but also broken by collision attacks. Deprecated for most cryptographic uses due to security vulnerabilities. | | |
| 73 | | `SHA256` | Part of the SHA-2 family, this 256-bit hash function is widely used and considered secure for most applications. Slower than MD5 and SHA1 but much more secure. | | |
| 74 | | `SHA512` | Another member of the SHA-2 family, this 512-bit hash function offers greater security than SHA256 but is slower and produces larger hashes. Suitable for high-security environments. | | |
| 75 | | `BLAKE2B256` | A fast, secure alternative to SHA-2 designed for efficiency, producing a 256-bit hash. It is optimized for performance while maintaining strong cryptographic security. | | |
| 76 | ||
| 77 | #### `constants.cipher` | |
| 78 | ||
| 79 | The supported symmetric cipher algorithms. | |
| 80 | ||
| 81 | | Constant | Description | | |
| 82 | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | |
| 83 | | `AES128ECB` | AES with a 128-bit key in ECB (Electronic Codebook) mode. Fast but insecure due to deterministic encryption of identical plaintext blocks. Not recommended. | | |
| 84 | | `AES128CBC` | AES with a 128-bit key in CBC (Cipher Block Chaining) mode. Provides better security than ECB by chaining blocks, but requires an IV and is slower. | | |
| 85 | | `AES128CTR` | AES with a 128-bit key in CTR (Counter) mode. A secure and parallelizable mode that turns a block cipher into a stream cipher. Requires a nonce/IV. | | |
| 86 | | `AES128OFB` | AES with a 128-bit key in OFB (Output Feedback) mode. Converts AES into a stream cipher; less common than CTR and more sensitive to IV reuse. | | |
| 87 | | `AES256ECB` | AES with a 256-bit key in ECB mode. Inherits the weaknesses of ECB; not suitable for encrypting more than a block at a time securely. | | |
| 88 | | `AES256CBC` | AES with a 256-bit key in CBC mode. Commonly used and reasonably secure with proper IV and padding management. | | |
| 89 | | `AES256CTR` | AES with a 256-bit key in CTR mode. Offers high performance and strong security if nonces are never reused. | | |
| 90 | | `AES256OFB` | AES with a 256-bit key in OFB mode. Like CTR, it turns AES into a stream cipher but with different feedback mechanics; less commonly used. | | |
| 91 | | `AES128GCM` | AES with a 128-bit key in GCM (Galois/Counter Mode). Provides authenticated encryption with associated data (AEAD). Fast and secure with proper nonce usage. | | |
| 92 | | `AES256GCM` | AES with a 256-bit key in GCM mode. Offers strong authenticated encryption; commonly used in TLS and secure messaging. | | |
| 93 | | `CHACHA20POLY1305` | A modern AEAD cipher combining the ChaCha20 stream cipher and Poly1305 MAC. Fast and secure, especially efficient on devices without AES hardware support. | | |
| 94 | | `XCHACHA20POLY1305` | An extended variant of ChaCha20-Poly1305 that supports longer nonces (192-bit). Improves nonce reuse resistance and is easier to use safely. | | |
| 95 | ||
| 96 | ## License | |
| 97 | ||
| 98 | Apache-2.0 |
| … | … | @@ -0,0 +1,1509 @@ |
| 1 | #include <assert.h> | |
| 2 | #include <bare.h> | |
| 3 | #include <js.h> | |
| 4 | #include <openssl/bytestring.h> | |
| 5 | #include <openssl/cipher.h> | |
| 6 | #include <openssl/curve25519.h> | |
| 7 | #include <openssl/digest.h> | |
| 8 | #include <openssl/evp.h> | |
| 9 | #include <openssl/hmac.h> | |
| 10 | #include <openssl/mem.h> | |
| 11 | #include <openssl/rand.h> | |
| 12 | #include <openssl/ripemd.h> | |
| 13 | #include <stddef.h> | |
| 14 | ||
| 15 | enum { | |
| 16 | // Digests | |
| 17 | bare_crypto_md5 = 1, | |
| 18 | bare_crypto_sha1, | |
| 19 | bare_crypto_sha256, | |
| 20 | bare_crypto_sha384, | |
| 21 | bare_crypto_sha512, | |
| 22 | bare_crypto_blake2b256, | |
| 23 | bare_crypto_ripemd160, | |
| 24 | ||
| 25 | // Unauthenticated ciphers | |
| 26 | bare_crypto_aes_128_ecb, | |
| 27 | bare_crypto_aes_128_cbc, | |
| 28 | bare_crypto_aes_128_ctr, | |
| 29 | bare_crypto_aes_128_ofb, | |
| 30 | bare_crypto_aes_256_ecb, | |
| 31 | bare_crypto_aes_256_cbc, | |
| 32 | bare_crypto_aes_256_ctr, | |
| 33 | bare_crypto_aes_256_ofb, | |
| 34 | ||
| 35 | // Authenticated ciphers | |
| 36 | bare_crypto_aes_128_gcm, | |
| 37 | bare_crypto_aes_256_gcm, | |
| 38 | bare_crypto_chacha20_poly1305, | |
| 39 | bare_crypto_xchacha20_poly1305, | |
| 40 | ||
| 41 | // Signature algorithms | |
| 42 | bare_crypto_ed25519, | |
| 43 | }; | |
| 44 | ||
| 45 | typedef struct { | |
| 46 | EVP_MD_CTX context; | |
| 47 | } bare_crypto_digest_t; | |
| 48 | ||
| 49 | typedef struct { | |
| 50 | RIPEMD160_CTX context; | |
| 51 | } bare_crypto_ripemd160_t; | |
| 52 | ||
| 53 | typedef struct { | |
| 54 | HMAC_CTX context; | |
| 55 | } bare_crypto_hmac_t; | |
| 56 | ||
| 57 | typedef struct { | |
| 58 | EVP_CIPHER_CTX context; | |
| 59 | } bare_crypto_cipher_t; | |
| 60 | ||
| 61 | typedef struct { | |
| 62 | EVP_AEAD_CTX context; | |
| 63 | } bare_crypto_aead_t; | |
| 64 | ||
| 65 | static inline const EVP_MD * | |
| 66 | bare_crypto__to_digest(js_env_t *env, int type) { | |
| 67 | int err; | |
| 68 | ||
| 69 | switch (type) { | |
| 70 | #define V(algorithm) \ | |
| 71 | case bare_crypto_##algorithm: \ | |
| 72 | return EVP_##algorithm(); | |
| 73 | ||
| 74 | V(md5) | |
| 75 | V(sha1) | |
| 76 | V(sha256) | |
| 77 | V(sha384) | |
| 78 | V(sha512) | |
| 79 | V(blake2b256) | |
| 80 | #undef V | |
| 81 | ||
| 82 | default: | |
| 83 | err = js_throw_error(env, NULL, "Unknown digest algorithm"); | |
| 84 | assert(err == 0); | |
| 85 | ||
| 86 | return NULL; | |
| 87 | } | |
| 88 | } | |
| 89 | ||
| 90 | static inline const EVP_CIPHER * | |
| 91 | bare_crypto__to_cipher(js_env_t *env, int type) { | |
| 92 | int err; | |
| 93 | ||
| 94 | switch (type) { | |
| 95 | #define V(algorithm) \ | |
| 96 | case bare_crypto_##algorithm: \ | |
| 97 | return EVP_##algorithm(); | |
| 98 | ||
| 99 | V(aes_128_ecb) | |
| 100 | V(aes_128_cbc) | |
| 101 | V(aes_128_ctr) | |
| 102 | V(aes_128_ofb) | |
| 103 | V(aes_256_ecb) | |
| 104 | V(aes_256_cbc) | |
| 105 | V(aes_256_ctr) | |
| 106 | V(aes_256_ofb) | |
| 107 | #undef V | |
| 108 | ||
| 109 | default: | |
| 110 | err = js_throw_error(env, NULL, "Unknown cipher algorithm"); | |
| 111 | assert(err == 0); | |
| 112 | ||
| 113 | return NULL; | |
| 114 | } | |
| 115 | } | |
| 116 | ||
| 117 | static inline const EVP_AEAD * | |
| 118 | bare_crypto__to_aead(js_env_t *env, int type) { | |
| 119 | int err; | |
| 120 | ||
| 121 | switch (type) { | |
| 122 | #define V(algorithm) \ | |
| 123 | case bare_crypto_##algorithm: \ | |
| 124 | return EVP_aead_##algorithm(); | |
| 125 | ||
| 126 | V(aes_128_gcm) | |
| 127 | V(aes_256_gcm) | |
| 128 | V(chacha20_poly1305) | |
| 129 | V(xchacha20_poly1305) | |
| 130 | #undef V | |
| 131 | ||
| 132 | default: | |
| 133 | err = js_throw_error(env, NULL, "Unknown cipher algorithm"); | |
| 134 | assert(err == 0); | |
| 135 | ||
| 136 | return NULL; | |
| 137 | } | |
| 138 | } | |
| 139 | ||
| 140 | static js_value_t * | |
| 141 | bare_crypto_digest_init(js_env_t *env, js_callback_info_t *info) { | |
| 142 | int err; | |
| 143 | ||
| 144 | size_t argc = 1; | |
| 145 | js_value_t *argv[1]; | |
| 146 | ||
| 147 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 148 | assert(err == 0); | |
| 149 | ||
| 150 | assert(argc == 1); | |
| 151 | ||
| 152 | uint32_t type; | |
| 153 | err = js_get_value_uint32(env, argv[0], &type); | |
| 154 | assert(err == 0); | |
| 155 | ||
| 156 | js_value_t *handle; | |
| 157 | ||
| 158 | bare_crypto_digest_t *digest; | |
| 159 | err = js_create_arraybuffer(env, sizeof(bare_crypto_digest_t), (void **) &digest, &handle); | |
| 160 | assert(err == 0); | |
| 161 | ||
| 162 | const EVP_MD *algorithm = bare_crypto__to_digest(env, type); | |
| 163 | ||
| 164 | if (algorithm == NULL) return NULL; | |
| 165 | ||
| 166 | EVP_MD_CTX_init(&digest->context); | |
| 167 | ||
| 168 | err = EVP_DigestInit(&digest->context, algorithm); | |
| 169 | assert(err == 1); | |
| 170 | ||
| 171 | return handle; | |
| 172 | } | |
| 173 | ||
| 174 | static js_value_t * | |
| 175 | bare_crypto_digest_update(js_env_t *env, js_callback_info_t *info) { | |
| 176 | int err; | |
| 177 | ||
| 178 | size_t argc = 4; | |
| 179 | js_value_t *argv[4]; | |
| 180 | ||
| 181 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 182 | assert(err == 0); | |
| 183 | ||
| 184 | assert(argc == 4); | |
| 185 | ||
| 186 | bare_crypto_digest_t *digest; | |
| 187 | err = js_get_arraybuffer_info(env, argv[0], (void **) &digest, NULL); | |
| 188 | assert(err == 0); | |
| 189 | ||
| 190 | void *data; | |
| 191 | err = js_get_arraybuffer_info(env, argv[1], &data, NULL); | |
| 192 | assert(err == 0); | |
| 193 | ||
| 194 | int64_t offset; | |
| 195 | err = js_get_value_int64(env, argv[2], &offset); | |
| 196 | assert(err == 0); | |
| 197 | ||
| 198 | int64_t len; | |
| 199 | err = js_get_value_int64(env, argv[3], &len); | |
| 200 | assert(err == 0); | |
| 201 | ||
| 202 | err = EVP_DigestUpdate(&digest->context, &data[offset], len); | |
| 203 | assert(err == 1); | |
| 204 | ||
| 205 | return NULL; | |
| 206 | } | |
| 207 | ||
| 208 | static js_value_t * | |
| 209 | bare_crypto_digest_final(js_env_t *env, js_callback_info_t *info) { | |
| 210 | int err; | |
| 211 | ||
| 212 | size_t argc = 1; | |
| 213 | js_value_t *argv[1]; | |
| 214 | ||
| 215 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 216 | assert(err == 0); | |
| 217 | ||
| 218 | assert(argc == 1); | |
| 219 | ||
| 220 | bare_crypto_digest_t *digest; | |
| 221 | err = js_get_arraybuffer_info(env, argv[0], (void **) &digest, NULL); | |
| 222 | assert(err == 0); | |
| 223 | ||
| 224 | js_value_t *result; | |
| 225 | ||
| 226 | size_t len = EVP_MD_CTX_size(&digest->context); | |
| 227 | ||
| 228 | uint8_t *out; | |
| 229 | err = js_create_arraybuffer(env, len, (void **) &out, &result); | |
| 230 | assert(err == 0); | |
| 231 | ||
| 232 | err = EVP_DigestFinal(&digest->context, out, NULL); | |
| 233 | assert(err == 1); | |
| 234 | ||
| 235 | return result; | |
| 236 | } | |
| 237 | ||
| 238 | static js_value_t * | |
| 239 | bare_crypto_ripemd160_init(js_env_t *env, js_callback_info_t *info) { | |
| 240 | int err; | |
| 241 | ||
| 242 | js_value_t *handle; | |
| 243 | ||
| 244 | bare_crypto_ripemd160_t *ripemd160; | |
| 245 | err = js_create_arraybuffer(env, sizeof(bare_crypto_ripemd160_t), (void **) &ripemd160, &handle); | |
| 246 | assert(err == 0); | |
| 247 | ||
| 248 | RIPEMD160_Init(&ripemd160->context); | |
| 249 | ||
| 250 | return handle; | |
| 251 | } | |
| 252 | ||
| 253 | static js_value_t * | |
| 254 | bare_crypto_ripemd160_update(js_env_t *env, js_callback_info_t *info) { | |
| 255 | int err; | |
| 256 | ||
| 257 | size_t argc = 4; | |
| 258 | js_value_t *argv[4]; | |
| 259 | ||
| 260 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 261 | assert(err == 0); | |
| 262 | ||
| 263 | assert(argc == 4); | |
| 264 | ||
| 265 | bare_crypto_ripemd160_t *ripemd160; | |
| 266 | err = js_get_arraybuffer_info(env, argv[0], (void **) &ripemd160, NULL); | |
| 267 | assert(err == 0); | |
| 268 | ||
| 269 | void *data; | |
| 270 | err = js_get_arraybuffer_info(env, argv[1], &data, NULL); | |
| 271 | assert(err == 0); | |
| 272 | ||
| 273 | int64_t offset; | |
| 274 | err = js_get_value_int64(env, argv[2], &offset); | |
| 275 | assert(err == 0); | |
| 276 | ||
| 277 | int64_t len; | |
| 278 | err = js_get_value_int64(env, argv[3], &len); | |
| 279 | assert(err == 0); | |
| 280 | ||
| 281 | err = RIPEMD160_Update(&ripemd160->context, &data[offset], len); | |
| 282 | assert(err == 1); | |
| 283 | ||
| 284 | return NULL; | |
| 285 | } | |
| 286 | ||
| 287 | static js_value_t * | |
| 288 | bare_crypto_ripemd160_final(js_env_t *env, js_callback_info_t *info) { | |
| 289 | int err; | |
| 290 | ||
| 291 | size_t argc = 1; | |
| 292 | js_value_t *argv[1]; | |
| 293 | ||
| 294 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 295 | assert(err == 0); | |
| 296 | ||
| 297 | assert(argc == 1); | |
| 298 | ||
| 299 | bare_crypto_ripemd160_t *ripemd160; | |
| 300 | err = js_get_arraybuffer_info(env, argv[0], (void **) &ripemd160, NULL); | |
| 301 | assert(err == 0); | |
| 302 | ||
| 303 | js_value_t *result; | |
| 304 | ||
| 305 | uint8_t *out; | |
| 306 | err = js_create_arraybuffer(env, RIPEMD160_DIGEST_LENGTH, (void **) &out, &result); | |
| 307 | assert(err == 0); | |
| 308 | ||
| 309 | err = RIPEMD160_Final(out, &ripemd160->context); | |
| 310 | assert(err == 1); | |
| 311 | ||
| 312 | return result; | |
| 313 | } | |
| 314 | ||
| 315 | static js_value_t * | |
| 316 | bare_crypto_hmac_init(js_env_t *env, js_callback_info_t *info) { | |
| 317 | int err; | |
| 318 | ||
| 319 | size_t argc = 4; | |
| 320 | js_value_t *argv[4]; | |
| 321 | ||
| 322 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 323 | assert(err == 0); | |
| 324 | ||
| 325 | assert(argc == 4); | |
| 326 | ||
| 327 | uint32_t type; | |
| 328 | err = js_get_value_uint32(env, argv[0], &type); | |
| 329 | assert(err == 0); | |
| 330 | ||
| 331 | char *key; | |
| 332 | err = js_get_arraybuffer_info(env, argv[1], (void **) &key, NULL); | |
| 333 | assert(err == 0); | |
| 334 | ||
| 335 | int64_t offset; | |
| 336 | err = js_get_value_int64(env, argv[2], &offset); | |
| 337 | assert(err == 0); | |
| 338 | ||
| 339 | int64_t len; | |
| 340 | err = js_get_value_int64(env, argv[3], &len); | |
| 341 | assert(err == 0); | |
| 342 | ||
| 343 | js_value_t *handle; | |
| 344 | ||
| 345 | bare_crypto_hmac_t *hmac; | |
| 346 | err = js_create_arraybuffer(env, sizeof(bare_crypto_hmac_t), (void **) &hmac, &handle); | |
| 347 | assert(err == 0); | |
| 348 | ||
| 349 | const EVP_MD *algorithm = bare_crypto__to_digest(env, type); | |
| 350 | ||
| 351 | if (algorithm == NULL) return NULL; | |
| 352 | ||
| 353 | HMAC_CTX_init(&hmac->context); | |
| 354 | ||
| 355 | err = HMAC_Init_ex(&hmac->context, &key[offset], len, algorithm, NULL); | |
| 356 | assert(err == 1); | |
| 357 | ||
| 358 | return handle; | |
| 359 | } | |
| 360 | ||
| 361 | static js_value_t * | |
| 362 | bare_crypto_hmac_update(js_env_t *env, js_callback_info_t *info) { | |
| 363 | int err; | |
| 364 | ||
| 365 | size_t argc = 4; | |
| 366 | js_value_t *argv[4]; | |
| 367 | ||
| 368 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 369 | assert(err == 0); | |
| 370 | ||
| 371 | assert(argc == 4); | |
| 372 | ||
| 373 | bare_crypto_hmac_t *hmac; | |
| 374 | err = js_get_arraybuffer_info(env, argv[0], (void **) &hmac, NULL); | |
| 375 | assert(err == 0); | |
| 376 | ||
| 377 | void *data; | |
| 378 | err = js_get_arraybuffer_info(env, argv[1], &data, NULL); | |
| 379 | assert(err == 0); | |
| 380 | ||
| 381 | int64_t offset; | |
| 382 | err = js_get_value_int64(env, argv[2], &offset); | |
| 383 | assert(err == 0); | |
| 384 | ||
| 385 | int64_t len; | |
| 386 | err = js_get_value_int64(env, argv[3], &len); | |
| 387 | assert(err == 0); | |
| 388 | ||
| 389 | err = HMAC_Update(&hmac->context, &data[offset], len); | |
| 390 | assert(err == 1); | |
| 391 | ||
| 392 | return NULL; | |
| 393 | } | |
| 394 | ||
| 395 | static js_value_t * | |
| 396 | bare_crypto_hmac_final(js_env_t *env, js_callback_info_t *info) { | |
| 397 | int err; | |
| 398 | ||
| 399 | size_t argc = 1; | |
| 400 | js_value_t *argv[1]; | |
| 401 | ||
| 402 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 403 | assert(err == 0); | |
| 404 | ||
| 405 | assert(argc == 1); | |
| 406 | ||
| 407 | bare_crypto_hmac_t *hmac; | |
| 408 | err = js_get_arraybuffer_info(env, argv[0], (void **) &hmac, NULL); | |
| 409 | assert(err == 0); | |
| 410 | ||
| 411 | js_value_t *result; | |
| 412 | ||
| 413 | size_t len = HMAC_size(&hmac->context); | |
| 414 | ||
| 415 | uint8_t *digest; | |
| 416 | err = js_create_arraybuffer(env, len, (void **) &digest, &result); | |
| 417 | assert(err == 0); | |
| 418 | ||
| 419 | err = HMAC_Final(&hmac->context, digest, NULL); | |
| 420 | assert(err == 1); | |
| 421 | ||
| 422 | return result; | |
| 423 | } | |
| 424 | ||
| 425 | static js_value_t * | |
| 426 | bare_crypto_cipher_key_length(js_env_t *env, js_callback_info_t *info) { | |
| 427 | int err; | |
| 428 | ||
| 429 | size_t argc = 1; | |
| 430 | js_value_t *argv[1]; | |
| 431 | ||
| 432 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 433 | assert(err == 0); | |
| 434 | ||
| 435 | assert(argc == 1); | |
| 436 | ||
| 437 | uint32_t type; | |
| 438 | err = js_get_value_uint32(env, argv[0], &type); | |
| 439 | assert(err == 0); | |
| 440 | ||
| 441 | const EVP_CIPHER *algorithm = bare_crypto__to_cipher(env, type); | |
| 442 | ||
| 443 | if (algorithm == NULL) return NULL; | |
| 444 | ||
| 445 | js_value_t *result; | |
| 446 | err = js_create_uint32(env, EVP_CIPHER_key_length(algorithm), &result); | |
| 447 | assert(err == 0); | |
| 448 | ||
| 449 | return result; | |
| 450 | } | |
| 451 | ||
| 452 | static js_value_t * | |
| 453 | bare_crypto_cipher_iv_length(js_env_t *env, js_callback_info_t *info) { | |
| 454 | int err; | |
| 455 | ||
| 456 | size_t argc = 1; | |
| 457 | js_value_t *argv[1]; | |
| 458 | ||
| 459 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 460 | assert(err == 0); | |
| 461 | ||
| 462 | assert(argc == 1); | |
| 463 | ||
| 464 | uint32_t type; | |
| 465 | err = js_get_value_uint32(env, argv[0], &type); | |
| 466 | assert(err == 0); | |
| 467 | ||
| 468 | const EVP_CIPHER *algorithm = bare_crypto__to_cipher(env, type); | |
| 469 | ||
| 470 | if (algorithm == NULL) return NULL; | |
| 471 | ||
| 472 | js_value_t *result; | |
| 473 | err = js_create_uint32(env, EVP_CIPHER_iv_length(algorithm), &result); | |
| 474 | assert(err == 0); | |
| 475 | ||
| 476 | return result; | |
| 477 | } | |
| 478 | ||
| 479 | static js_value_t * | |
| 480 | bare_crypto_cipher_block_size(js_env_t *env, js_callback_info_t *info) { | |
| 481 | int err; | |
| 482 | ||
| 483 | size_t argc = 1; | |
| 484 | js_value_t *argv[1]; | |
| 485 | ||
| 486 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 487 | assert(err == 0); | |
| 488 | ||
| 489 | assert(argc == 1); | |
| 490 | ||
| 491 | bare_crypto_cipher_t *cipher; | |
| 492 | err = js_get_arraybuffer_info(env, argv[0], (void **) &cipher, NULL); | |
| 493 | assert(err == 0); | |
| 494 | ||
| 495 | js_value_t *result; | |
| 496 | err = js_create_uint32(env, EVP_CIPHER_CTX_block_size(&cipher->context), &result); | |
| 497 | assert(err == 0); | |
| 498 | ||
| 499 | return result; | |
| 500 | } | |
| 501 | ||
| 502 | static js_value_t * | |
| 503 | bare_crypto_cipher_init(js_env_t *env, js_callback_info_t *info) { | |
| 504 | int err; | |
| 505 | ||
| 506 | size_t argc = 8; | |
| 507 | js_value_t *argv[8]; | |
| 508 | ||
| 509 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 510 | assert(err == 0); | |
| 511 | ||
| 512 | assert(argc == 8); | |
| 513 | ||
| 514 | uint32_t type; | |
| 515 | err = js_get_value_uint32(env, argv[0], &type); | |
| 516 | assert(err == 0); | |
| 517 | ||
| 518 | uint8_t *key; | |
| 519 | err = js_get_arraybuffer_info(env, argv[1], (void **) &key, NULL); | |
| 520 | assert(err == 0); | |
| 521 | ||
| 522 | int64_t key_offset; | |
| 523 | err = js_get_value_int64(env, argv[2], &key_offset); | |
| 524 | assert(err == 0); | |
| 525 | ||
| 526 | int64_t key_len; | |
| 527 | err = js_get_value_int64(env, argv[3], &key_len); | |
| 528 | assert(err == 0); | |
| 529 | ||
| 530 | uint8_t *iv; | |
| 531 | err = js_get_arraybuffer_info(env, argv[4], (void **) &iv, NULL); | |
| 532 | assert(err == 0); | |
| 533 | ||
| 534 | int64_t iv_offset; | |
| 535 | err = js_get_value_int64(env, argv[5], &iv_offset); | |
| 536 | assert(err == 0); | |
| 537 | ||
| 538 | int64_t iv_len; | |
| 539 | err = js_get_value_int64(env, argv[6], &iv_len); | |
| 540 | assert(err == 0); | |
| 541 | ||
| 542 | bool encrypt; | |
| 543 | err = js_get_value_bool(env, argv[7], &encrypt); | |
| 544 | assert(err == 0); | |
| 545 | ||
| 546 | js_value_t *handle; | |
| 547 | ||
| 548 | bare_crypto_cipher_t *cipher; | |
| 549 | err = js_create_arraybuffer(env, sizeof(bare_crypto_cipher_t), (void **) &cipher, &handle); | |
| 550 | assert(err == 0); | |
| 551 | ||
| 552 | const EVP_CIPHER *algorithm = bare_crypto__to_cipher(env, type); | |
| 553 | ||
| 554 | if (algorithm == NULL) return NULL; | |
| 555 | ||
| 556 | err = EVP_CipherInit(&cipher->context, algorithm, &key[key_offset], &iv[iv_offset], encrypt); | |
| 557 | assert(err == 1); | |
| 558 | ||
| 559 | return handle; | |
| 560 | } | |
| 561 | ||
| 562 | static js_value_t * | |
| 563 | bare_crypto_cipher_update(js_env_t *env, js_callback_info_t *info) { | |
| 564 | int err; | |
| 565 | ||
| 566 | size_t argc = 5; | |
| 567 | js_value_t *argv[5]; | |
| 568 | ||
| 569 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 570 | assert(err == 0); | |
| 571 | ||
| 572 | assert(argc == 5); | |
| 573 | ||
| 574 | bare_crypto_cipher_t *cipher; | |
| 575 | err = js_get_arraybuffer_info(env, argv[0], (void **) &cipher, NULL); | |
| 576 | assert(err == 0); | |
| 577 | ||
| 578 | uint8_t *data; | |
| 579 | err = js_get_arraybuffer_info(env, argv[1], (void **) &data, NULL); | |
| 580 | assert(err == 0); | |
| 581 | ||
| 582 | int64_t offset; | |
| 583 | err = js_get_value_int64(env, argv[2], &offset); | |
| 584 | assert(err == 0); | |
| 585 | ||
| 586 | int64_t len; | |
| 587 | err = js_get_value_int64(env, argv[3], &len); | |
| 588 | assert(err == 0); | |
| 589 | ||
| 590 | uint8_t *out; | |
| 591 | err = js_get_arraybuffer_info(env, argv[4], (void **) &out, NULL); | |
| 592 | assert(err == 0); | |
| 593 | ||
| 594 | int written; | |
| 595 | err = EVP_CipherUpdate(&cipher->context, out, &written, &data[offset], len); | |
| 596 | ||
| 597 | if (err != 1) { | |
| 598 | err = js_throw_error(env, NULL, "Cipher update failed"); | |
| 599 | assert(err == 0); | |
| 600 | ||
| 601 | return NULL; | |
| 602 | } | |
| 603 | ||
| 604 | js_value_t *result; | |
| 605 | err = js_create_int32(env, written, &result); | |
| 606 | assert(err == 0); | |
| 607 | ||
| 608 | return result; | |
| 609 | } | |
| 610 | ||
| 611 | static js_value_t * | |
| 612 | bare_crypto_cipher_final(js_env_t *env, js_callback_info_t *info) { | |
| 613 | int err; | |
| 614 | ||
| 615 | size_t argc = 2; | |
| 616 | js_value_t *argv[2]; | |
| 617 | ||
| 618 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 619 | assert(err == 0); | |
| 620 | ||
| 621 | assert(argc == 2); | |
| 622 | ||
| 623 | bare_crypto_cipher_t *cipher; | |
| 624 | err = js_get_arraybuffer_info(env, argv[0], (void **) &cipher, NULL); | |
| 625 | assert(err == 0); | |
| 626 | ||
| 627 | uint8_t *out; | |
| 628 | err = js_get_arraybuffer_info(env, argv[1], (void **) &out, NULL); | |
| 629 | assert(err == 0); | |
| 630 | ||
| 631 | int written; | |
| 632 | err = EVP_CipherFinal(&cipher->context, out, &written); | |
| 633 | ||
| 634 | EVP_CIPHER_CTX_cleanup(&cipher->context); | |
| 635 | ||
| 636 | if (err != 1) { | |
| 637 | err = js_throw_error(env, NULL, "Cipher finalisation failed"); | |
| 638 | assert(err == 0); | |
| 639 | ||
| 640 | return NULL; | |
| 641 | } | |
| 642 | ||
| 643 | js_value_t *result; | |
| 644 | err = js_create_int32(env, written, &result); | |
| 645 | assert(err == 0); | |
| 646 | ||
| 647 | return result; | |
| 648 | } | |
| 649 | ||
| 650 | static js_value_t * | |
| 651 | bare_crypto_cipher_set_padding(js_env_t *env, js_callback_info_t *info) { | |
| 652 | int err; | |
| 653 | ||
| 654 | size_t argc = 2; | |
| 655 | js_value_t *argv[2]; | |
| 656 | ||
| 657 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 658 | assert(err == 0); | |
| 659 | ||
| 660 | assert(argc == 2); | |
| 661 | ||
| 662 | bare_crypto_cipher_t *cipher; | |
| 663 | err = js_get_arraybuffer_info(env, argv[0], (void **) &cipher, NULL); | |
| 664 | assert(err == 0); | |
| 665 | ||
| 666 | bool pad; | |
| 667 | err = js_get_value_bool(env, argv[1], &pad); | |
| 668 | assert(err == 0); | |
| 669 | ||
| 670 | err = EVP_CIPHER_CTX_set_padding(&cipher->context, pad); | |
| 671 | assert(err == 1); | |
| 672 | ||
| 673 | return NULL; | |
| 674 | } | |
| 675 | ||
| 676 | static js_value_t * | |
| 677 | bare_crypto_aead_key_length(js_env_t *env, js_callback_info_t *info) { | |
| 678 | int err; | |
| 679 | ||
| 680 | size_t argc = 1; | |
| 681 | js_value_t *argv[1]; | |
| 682 | ||
| 683 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 684 | assert(err == 0); | |
| 685 | ||
| 686 | assert(argc == 1); | |
| 687 | ||
| 688 | uint32_t type; | |
| 689 | err = js_get_value_uint32(env, argv[0], &type); | |
| 690 | assert(err == 0); | |
| 691 | ||
| 692 | const EVP_AEAD *algorithm = bare_crypto__to_aead(env, type); | |
| 693 | ||
| 694 | if (algorithm == NULL) return NULL; | |
| 695 | ||
| 696 | js_value_t *result; | |
| 697 | err = js_create_int64(env, EVP_AEAD_key_length(algorithm), &result); | |
| 698 | assert(err == 0); | |
| 699 | ||
| 700 | return result; | |
| 701 | } | |
| 702 | ||
| 703 | static js_value_t * | |
| 704 | bare_crypto_aead_nonce_length(js_env_t *env, js_callback_info_t *info) { | |
| 705 | int err; | |
| 706 | ||
| 707 | size_t argc = 1; | |
| 708 | js_value_t *argv[1]; | |
| 709 | ||
| 710 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 711 | assert(err == 0); | |
| 712 | ||
| 713 | assert(argc == 1); | |
| 714 | ||
| 715 | uint32_t type; | |
| 716 | err = js_get_value_uint32(env, argv[0], &type); | |
| 717 | assert(err == 0); | |
| 718 | ||
| 719 | const EVP_AEAD *algorithm = bare_crypto__to_aead(env, type); | |
| 720 | ||
| 721 | if (algorithm == NULL) return NULL; | |
| 722 | ||
| 723 | js_value_t *result; | |
| 724 | err = js_create_int64(env, EVP_AEAD_nonce_length(algorithm), &result); | |
| 725 | assert(err == 0); | |
| 726 | ||
| 727 | return result; | |
| 728 | } | |
| 729 | ||
| 730 | static js_value_t * | |
| 731 | bare_crypto_aead_max_overhead(js_env_t *env, js_callback_info_t *info) { | |
| 732 | int err; | |
| 733 | ||
| 734 | size_t argc = 1; | |
| 735 | js_value_t *argv[1]; | |
| 736 | ||
| 737 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 738 | assert(err == 0); | |
| 739 | ||
| 740 | assert(argc == 1); | |
| 741 | ||
| 742 | bare_crypto_aead_t *aead; | |
| 743 | err = js_get_arraybuffer_info(env, argv[0], (void **) &aead, NULL); | |
| 744 | assert(err == 0); | |
| 745 | ||
| 746 | js_value_t *result; | |
| 747 | err = js_create_int64(env, EVP_AEAD_max_overhead(EVP_AEAD_CTX_aead(&aead->context)), &result); | |
| 748 | assert(err == 0); | |
| 749 | ||
| 750 | return result; | |
| 751 | } | |
| 752 | ||
| 753 | static js_value_t * | |
| 754 | bare_crypto_aead_max_tag_length(js_env_t *env, js_callback_info_t *info) { | |
| 755 | int err; | |
| 756 | ||
| 757 | size_t argc = 1; | |
| 758 | js_value_t *argv[1]; | |
| 759 | ||
| 760 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 761 | assert(err == 0); | |
| 762 | ||
| 763 | assert(argc == 1); | |
| 764 | ||
| 765 | bare_crypto_aead_t *aead; | |
| 766 | err = js_get_arraybuffer_info(env, argv[0], (void **) &aead, NULL); | |
| 767 | assert(err == 0); | |
| 768 | ||
| 769 | js_value_t *result; | |
| 770 | err = js_create_int64(env, EVP_AEAD_max_tag_len(EVP_AEAD_CTX_aead(&aead->context)), &result); | |
| 771 | assert(err == 0); | |
| 772 | ||
| 773 | return result; | |
| 774 | } | |
| 775 | ||
| 776 | static js_value_t * | |
| 777 | bare_crypto_aead_init(js_env_t *env, js_callback_info_t *info) { | |
| 778 | int err; | |
| 779 | ||
| 780 | size_t argc = 5; | |
| 781 | js_value_t *argv[5]; | |
| 782 | ||
| 783 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 784 | assert(err == 0); | |
| 785 | ||
| 786 | assert(argc == 5); | |
| 787 | ||
| 788 | uint32_t type; | |
| 789 | err = js_get_value_uint32(env, argv[0], &type); | |
| 790 | assert(err == 0); | |
| 791 | ||
| 792 | uint8_t *key; | |
| 793 | err = js_get_arraybuffer_info(env, argv[1], (void **) &key, NULL); | |
| 794 | assert(err == 0); | |
| 795 | ||
| 796 | int64_t key_offset; | |
| 797 | err = js_get_value_int64(env, argv[2], &key_offset); | |
| 798 | assert(err == 0); | |
| 799 | ||
| 800 | int64_t key_len; | |
| 801 | err = js_get_value_int64(env, argv[3], &key_len); | |
| 802 | assert(err == 0); | |
| 803 | ||
| 804 | int64_t tag_len; | |
| 805 | err = js_get_value_int64(env, argv[4], &tag_len); | |
| 806 | assert(err == 0); | |
| 807 | ||
| 808 | js_value_t *handle; | |
| 809 | ||
| 810 | bare_crypto_aead_t *aead; | |
| 811 | err = js_create_arraybuffer(env, sizeof(bare_crypto_aead_t), (void **) &aead, &handle); | |
| 812 | assert(err == 0); | |
| 813 | ||
| 814 | const EVP_AEAD *algorithm = bare_crypto__to_aead(env, type); | |
| 815 | ||
| 816 | if (algorithm == NULL) return NULL; | |
| 817 | ||
| 818 | err = EVP_AEAD_CTX_init(&aead->context, algorithm, &key[key_offset], key_len, tag_len, NULL); | |
| 819 | assert(err == 1); | |
| 820 | ||
| 821 | return handle; | |
| 822 | } | |
| 823 | ||
| 824 | static js_value_t * | |
| 825 | bare_crypto_aead_seal(js_env_t *env, js_callback_info_t *info) { | |
| 826 | int err; | |
| 827 | ||
| 828 | size_t argc = 11; | |
| 829 | js_value_t *argv[11]; | |
| 830 | ||
| 831 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 832 | assert(err == 0); | |
| 833 | ||
| 834 | assert(argc == 11); | |
| 835 | ||
| 836 | bare_crypto_aead_t *aead; | |
| 837 | err = js_get_arraybuffer_info(env, argv[0], (void **) &aead, NULL); | |
| 838 | assert(err == 0); | |
| 839 | ||
| 840 | uint8_t *data; | |
| 841 | err = js_get_arraybuffer_info(env, argv[1], (void **) &data, NULL); | |
| 842 | assert(err == 0); | |
| 843 | ||
| 844 | int64_t data_offset; | |
| 845 | err = js_get_value_int64(env, argv[2], &data_offset); | |
| 846 | assert(err == 0); | |
| 847 | ||
| 848 | int64_t data_len; | |
| 849 | err = js_get_value_int64(env, argv[3], &data_len); | |
| 850 | assert(err == 0); | |
| 851 | ||
| 852 | uint8_t *nonce; | |
| 853 | err = js_get_arraybuffer_info(env, argv[4], (void **) &nonce, NULL); | |
| 854 | assert(err == 0); | |
| 855 | ||
| 856 | int64_t nonce_offset; | |
| 857 | err = js_get_value_int64(env, argv[5], &nonce_offset); | |
| 858 | assert(err == 0); | |
| 859 | ||
| 860 | int64_t nonce_len; | |
| 861 | err = js_get_value_int64(env, argv[6], &nonce_len); | |
| 862 | assert(err == 0); | |
| 863 | ||
| 864 | uint8_t *ad; | |
| 865 | err = js_get_arraybuffer_info(env, argv[7], (void **) &ad, NULL); | |
| 866 | assert(err == 0); | |
| 867 | ||
| 868 | int64_t ad_offset; | |
| 869 | err = js_get_value_int64(env, argv[8], &ad_offset); | |
| 870 | assert(err == 0); | |
| 871 | ||
| 872 | int64_t ad_len; | |
| 873 | err = js_get_value_int64(env, argv[9], &ad_len); | |
| 874 | assert(err == 0); | |
| 875 | ||
| 876 | size_t out_len; | |
| 877 | uint8_t *out; | |
| 878 | err = js_get_arraybuffer_info(env, argv[10], (void **) &out, &out_len); | |
| 879 | assert(err == 0); | |
| 880 | ||
| 881 | size_t written; | |
| 882 | err = EVP_AEAD_CTX_seal( | |
| 883 | &aead->context, | |
| 884 | out, | |
| 885 | &written, | |
| 886 | out_len, | |
| 887 | &nonce[nonce_offset], | |
| 888 | nonce_len, | |
| 889 | &data[data_offset], | |
| 890 | data_len, | |
| 891 | ad_len ? &ad[ad_offset] : NULL, | |
| 892 | ad_len | |
| 893 | ); | |
| 894 | ||
| 895 | EVP_AEAD_CTX_cleanup(&aead->context); | |
| 896 | ||
| 897 | if (err != 1) { | |
| 898 | err = js_throw_error(env, NULL, "Encryption failed"); | |
| 899 | assert(err == 0); | |
| 900 | } | |
| 901 | ||
| 902 | js_value_t *result; | |
| 903 | err = js_create_int32(env, written, &result); | |
| 904 | assert(err == 0); | |
| 905 | ||
| 906 | return result; | |
| 907 | } | |
| 908 | ||
| 909 | static js_value_t * | |
| 910 | bare_crypto_aead_open(js_env_t *env, js_callback_info_t *info) { | |
| 911 | int err; | |
| 912 | ||
| 913 | size_t argc = 11; | |
| 914 | js_value_t *argv[11]; | |
| 915 | ||
| 916 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 917 | assert(err == 0); | |
| 918 | ||
| 919 | assert(argc == 11); | |
| 920 | ||
| 921 | bare_crypto_aead_t *aead; | |
| 922 | err = js_get_arraybuffer_info(env, argv[0], (void **) &aead, NULL); | |
| 923 | assert(err == 0); | |
| 924 | ||
| 925 | uint8_t *data; | |
| 926 | err = js_get_arraybuffer_info(env, argv[1], (void **) &data, NULL); | |
| 927 | assert(err == 0); | |
| 928 | ||
| 929 | int64_t data_offset; | |
| 930 | err = js_get_value_int64(env, argv[2], &data_offset); | |
| 931 | assert(err == 0); | |
| 932 | ||
| 933 | int64_t data_len; | |
| 934 | err = js_get_value_int64(env, argv[3], &data_len); | |
| 935 | assert(err == 0); | |
| 936 | ||
| 937 | uint8_t *nonce; | |
| 938 | err = js_get_arraybuffer_info(env, argv[4], (void **) &nonce, NULL); | |
| 939 | assert(err == 0); | |
| 940 | ||
| 941 | int64_t nonce_offset; | |
| 942 | err = js_get_value_int64(env, argv[5], &nonce_offset); | |
| 943 | assert(err == 0); | |
| 944 | ||
| 945 | int64_t nonce_len; | |
| 946 | err = js_get_value_int64(env, argv[6], &nonce_len); | |
| 947 | assert(err == 0); | |
| 948 | ||
| 949 | uint8_t *ad; | |
| 950 | err = js_get_arraybuffer_info(env, argv[7], (void **) &ad, NULL); | |
| 951 | assert(err == 0); | |
| 952 | ||
| 953 | int64_t ad_offset; | |
| 954 | err = js_get_value_int64(env, argv[8], &ad_offset); | |
| 955 | assert(err == 0); | |
| 956 | ||
| 957 | int64_t ad_len; | |
| 958 | err = js_get_value_int64(env, argv[9], &ad_len); | |
| 959 | assert(err == 0); | |
| 960 | ||
| 961 | size_t out_len; | |
| 962 | uint8_t *out; | |
| 963 | err = js_get_arraybuffer_info(env, argv[10], (void **) &out, &out_len); | |
| 964 | assert(err == 0); | |
| 965 | ||
| 966 | size_t written; | |
| 967 | err = EVP_AEAD_CTX_open( | |
| 968 | &aead->context, | |
| 969 | out, | |
| 970 | &written, | |
| 971 | out_len, | |
| 972 | &nonce[nonce_offset], | |
| 973 | nonce_len, | |
| 974 | &data[data_offset], | |
| 975 | data_len, | |
| 976 | ad_len ? &ad[ad_offset] : NULL, | |
| 977 | ad_len | |
| 978 | ); | |
| 979 | ||
| 980 | EVP_AEAD_CTX_cleanup(&aead->context); | |
| 981 | ||
| 982 | if (err != 1) { | |
| 983 | err = js_throw_error(env, NULL, "Decryption failed"); | |
| 984 | assert(err == 0); | |
| 985 | } | |
| 986 | ||
| 987 | js_value_t *result; | |
| 988 | err = js_create_int32(env, written, &result); | |
| 989 | assert(err == 0); | |
| 990 | ||
| 991 | return result; | |
| 992 | } | |
| 993 | ||
| 994 | static js_value_t * | |
| 995 | bare_crypto_ed25519_generate_keypair(js_env_t *env, js_callback_info_t *info) { | |
| 996 | int err; | |
| 997 | ||
| 998 | js_value_t *result; | |
| 999 | err = js_create_object(env, &result); | |
| 1000 | assert(err == 0); | |
| 1001 | ||
| 1002 | js_value_t *handle; | |
| 1003 | ||
| 1004 | uint8_t *public_key; | |
| 1005 | err = js_create_arraybuffer(env, ED25519_PUBLIC_KEY_LEN, (void **) &public_key, &handle); | |
| 1006 | assert(err == 0); | |
| 1007 | ||
| 1008 | err = js_set_named_property(env, result, "publicKey", handle); | |
| 1009 | assert(err == 0); | |
| 1010 | ||
| 1011 | uint8_t *private_key; | |
| 1012 | err = js_create_arraybuffer(env, ED25519_PRIVATE_KEY_LEN, (void **) &private_key, &handle); | |
| 1013 | assert(err == 0); | |
| 1014 | ||
| 1015 | err = js_set_named_property(env, result, "privateKey", handle); | |
| 1016 | assert(err == 0); | |
| 1017 | ||
| 1018 | ED25519_keypair(public_key, private_key); | |
| 1019 | ||
| 1020 | return result; | |
| 1021 | } | |
| 1022 | ||
| 1023 | static js_value_t * | |
| 1024 | bare_crypto_ed25519_sign(js_env_t *env, js_callback_info_t *info) { | |
| 1025 | int err; | |
| 1026 | ||
| 1027 | size_t argc = 4; | |
| 1028 | js_value_t *argv[4]; | |
| 1029 | ||
| 1030 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 1031 | assert(err == 0); | |
| 1032 | ||
| 1033 | assert(argc == 4); | |
| 1034 | ||
| 1035 | uint8_t *data; | |
| 1036 | err = js_get_arraybuffer_info(env, argv[0], (void **) &data, NULL); | |
| 1037 | assert(err == 0); | |
| 1038 | ||
| 1039 | uint32_t offset; | |
| 1040 | err = js_get_value_uint32(env, argv[1], &offset); | |
| 1041 | assert(err == 0); | |
| 1042 | ||
| 1043 | uint32_t len; | |
| 1044 | err = js_get_value_uint32(env, argv[2], &len); | |
| 1045 | assert(err == 0); | |
| 1046 | ||
| 1047 | uint8_t *private_key; | |
| 1048 | err = js_get_arraybuffer_info(env, argv[3], (void **) &private_key, NULL); | |
| 1049 | assert(err == 0); | |
| 1050 | ||
| 1051 | js_value_t *handle; | |
| 1052 | ||
| 1053 | uint8_t *signature; | |
| 1054 | err = js_create_arraybuffer(env, ED25519_SIGNATURE_LEN, (void **) &signature, &handle); | |
| 1055 | assert(err == 0); | |
| 1056 | ||
| 1057 | err = ED25519_sign(signature, &data[offset], len, private_key); | |
| 1058 | assert(err == 1); | |
| 1059 | ||
| 1060 | return handle; | |
| 1061 | } | |
| 1062 | ||
| 1063 | static js_value_t * | |
| 1064 | bare_crypto_ed25519_verify(js_env_t *env, js_callback_info_t *info) { | |
| 1065 | int err; | |
| 1066 | ||
| 1067 | size_t argc = 6; | |
| 1068 | js_value_t *argv[6]; | |
| 1069 | ||
| 1070 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 1071 | assert(err == 0); | |
| 1072 | ||
| 1073 | assert(argc == 6); | |
| 1074 | ||
| 1075 | uint8_t *data; | |
| 1076 | err = js_get_arraybuffer_info(env, argv[0], (void **) &data, NULL); | |
| 1077 | assert(err == 0); | |
| 1078 | ||
| 1079 | uint32_t data_offset; | |
| 1080 | err = js_get_value_uint32(env, argv[1], &data_offset); | |
| 1081 | assert(err == 0); | |
| 1082 | ||
| 1083 | uint32_t data_len; | |
| 1084 | err = js_get_value_uint32(env, argv[2], &data_len); | |
| 1085 | assert(err == 0); | |
| 1086 | ||
| 1087 | uint8_t *signature; | |
| 1088 | err = js_get_arraybuffer_info(env, argv[3], (void **) &signature, NULL); | |
| 1089 | assert(err == 0); | |
| 1090 | ||
| 1091 | uint32_t signature_offset; | |
| 1092 | err = js_get_value_uint32(env, argv[4], &signature_offset); | |
| 1093 | assert(err == 0); | |
| 1094 | ||
| 1095 | uint8_t *public_key; | |
| 1096 | err = js_get_arraybuffer_info(env, argv[5], (void **) &public_key, NULL); | |
| 1097 | assert(err == 0); | |
| 1098 | ||
| 1099 | js_value_t *result; | |
| 1100 | err = js_get_boolean(env, ED25519_verify(&data[data_offset], data_len, &signature[signature_offset], public_key), &result); | |
| 1101 | assert(err == 0); | |
| 1102 | ||
| 1103 | return result; | |
| 1104 | } | |
| 1105 | ||
| 1106 | static js_value_t * | |
| 1107 | bare_crypto_ed25519_to_spki(js_env_t *env, js_callback_info_t *info) { | |
| 1108 | int err; | |
| 1109 | ||
| 1110 | size_t argc = 1; | |
| 1111 | js_value_t *argv[1]; | |
| 1112 | ||
| 1113 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 1114 | assert(err == 0); | |
| 1115 | ||
| 1116 | assert(argc == 1); | |
| 1117 | ||
| 1118 | uint8_t *public_key; | |
| 1119 | err = js_get_arraybuffer_info(env, argv[0], (void **) &public_key, NULL); | |
| 1120 | assert(err == 0); | |
| 1121 | ||
| 1122 | EVP_PKEY *pkey = EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, NULL, public_key, 32); | |
| 1123 | ||
| 1124 | CBB bytes; | |
| 1125 | err = CBB_init(&bytes, 0); | |
| 1126 | assert(err == 1); | |
| 1127 | ||
| 1128 | err = EVP_marshal_public_key(&bytes, pkey); | |
| 1129 | assert(err == 1); | |
| 1130 | ||
| 1131 | EVP_PKEY_free(pkey); | |
| 1132 | ||
| 1133 | uint8_t *der; | |
| 1134 | size_t len; | |
| 1135 | err = CBB_finish(&bytes, &der, &len); | |
| 1136 | assert(err == 1); | |
| 1137 | ||
| 1138 | js_value_t *handle; | |
| 1139 | ||
| 1140 | void *data; | |
| 1141 | err = js_create_arraybuffer(env, len, &data, &handle); | |
| 1142 | assert(err == 0); | |
| 1143 | ||
| 1144 | memcpy(data, der, len); | |
| 1145 | ||
| 1146 | OPENSSL_free(der); | |
| 1147 | ||
| 1148 | return handle; | |
| 1149 | } | |
| 1150 | ||
| 1151 | static js_value_t * | |
| 1152 | bare_crypto_ed25519_from_spki(js_env_t *env, js_callback_info_t *info) { | |
| 1153 | int err; | |
| 1154 | ||
| 1155 | size_t argc = 3; | |
| 1156 | js_value_t *argv[3]; | |
| 1157 | ||
| 1158 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 1159 | assert(err == 0); | |
| 1160 | ||
| 1161 | assert(argc == 3); | |
| 1162 | ||
| 1163 | uint8_t *der; | |
| 1164 | err = js_get_arraybuffer_info(env, argv[0], (void **) &der, NULL); | |
| 1165 | assert(err == 0); | |
| 1166 | ||
| 1167 | uint32_t offset; | |
| 1168 | err = js_get_value_uint32(env, argv[1], &offset); | |
| 1169 | assert(err == 0); | |
| 1170 | ||
| 1171 | uint32_t len; | |
| 1172 | err = js_get_value_uint32(env, argv[2], &len); | |
| 1173 | assert(err == 0); | |
| 1174 | ||
| 1175 | CBS bytes; | |
| 1176 | CBS_init(&bytes, &der[offset], len); | |
| 1177 | ||
| 1178 | EVP_PKEY *pkey = EVP_parse_public_key(&bytes); | |
| 1179 | ||
| 1180 | if (pkey == NULL) { | |
| 1181 | err = js_throw_error(env, NULL, "Invalid input"); | |
| 1182 | assert(err == 0); | |
| 1183 | ||
| 1184 | return NULL; | |
| 1185 | } | |
| 1186 | ||
| 1187 | js_value_t *handle; | |
| 1188 | ||
| 1189 | uint8_t *public_key; | |
| 1190 | err = js_create_arraybuffer(env, ED25519_PUBLIC_KEY_LEN, (void **) &public_key, &handle); | |
| 1191 | assert(err == 0); | |
| 1192 | ||
| 1193 | size_t written = ED25519_PUBLIC_KEY_LEN; | |
| 1194 | err = EVP_PKEY_get_raw_public_key(pkey, public_key, &written); | |
| 1195 | ||
| 1196 | EVP_PKEY_free(pkey); | |
| 1197 | ||
| 1198 | if (err != 1) { | |
| 1199 | err = js_throw_error(env, NULL, "Invalid input"); | |
| 1200 | assert(err == 0); | |
| 1201 | ||
| 1202 | return NULL; | |
| 1203 | } | |
| 1204 | ||
| 1205 | return handle; | |
| 1206 | } | |
| 1207 | ||
| 1208 | static js_value_t * | |
| 1209 | bare_crypto_ed25519_to_pkcs8(js_env_t *env, js_callback_info_t *info) { | |
| 1210 | int err; | |
| 1211 | ||
| 1212 | size_t argc = 1; | |
| 1213 | js_value_t *argv[1]; | |
| 1214 | ||
| 1215 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 1216 | assert(err == 0); | |
| 1217 | ||
| 1218 | assert(argc == 1); | |
| 1219 | ||
| 1220 | uint8_t *private_key; | |
| 1221 | err = js_get_arraybuffer_info(env, argv[0], (void **) &private_key, NULL); | |
| 1222 | assert(err == 0); | |
| 1223 | ||
| 1224 | EVP_PKEY *pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, NULL, private_key, 32); | |
| 1225 | ||
| 1226 | CBB bytes; | |
| 1227 | err = CBB_init(&bytes, 0); | |
| 1228 | assert(err == 1); | |
| 1229 | ||
| 1230 | err = EVP_marshal_private_key(&bytes, pkey); | |
| 1231 | assert(err == 1); | |
| 1232 | ||
| 1233 | EVP_PKEY_free(pkey); | |
| 1234 | ||
| 1235 | uint8_t *der; | |
| 1236 | size_t len; | |
| 1237 | err = CBB_finish(&bytes, &der, &len); | |
| 1238 | assert(err == 1); | |
| 1239 | ||
| 1240 | js_value_t *handle; | |
| 1241 | ||
| 1242 | void *data; | |
| 1243 | err = js_create_arraybuffer(env, len, &data, &handle); | |
| 1244 | assert(err == 0); | |
| 1245 | ||
| 1246 | memcpy(data, der, len); | |
| 1247 | ||
| 1248 | OPENSSL_free(der); | |
| 1249 | ||
| 1250 | return handle; | |
| 1251 | } | |
| 1252 | ||
| 1253 | static js_value_t * | |
| 1254 | bare_crypto_ed25519_from_pkcs8(js_env_t *env, js_callback_info_t *info) { | |
| 1255 | int err; | |
| 1256 | ||
| 1257 | size_t argc = 3; | |
| 1258 | js_value_t *argv[3]; | |
| 1259 | ||
| 1260 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 1261 | assert(err == 0); | |
| 1262 | ||
| 1263 | assert(argc == 3); | |
| 1264 | ||
| 1265 | uint8_t *der; | |
| 1266 | err = js_get_arraybuffer_info(env, argv[0], (void **) &der, NULL); | |
| 1267 | assert(err == 0); | |
| 1268 | ||
| 1269 | uint32_t offset; | |
| 1270 | err = js_get_value_uint32(env, argv[1], &offset); | |
| 1271 | assert(err == 0); | |
| 1272 | ||
| 1273 | uint32_t len; | |
| 1274 | err = js_get_value_uint32(env, argv[2], &len); | |
| 1275 | assert(err == 0); | |
| 1276 | ||
| 1277 | CBS bytes; | |
| 1278 | CBS_init(&bytes, &der[offset], len); | |
| 1279 | ||
| 1280 | EVP_PKEY *pkey = EVP_parse_private_key(&bytes); | |
| 1281 | ||
| 1282 | if (pkey == NULL) { | |
| 1283 | err = js_throw_error(env, NULL, "Invalid input"); | |
| 1284 | assert(err == 0); | |
| 1285 | ||
| 1286 | return NULL; | |
| 1287 | } | |
| 1288 | ||
| 1289 | js_value_t *handle; | |
| 1290 | ||
| 1291 | uint8_t *private_key; | |
| 1292 | err = js_create_arraybuffer(env, ED25519_PRIVATE_KEY_LEN, (void **) &private_key, &handle); | |
| 1293 | assert(err == 0); | |
| 1294 | ||
| 1295 | size_t written = ED25519_PRIVATE_KEY_LEN; | |
| 1296 | err = EVP_PKEY_get_raw_private_key(pkey, private_key, &written); | |
| 1297 | ||
| 1298 | if (err != 1) { | |
| 1299 | EVP_PKEY_free(pkey); | |
| 1300 | ||
| 1301 | err = js_throw_error(env, NULL, "Invalid input"); | |
| 1302 | assert(err == 0); | |
| 1303 | ||
| 1304 | return NULL; | |
| 1305 | } | |
| 1306 | ||
| 1307 | written = ED25519_PUBLIC_KEY_LEN; | |
| 1308 | err = EVP_PKEY_get_raw_public_key(pkey, &private_key[32], &written); | |
| 1309 | ||
| 1310 | EVP_PKEY_free(pkey); | |
| 1311 | ||
| 1312 | if (err != 1) { | |
| 1313 | err = js_throw_error(env, NULL, "Invalid input"); | |
| 1314 | assert(err == 0); | |
| 1315 | ||
| 1316 | return NULL; | |
| 1317 | } | |
| 1318 | ||
| 1319 | return handle; | |
| 1320 | } | |
| 1321 | ||
| 1322 | static js_value_t * | |
| 1323 | bare_crypto_random_fill(js_env_t *env, js_callback_info_t *info) { | |
| 1324 | int err; | |
| 1325 | ||
| 1326 | size_t argc = 3; | |
| 1327 | js_value_t *argv[3]; | |
| 1328 | ||
| 1329 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 1330 | assert(err == 0); | |
| 1331 | ||
| 1332 | assert(argc == 3); | |
| 1333 | ||
| 1334 | uint8_t *data; | |
| 1335 | err = js_get_arraybuffer_info(env, argv[0], (void **) &data, NULL); | |
| 1336 | assert(err == 0); | |
| 1337 | ||
| 1338 | uint32_t offset; | |
| 1339 | err = js_get_value_uint32(env, argv[1], &offset); | |
| 1340 | assert(err == 0); | |
| 1341 | ||
| 1342 | uint32_t len; | |
| 1343 | err = js_get_value_uint32(env, argv[2], &len); | |
| 1344 | assert(err == 0); | |
| 1345 | ||
| 1346 | err = RAND_bytes(&data[offset], len); | |
| 1347 | assert(err == 1); | |
| 1348 | ||
| 1349 | return NULL; | |
| 1350 | } | |
| 1351 | ||
| 1352 | static js_value_t * | |
| 1353 | bare_crypto_pbkdf2(js_env_t *env, js_callback_info_t *info) { | |
| 1354 | int err; | |
| 1355 | ||
| 1356 | size_t argc = 9; | |
| 1357 | js_value_t *argv[9]; | |
| 1358 | ||
| 1359 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 1360 | assert(err == 0); | |
| 1361 | ||
| 1362 | assert(argc == 9); | |
| 1363 | ||
| 1364 | char *password; | |
| 1365 | err = js_get_arraybuffer_info(env, argv[0], (void **) &password, NULL); | |
| 1366 | assert(err == 0); | |
| 1367 | ||
| 1368 | int64_t password_offset; | |
| 1369 | err = js_get_value_int64(env, argv[1], &password_offset); | |
| 1370 | assert(err == 0); | |
| 1371 | ||
| 1372 | int64_t password_len; | |
| 1373 | err = js_get_value_int64(env, argv[2], &password_len); | |
| 1374 | assert(err == 0); | |
| 1375 | ||
| 1376 | uint8_t *salt; | |
| 1377 | err = js_get_arraybuffer_info(env, argv[3], (void **) &salt, NULL); | |
| 1378 | assert(err == 0); | |
| 1379 | ||
| 1380 | int64_t salt_offset; | |
| 1381 | err = js_get_value_int64(env, argv[4], &salt_offset); | |
| 1382 | assert(err == 0); | |
| 1383 | ||
| 1384 | int64_t salt_len; | |
| 1385 | err = js_get_value_int64(env, argv[5], &salt_len); | |
| 1386 | assert(err == 0); | |
| 1387 | ||
| 1388 | uint32_t iterations; | |
| 1389 | err = js_get_value_uint32(env, argv[6], &iterations); | |
| 1390 | assert(err == 0); | |
| 1391 | ||
| 1392 | uint32_t type; | |
| 1393 | err = js_get_value_uint32(env, argv[7], &type); | |
| 1394 | assert(err == 0); | |
| 1395 | ||
| 1396 | int64_t key_len; | |
| 1397 | err = js_get_value_int64(env, argv[8], &key_len); | |
| 1398 | assert(err == 0); | |
| 1399 | ||
| 1400 | js_value_t *handle; | |
| 1401 | ||
| 1402 | uint8_t *out; | |
| 1403 | err = js_create_arraybuffer(env, key_len, (void **) &out, &handle); | |
| 1404 | assert(err == 0); | |
| 1405 | ||
| 1406 | const EVP_MD *algorithm = bare_crypto__to_digest(env, type); | |
| 1407 | ||
| 1408 | if (algorithm == NULL) return NULL; | |
| 1409 | ||
| 1410 | err = PKCS5_PBKDF2_HMAC(&password[password_offset], password_len, &salt[salt_offset], salt_len, iterations, algorithm, key_len, out); | |
| 1411 | assert(err == 1); | |
| 1412 | ||
| 1413 | return handle; | |
| 1414 | } | |
| 1415 | ||
| 1416 | static js_value_t * | |
| 1417 | bare_crypto_exports(js_env_t *env, js_value_t *exports) { | |
| 1418 | int err; | |
| 1419 | ||
| 1420 | #define V(name, fn) \ | |
| 1421 | { \ | |
| 1422 | js_value_t *val; \ | |
| 1423 | err = js_create_function(env, name, -1, fn, NULL, &val); \ | |
| 1424 | assert(err == 0); \ | |
| 1425 | err = js_set_named_property(env, exports, name, val); \ | |
| 1426 | assert(err == 0); \ | |
| 1427 | } | |
| 1428 | ||
| 1429 | V("digestInit", bare_crypto_digest_init) | |
| 1430 | V("digestUpdate", bare_crypto_digest_update) | |
| 1431 | V("digestFinal", bare_crypto_digest_final) | |
| 1432 | ||
| 1433 | V("ripemd160Init", bare_crypto_ripemd160_init) | |
| 1434 | V("ripemd160Update", bare_crypto_ripemd160_update) | |
| 1435 | V("ripemd160Final", bare_crypto_ripemd160_final) | |
| 1436 | ||
| 1437 | V("hmacInit", bare_crypto_hmac_init) | |
| 1438 | V("hmacUpdate", bare_crypto_hmac_update) | |
| 1439 | V("hmacFinal", bare_crypto_hmac_final) | |
| 1440 | ||
| 1441 | V("cipherKeyLength", bare_crypto_cipher_key_length) | |
| 1442 | V("cipherIVLength", bare_crypto_cipher_iv_length) | |
| 1443 | V("cipherBlockSize", bare_crypto_cipher_block_size) | |
| 1444 | V("cipherInit", bare_crypto_cipher_init) | |
| 1445 | V("cipherUpdate", bare_crypto_cipher_update) | |
| 1446 | V("cipherFinal", bare_crypto_cipher_final) | |
| 1447 | V("cipherSetPadding", bare_crypto_cipher_set_padding) | |
| 1448 | ||
| 1449 | V("aeadKeyLength", bare_crypto_aead_key_length) | |
| 1450 | V("aeadNonceLength", bare_crypto_aead_nonce_length) | |
| 1451 | V("aeadMaxOverhead", bare_crypto_aead_max_overhead) | |
| 1452 | V("aeadMaxTagLength", bare_crypto_aead_max_tag_length) | |
| 1453 | V("aeadInit", bare_crypto_aead_init) | |
| 1454 | V("aeadSeal", bare_crypto_aead_seal) | |
| 1455 | V("aeadOpen", bare_crypto_aead_open) | |
| 1456 | ||
| 1457 | V("ed25519GenerateKeypair", bare_crypto_ed25519_generate_keypair) | |
| 1458 | V("ed25519Sign", bare_crypto_ed25519_sign) | |
| 1459 | V("ed25519Verify", bare_crypto_ed25519_verify) | |
| 1460 | V("ed25519ToSPKI", bare_crypto_ed25519_to_spki) | |
| 1461 | V("ed25519FromSPKI", bare_crypto_ed25519_from_spki) | |
| 1462 | V("ed25519ToPKCS8", bare_crypto_ed25519_to_pkcs8) | |
| 1463 | V("ed25519FromPKCS8", bare_crypto_ed25519_from_pkcs8) | |
| 1464 | ||
| 1465 | V("randomFill", bare_crypto_random_fill) | |
| 1466 | ||
| 1467 | V("pbkdf2", bare_crypto_pbkdf2); | |
| 1468 | #undef V | |
| 1469 | ||
| 1470 | #define V(name, n) \ | |
| 1471 | { \ | |
| 1472 | js_value_t *val; \ | |
| 1473 | err = js_create_uint32(env, n, &val); \ | |
| 1474 | assert(err == 0); \ | |
| 1475 | err = js_set_named_property(env, exports, name, val); \ | |
| 1476 | assert(err == 0); \ | |
| 1477 | } | |
| 1478 | ||
| 1479 | // Digest algorithms | |
| 1480 | V("MD5", bare_crypto_md5) | |
| 1481 | V("SHA1", bare_crypto_sha1) | |
| 1482 | V("SHA256", bare_crypto_sha256) | |
| 1483 | V("SHA384", bare_crypto_sha384) | |
| 1484 | V("SHA512", bare_crypto_sha512) | |
| 1485 | V("BLAKE2B256", bare_crypto_blake2b256) | |
| 1486 | V("RIPEMD160", bare_crypto_ripemd160) | |
| 1487 | ||
| 1488 | // Cipher algorithms | |
| 1489 | V("AES128ECB", bare_crypto_aes_128_ecb) | |
| 1490 | V("AES128CBC", bare_crypto_aes_128_cbc) | |
| 1491 | V("AES128CTR", bare_crypto_aes_128_ctr) | |
| 1492 | V("AES128OFB", bare_crypto_aes_128_ofb) | |
| 1493 | V("AES256ECB", bare_crypto_aes_256_ecb) | |
| 1494 | V("AES256CBC", bare_crypto_aes_256_cbc) | |
| 1495 | V("AES256CTR", bare_crypto_aes_256_ctr) | |
| 1496 | V("AES256OFB", bare_crypto_aes_256_ofb) | |
| 1497 | V("AES128GCM", bare_crypto_aes_128_gcm) | |
| 1498 | V("AES256GCM", bare_crypto_aes_256_gcm) | |
| 1499 | V("CHACHA20POLY1305", bare_crypto_chacha20_poly1305) | |
| 1500 | V("XCHACHA20POLY1305", bare_crypto_xchacha20_poly1305) | |
| 1501 | ||
| 1502 | // Signature algorithms | |
| 1503 | V("ED25519", bare_crypto_ed25519) | |
| 1504 | #undef V | |
| 1505 | ||
| 1506 | return exports; | |
| 1507 | } | |
| 1508 | ||
| 1509 | BARE_MODULE(bare_crypto, bare_crypto_exports) |
| … | … | @@ -0,0 +1 @@ |
| 1 | module.exports = require.addon() |
| … | … | @@ -0,0 +1,7 @@ |
| 1 | export * as crypto from './web' | |
| 2 | ||
| 3 | type Crypto = typeof crypto | |
| 4 | ||
| 5 | declare global { | |
| 6 | const crypto: Crypto | |
| 7 | } |
| … | … | @@ -0,0 +1,6 @@ |
| 1 | const crypto = require('./web') | |
| 2 | ||
| 3 | global.crypto = crypto | |
| 4 | global.Crypto = crypto.Crypto | |
| 5 | global.CryptoKey = crypto.CryptoKey | |
| 6 | global.SubtleCrypto = crypto.SubtleCrypto |
| … | … | @@ -0,0 +1,69 @@ |
| 1 | import { TransformOptions } from 'bare-stream' | |
| 2 | import Buffer from 'bare-buffer' | |
| 3 | import constants from './lib/constants' | |
| 4 | import Hash, { type HashAlgorithm } from './lib/hash' | |
| 5 | import Hmac from './lib/hmac' | |
| 6 | import { Cipheriv, Decipheriv, type CipherAlgorithm } from './lib/cipher' | |
| 7 | import { randomBytes, randomFill, randomUUID } from './lib/random' | |
| 8 | import pbkdf2 from './lib/pbkdf2' | |
| 9 | import { generateKeyPair } from './lib/key' | |
| 10 | import { sign, verify } from './lib/signature' | |
| 11 | import web from './web' | |
| 12 | ||
| 13 | declare function createHash(algorithm: HashAlgorithm | number, opts?: TransformOptions<Hash>): Hash | |
| 14 | ||
| 15 | declare function createHmac( | |
| 16 | algorithm: HashAlgorithm | number, | |
| 17 | key: string | Buffer, | |
| 18 | opts?: TransformOptions<Hmac> | |
| 19 | ): Hmac | |
| 20 | ||
| 21 | declare function createCipheriv( | |
| 22 | algorithm: CipherAlgorithm | number, | |
| 23 | key: string | Buffer, | |
| 24 | iv: string | Buffer, | |
| 25 | opts?: TransformOptions<Cipheriv> | |
| 26 | ): Cipheriv | |
| 27 | ||
| 28 | declare function createDecipheriv( | |
| 29 | algorithm: CipherAlgorithm | number, | |
| 30 | key: string | Buffer, | |
| 31 | iv: string | Buffer, | |
| 32 | opts?: TransformOptions<Cipheriv> | |
| 33 | ): Decipheriv | |
| 34 | ||
| 35 | declare function randomFillSync<B extends ArrayBuffer | ArrayBufferView>( | |
| 36 | buffer: B, | |
| 37 | offset?: number, | |
| 38 | size?: number | |
| 39 | ): B | |
| 40 | ||
| 41 | declare function pbkdf2Sync( | |
| 42 | password: string | ArrayBufferView, | |
| 43 | salt: string | ArrayBufferView, | |
| 44 | iterations: number, | |
| 45 | keylen: number, | |
| 46 | digest: HashAlgorithm | number | |
| 47 | ): Buffer | |
| 48 | ||
| 49 | export { | |
| 50 | constants, | |
| 51 | Hash, | |
| 52 | createHash, | |
| 53 | Hmac, | |
| 54 | createHmac, | |
| 55 | Cipheriv, | |
| 56 | createCipheriv, | |
| 57 | Decipheriv, | |
| 58 | createDecipheriv, | |
| 59 | randomBytes, | |
| 60 | randomFill, | |
| 61 | randomUUID, | |
| 62 | randomFillSync, | |
| 63 | pbkdf2, | |
| 64 | pbkdf2Sync, | |
| 65 | generateKeyPair, | |
| 66 | sign, | |
| 67 | verify, | |
| 68 | web as webcrypto | |
| 69 | } |
| … | … | @@ -0,0 +1,61 @@ |
| 1 | const constants = require('./lib/constants') | |
| 2 | const Hash = require('./lib/hash') | |
| 3 | const Hmac = require('./lib/hmac') | |
| 4 | const { Cipheriv, Decipheriv } = require('./lib/cipher') | |
| 5 | const { randomBytes, randomFill, randomUUID } = require('./lib/random') | |
| 6 | const pbkdf2 = require('./lib/pbkdf2') | |
| 7 | const { generateKeyPair } = require('./lib/key') | |
| 8 | const { sign, verify } = require('./lib/signature') | |
| 9 | ||
| 10 | exports.constants = constants | |
| 11 | ||
| 12 | exports.Hash = Hash | |
| 13 | ||
| 14 | exports.createHash = function createHash(algorithm, opts) { | |
| 15 | return new Hash(algorithm, opts) | |
| 16 | } | |
| 17 | ||
| 18 | exports.Hmac = Hmac | |
| 19 | ||
| 20 | exports.createHmac = function createHmac(algorithm, key, opts) { | |
| 21 | return new Hmac(algorithm, key, opts) | |
| 22 | } | |
| 23 | ||
| 24 | exports.Cipheriv = Cipheriv | |
| 25 | ||
| 26 | exports.createCipheriv = function createCipheriv(algorithm, key, iv, opts) { | |
| 27 | return new Cipheriv(algorithm, key, iv, opts) | |
| 28 | } | |
| 29 | ||
| 30 | exports.Decipheriv = Decipheriv | |
| 31 | ||
| 32 | exports.createDecipheriv = function createDecipheriv(algorithm, key, iv, opts) { | |
| 33 | return new Decipheriv(algorithm, key, iv, opts) | |
| 34 | } | |
| 35 | ||
| 36 | exports.randomBytes = randomBytes | |
| 37 | ||
| 38 | exports.randomFill = randomFill | |
| 39 | ||
| 40 | // For Node.js compatibility | |
| 41 | exports.randomFillSync = function randomFillSync(buffer, offset, size) { | |
| 42 | return exports.randomFill(buffer, offset, size) | |
| 43 | } | |
| 44 | ||
| 45 | exports.randomUUID = randomUUID | |
| 46 | ||
| 47 | exports.pbkdf2 = pbkdf2 | |
| 48 | ||
| 49 | // For Node.js compatibility | |
| 50 | exports.pbkdf2Sync = function pbkdf2Sync(password, salt, iterations, keylen, digest) { | |
| 51 | return exports.pbkdf2(password, salt, iterations, keylen, digest) | |
| 52 | } | |
| 53 | ||
| 54 | exports.generateKeyPair = generateKeyPair | |
| 55 | ||
| 56 | exports.sign = sign | |
| 57 | ||
| 58 | exports.verify = verify | |
| 59 | ||
| 60 | // For Node.js compatibility | |
| 61 | exports.webcrypto = require('./web') |
| … | … | @@ -0,0 +1,62 @@ |
| 1 | import { Transform, TransformOptions } from 'bare-stream' | |
| 2 | import Buffer, { BufferEncoding } from 'bare-buffer' | |
| 3 | ||
| 4 | export type CipherAlgorithm = | |
| 5 | | 'aes-128-cbc' | |
| 6 | | 'aes-128-ctr' | |
| 7 | | 'aes-128-ecb' | |
| 8 | | 'aes-128-gcm' | |
| 9 | | 'aes-128-ofb' | |
| 10 | | 'aes-256-cbc' | |
| 11 | | 'aes-256-ctr' | |
| 12 | | 'aes-256-ecb' | |
| 13 | | 'aes-256-gcm' | |
| 14 | | 'aes-256-ofb' | |
| 15 | | 'chacha20-poly1305' | |
| 16 | | 'xchacha20-poly1305' | |
| 17 | ||
| 18 | export class Cipheriv extends Transform { | |
| 19 | constructor( | |
| 20 | algorithm: CipherAlgorithm | number, | |
| 21 | key: string | Buffer, | |
| 22 | iv: string | Buffer, | |
| 23 | opts?: TransformOptions<Cipheriv> | |
| 24 | ) | |
| 25 | ||
| 26 | update( | |
| 27 | data: string | Buffer, | |
| 28 | inputEncoding?: BufferEncoding, | |
| 29 | outputEncoding?: BufferEncoding | |
| 30 | ): string | Buffer | |
| 31 | ||
| 32 | final(outputEncoding?: BufferEncoding): string | Buffer | |
| 33 | ||
| 34 | setAutoPadding(pad: unknown): this | |
| 35 | ||
| 36 | setAAD(buffer: string | Buffer, opts?: { encoding?: BufferEncoding }): this | |
| 37 | ||
| 38 | getAuthTag(): Buffer | |
| 39 | } | |
| 40 | ||
| 41 | export class Decipheriv extends Transform { | |
| 42 | constructor( | |
| 43 | algorithm: CipherAlgorithm | number, | |
| 44 | key: string | Buffer, | |
| 45 | iv: string | Buffer, | |
| 46 | opts?: TransformOptions<Cipheriv> | |
| 47 | ) | |
| 48 | ||
| 49 | update( | |
| 50 | data: string | Buffer, | |
| 51 | inputEncoding?: BufferEncoding, | |
| 52 | outputEncoding?: BufferEncoding | |
| 53 | ): string | Buffer | |
| 54 | ||
| 55 | final(outputEncoding?: BufferEncoding): string | Buffer | |
| 56 | ||
| 57 | setAutoPadding(pad: boolean): this | |
| 58 | ||
| 59 | setAAD(buffer: string | Buffer, opts?: { encoding?: BufferEncoding }): this | |
| 60 | ||
| 61 | setAuthTag(authTag: string | Buffer, encoding?: BufferEncoding): this | |
| 62 | } |
| … | … | @@ -0,0 +1,372 @@ |
| 1 | const { Transform } = require('bare-stream') | |
| 2 | const assert = require('bare-assert') | |
| 3 | const binding = require('../binding') | |
| 4 | const constants = require('./constants') | |
| 5 | ||
| 6 | const { | |
| 7 | cipher: { | |
| 8 | AES128ECB, | |
| 9 | AES128CBC, | |
| 10 | AES128CTR, | |
| 11 | AES128OFB, | |
| 12 | AES256ECB, | |
| 13 | AES256CBC, | |
| 14 | AES256CTR, | |
| 15 | AES256OFB, | |
| 16 | AES128GCM, | |
| 17 | AES256GCM, | |
| 18 | CHACHA20POLY1305, | |
| 19 | XCHACHA20POLY1305 | |
| 20 | } | |
| 21 | } = constants | |
| 22 | ||
| 23 | class CryptoCipher { | |
| 24 | constructor(algorithm, key, iv, encrypt, opts = {}) { | |
| 25 | const { encoding = 'utf8' } = opts | |
| 26 | ||
| 27 | if (typeof key === 'string') key = Buffer.from(key, encoding) | |
| 28 | if (typeof iv === 'string') iv = Buffer.from(iv, encoding) | |
| 29 | ||
| 30 | assert(ArrayBuffer.isView(key)) | |
| 31 | assert(ArrayBuffer.isView(iv)) | |
| 32 | ||
| 33 | if (key.byteLength !== binding.cipherKeyLength(algorithm)) { | |
| 34 | throw new RangeError('Invalid key length') | |
| 35 | } | |
| 36 | ||
| 37 | if (iv.byteLength < binding.cipherIVLength(algorithm)) { | |
| 38 | throw new RangeError('Invalid iv length') | |
| 39 | } | |
| 40 | ||
| 41 | this._handle = binding.cipherInit( | |
| 42 | algorithm, | |
| 43 | key.buffer, | |
| 44 | key.byteOffset, | |
| 45 | key.byteLength, | |
| 46 | iv.buffer, | |
| 47 | iv.byteOffset, | |
| 48 | iv.byteLength, | |
| 49 | encrypt | |
| 50 | ) | |
| 51 | } | |
| 52 | ||
| 53 | update(data, inputEncoding = 'utf8', outputEncoding) { | |
| 54 | if (this._handle === null) { | |
| 55 | throw new Error('Cipher has already been finalized') | |
| 56 | } | |
| 57 | ||
| 58 | if (typeof data === 'string') data = Buffer.from(data, inputEncoding) | |
| 59 | ||
| 60 | assert(ArrayBuffer.isView(data)) | |
| 61 | ||
| 62 | const out = new ArrayBuffer(data.byteLength + binding.cipherBlockSize(this._handle)) | |
| 63 | ||
| 64 | const written = binding.cipherUpdate( | |
| 65 | this._handle, | |
| 66 | data.buffer, | |
| 67 | data.byteOffset, | |
| 68 | data.byteLength, | |
| 69 | out | |
| 70 | ) | |
| 71 | ||
| 72 | const result = Buffer.from(out, 0, written) | |
| 73 | ||
| 74 | return outputEncoding ? result.toString(outputEncoding) : result | |
| 75 | } | |
| 76 | ||
| 77 | final(outputEncoding) { | |
| 78 | if (this._handle === null) { | |
| 79 | throw new Error('Cipher has already been finalized') | |
| 80 | } | |
| 81 | ||
| 82 | const out = new ArrayBuffer(binding.cipherBlockSize(this._handle)) | |
| 83 | ||
| 84 | const written = binding.cipherFinal(this._handle, out) | |
| 85 | ||
| 86 | this._handle = null | |
| 87 | ||
| 88 | const result = Buffer.from(out, 0, written) | |
| 89 | ||
| 90 | return outputEncoding ? result.toString(outputEncoding) : result | |
| 91 | } | |
| 92 | ||
| 93 | setAutoPadding(pad) { | |
| 94 | if (this._handle === null) { | |
| 95 | throw new Error('Cipher has already been finalized') | |
| 96 | } | |
| 97 | ||
| 98 | binding.cipherSetPadding(this._handle, pad) | |
| 99 | } | |
| 100 | } | |
| 101 | ||
| 102 | class CryptoAuthenticatedCipher { | |
| 103 | constructor(algorithm, key, nonce, opts = {}) { | |
| 104 | const { encoding = 'utf8', authTagLength = 16 } = opts | |
| 105 | ||
| 106 | if (typeof key === 'string') key = Buffer.from(key, encoding) | |
| 107 | if (typeof nonce === 'string') nonce = Buffer.from(nonce, encoding) | |
| 108 | ||
| 109 | assert(ArrayBuffer.isView(key)) | |
| 110 | assert(ArrayBuffer.isView(nonce)) | |
| 111 | ||
| 112 | if (key.byteLength !== binding.aeadKeyLength(algorithm)) { | |
| 113 | throw new RangeError('Invalid key length') | |
| 114 | } | |
| 115 | ||
| 116 | if (nonce.byteLength < binding.aeadNonceLength(algorithm)) { | |
| 117 | throw new RangeError('Invalid nonce length') | |
| 118 | } | |
| 119 | ||
| 120 | this._buffer = [] | |
| 121 | this._nonce = nonce | |
| 122 | this._authTag = null | |
| 123 | this._authTagLength = authTagLength | |
| 124 | this._additionalData = null | |
| 125 | ||
| 126 | this._handle = binding.aeadInit( | |
| 127 | algorithm, | |
| 128 | key.buffer, | |
| 129 | key.byteOffset, | |
| 130 | key.byteLength, | |
| 131 | authTagLength | |
| 132 | ) | |
| 133 | } | |
| 134 | ||
| 135 | update(data, inputEncoding = 'utf8', outputEncoding) { | |
| 136 | if (typeof data === 'string') data = Buffer.from(data, inputEncoding) | |
| 137 | ||
| 138 | assert(ArrayBuffer.isView(data)) | |
| 139 | ||
| 140 | this._buffer.push(data) | |
| 141 | ||
| 142 | return outputEncoding ? '' : Buffer.alloc(0) | |
| 143 | } | |
| 144 | ||
| 145 | setAAD(buffer, opts = {}) { | |
| 146 | const { encoding = 'utf8' } = opts | |
| 147 | ||
| 148 | if (typeof buffer === 'string') buffer = Buffer.from(buffer, encoding) | |
| 149 | ||
| 150 | assert(ArrayBuffer.isView(buffer)) | |
| 151 | ||
| 152 | this._additionalData = buffer | |
| 153 | } | |
| 154 | ||
| 155 | getAuthTag() { | |
| 156 | return this._authTag | |
| 157 | } | |
| 158 | ||
| 159 | setAuthTag(authTag, encoding) { | |
| 160 | if (typeof authTag === 'string') authTag = Buffer.from(authTag, encoding) | |
| 161 | ||
| 162 | assert(ArrayBuffer.isView(authTag)) | |
| 163 | ||
| 164 | this._authTag = authTag | |
| 165 | } | |
| 166 | } | |
| 167 | ||
| 168 | class CryptoAuthenticatedSeal extends CryptoAuthenticatedCipher { | |
| 169 | final(outputEncoding) { | |
| 170 | if (this._handle === null) { | |
| 171 | throw new Error('Cipher has already been finalized') | |
| 172 | } | |
| 173 | ||
| 174 | const data = this._buffer.length === 1 ? this._buffer[0] : Buffer.concat(this._buffer) | |
| 175 | ||
| 176 | const nonce = this._nonce | |
| 177 | const ad = this._additionalData || Buffer.alloc(0) | |
| 178 | ||
| 179 | const out = new ArrayBuffer(data.byteLength + binding.aeadMaxOverhead(this._handle)) | |
| 180 | ||
| 181 | const written = binding.aeadSeal( | |
| 182 | this._handle, | |
| 183 | data.buffer, | |
| 184 | data.byteOffset, | |
| 185 | data.byteLength, | |
| 186 | nonce.buffer, | |
| 187 | nonce.byteOffset, | |
| 188 | nonce.byteLength, | |
| 189 | ad.buffer, | |
| 190 | ad.byteOffset, | |
| 191 | ad.byteLength, | |
| 192 | out | |
| 193 | ) | |
| 194 | ||
| 195 | this._handle = null | |
| 196 | ||
| 197 | const cipherLength = written - this._authTagLength | |
| 198 | ||
| 199 | this._authTag = Buffer.from(out, cipherLength) | |
| 200 | ||
| 201 | const result = Buffer.from(out, 0, cipherLength) | |
| 202 | ||
| 203 | return outputEncoding ? result.toString(outputEncoding) : result | |
| 204 | } | |
| 205 | } | |
| 206 | ||
| 207 | class CryptoAuthenticatedOpen extends CryptoAuthenticatedCipher { | |
| 208 | final(outputEncoding) { | |
| 209 | if (this._handle === null) { | |
| 210 | throw new Error('Decipher has already been finalized') | |
| 211 | } | |
| 212 | ||
| 213 | this._buffer.push(this._authTag) | |
| 214 | ||
| 215 | const data = Buffer.concat(this._buffer) | |
| 216 | ||
| 217 | const nonce = this._nonce | |
| 218 | const ad = this._additionalData || Buffer.alloc(0) | |
| 219 | ||
| 220 | const out = new ArrayBuffer(data.byteLength) | |
| 221 | ||
| 222 | const written = binding.aeadOpen( | |
| 223 | this._handle, | |
| 224 | data.buffer, | |
| 225 | data.byteOffset, | |
| 226 | data.byteLength, | |
| 227 | nonce.buffer, | |
| 228 | nonce.byteOffset, | |
| 229 | nonce.byteLength, | |
| 230 | ad.buffer, | |
| 231 | ad.byteOffset, | |
| 232 | ad.byteLength, | |
| 233 | out | |
| 234 | ) | |
| 235 | ||
| 236 | this._handle = null | |
| 237 | ||
| 238 | const result = Buffer.from(out, 0, written) | |
| 239 | ||
| 240 | return outputEncoding ? result.toString(outputEncoding) : result | |
| 241 | } | |
| 242 | } | |
| 243 | ||
| 244 | exports.Cipheriv = class CryptoCipheriv extends Transform { | |
| 245 | constructor(algorithm, key, iv, opts = {}) { | |
| 246 | super(opts) | |
| 247 | ||
| 248 | algorithm = constants.toCipher(algorithm) | |
| 249 | ||
| 250 | switch (algorithm) { | |
| 251 | case AES128ECB: | |
| 252 | case AES128CBC: | |
| 253 | case AES128CTR: | |
| 254 | case AES128OFB: | |
| 255 | case AES256ECB: | |
| 256 | case AES256CBC: | |
| 257 | case AES256CTR: | |
| 258 | case AES256OFB: | |
| 259 | this._cipher = new CryptoCipher(algorithm, key, iv, true, opts) | |
| 260 | break | |
| 261 | ||
| 262 | case AES128GCM: | |
| 263 | case AES256GCM: | |
| 264 | case CHACHA20POLY1305: | |
| 265 | case XCHACHA20POLY1305: | |
| 266 | this._cipher = new CryptoAuthenticatedSeal(algorithm, key, iv, opts) | |
| 267 | break | |
| 268 | } | |
| 269 | } | |
| 270 | ||
| 271 | update(data, inputEncoding, outputEncoding) { | |
| 272 | return this._cipher.update(data, inputEncoding, outputEncoding) | |
| 273 | } | |
| 274 | ||
| 275 | final(outputEncoding) { | |
| 276 | return this._cipher.final(outputEncoding) | |
| 277 | } | |
| 278 | ||
| 279 | setAutoPadding(pad) { | |
| 280 | this._cipher.setAutoPadding(pad) | |
| 281 | ||
| 282 | return this | |
| 283 | } | |
| 284 | ||
| 285 | setAAD(buffer, opts) { | |
| 286 | this._cipher.setAAD(buffer, opts) | |
| 287 | ||
| 288 | return this | |
| 289 | } | |
| 290 | ||
| 291 | getAuthTag() { | |
| 292 | return this._cipher.getAuthTag() | |
| 293 | } | |
| 294 | ||
| 295 | _transform(data, encoding, cb) { | |
| 296 | this.push(this.update(data)) | |
| 297 | ||
| 298 | cb(null) | |
| 299 | } | |
| 300 | ||
| 301 | _flush(cb) { | |
| 302 | this.push(this.final()) | |
| 303 | ||
| 304 | cb(null) | |
| 305 | } | |
| 306 | } | |
| 307 | ||
| 308 | exports.Decipheriv = class CryptoDeipheriv extends Transform { | |
| 309 | constructor(algorithm, key, iv, opts = {}) { | |
| 310 | super(opts) | |
| 311 | ||
| 312 | algorithm = constants.toCipher(algorithm) | |
| 313 | ||
| 314 | switch (algorithm) { | |
| 315 | case AES128ECB: | |
| 316 | case AES128CBC: | |
| 317 | case AES128CTR: | |
| 318 | case AES128OFB: | |
| 319 | case AES256ECB: | |
| 320 | case AES256CBC: | |
| 321 | case AES256CTR: | |
| 322 | case AES256OFB: | |
| 323 | this._cipher = new CryptoCipher(algorithm, key, iv, false, opts) | |
| 324 | break | |
| 325 | ||
| 326 | case AES128GCM: | |
| 327 | case AES256GCM: | |
| 328 | case CHACHA20POLY1305: | |
| 329 | case XCHACHA20POLY1305: | |
| 330 | this._cipher = new CryptoAuthenticatedOpen(algorithm, key, iv, opts) | |
| 331 | break | |
| 332 | } | |
| 333 | } | |
| 334 | ||
| 335 | update(data, inputEncoding, outputEncoding) { | |
| 336 | return this._cipher.update(data, inputEncoding, outputEncoding) | |
| 337 | } | |
| 338 | ||
| 339 | final(outputEncoding) { | |
| 340 | return this._cipher.final(outputEncoding) | |
| 341 | } | |
| 342 | ||
| 343 | setAutoPadding(pad) { | |
| 344 | this._cipher.setAutoPadding(pad) | |
| 345 | ||
| 346 | return this | |
| 347 | } | |
| 348 | ||
| 349 | setAAD(buffer, opts) { | |
| 350 | this._cipher.setAAD(buffer, opts) | |
| 351 | ||
| 352 | return this | |
| 353 | } | |
| 354 | ||
| 355 | setAuthTag(authTag, encoding) { | |
| 356 | this._cipher.setAuthTag(authTag, encoding) | |
| 357 | ||
| 358 | return this | |
| 359 | } | |
| 360 | ||
| 361 | _transform(data, encoding, cb) { | |
| 362 | this.push(this.update(data)) | |
| 363 | ||
| 364 | cb(null) | |
| 365 | } | |
| 366 | ||
| 367 | _flush(cb) { | |
| 368 | this.push(this.final()) | |
| 369 | ||
| 370 | cb(null) | |
| 371 | } | |
| 372 | } |
| … | … | @@ -0,0 +1,12 @@ |
| 1 | import { type HashAlgorithm } from './hash' | |
| 2 | import { type SignatureAlgorithm } from './signature' | |
| 3 | import { type CipherAlgorithm } from './cipher' | |
| 4 | ||
| 5 | declare const constants: { | |
| 6 | hash: Record<HashAlgorithm, number> | |
| 7 | signature: Record<SignatureAlgorithm, number> | |
| 8 | cipher: Record<CipherAlgorithm, number> | |
| 9 | keyType: Record<SignatureAlgorithm, number> | |
| 10 | } | |
| 11 | ||
| 12 | export = constants |
| … | … | @@ -0,0 +1,99 @@ |
| 1 | const { fail } = require('bare-assert') | |
| 2 | const binding = require('../binding') | |
| 3 | const errors = require('./errors') | |
| 4 | ||
| 5 | module.exports = exports = { | |
| 6 | hash: { | |
| 7 | MD5: binding.MD5, | |
| 8 | SHA1: binding.SHA1, | |
| 9 | SHA256: binding.SHA256, | |
| 10 | SHA384: binding.SHA384, | |
| 11 | SHA512: binding.SHA512, | |
| 12 | BLAKE2B256: binding.BLAKE2B256, | |
| 13 | RIPEMD160: binding.RIPEMD160 | |
| 14 | }, | |
| 15 | signature: { | |
| 16 | ED25519: binding.ED25519 | |
| 17 | }, | |
| 18 | cipher: { | |
| 19 | AES128ECB: binding.AES128ECB, | |
| 20 | AES128CBC: binding.AES128CBC, | |
| 21 | AES128CTR: binding.AES128CTR, | |
| 22 | AES128OFB: binding.AES128OFB, | |
| 23 | AES256ECB: binding.AES256ECB, | |
| 24 | AES256CBC: binding.AES256CBC, | |
| 25 | AES256CTR: binding.AES256CTR, | |
| 26 | AES256OFB: binding.AES256OFB, | |
| 27 | AES128GCM: binding.AES128GCM, | |
| 28 | AES256GCM: binding.AES256GCM, | |
| 29 | CHACHA20POLY1305: binding.CHACHA20POLY1305, | |
| 30 | XCHACHA20POLY1305: binding.XCHACHA20POLY1305 | |
| 31 | }, | |
| 32 | keyType: { | |
| 33 | ED25519: binding.ED25519 | |
| 34 | } | |
| 35 | } | |
| 36 | ||
| 37 | exports.toHash = function toHash(hash) { | |
| 38 | if (typeof hash === 'number' && !isNaN(hash)) return hash | |
| 39 | ||
| 40 | if (typeof hash === 'string') { | |
| 41 | hash = hash.replace(/-/g, '') | |
| 42 | ||
| 43 | if (hash in exports.hash === false) { | |
| 44 | hash = hash.toUpperCase() | |
| 45 | ||
| 46 | if (hash in exports.hash === false) { | |
| 47 | throw errors.UNKNOWN_HASH(`Unknown hash '${hash}'`) | |
| 48 | } | |
| 49 | } | |
| 50 | ||
| 51 | return exports.hash[hash] | |
| 52 | } | |
| 53 | ||
| 54 | fail(`Hash must be a number or string. Received ${isNaN(hash) ? 'NaN' : typeof hash} (${hash})`) | |
| 55 | } | |
| 56 | ||
| 57 | exports.toCipher = function toCiper(cipher) { | |
| 58 | if (typeof cipher === 'number' && !isNaN(cipher)) return cipher | |
| 59 | ||
| 60 | if (typeof cipher === 'string') { | |
| 61 | cipher = cipher.replace(/-/g, '') | |
| 62 | ||
| 63 | if (cipher in exports.cipher === false) { | |
| 64 | cipher = cipher.toUpperCase() | |
| 65 | ||
| 66 | if (cipher in exports.cipher === false) { | |
| 67 | throw errors.UNKNOWN_CIPHER(`Unknown cipher '${cipher}'`) | |
| 68 | } | |
| 69 | } | |
| 70 | ||
| 71 | return exports.cipher[cipher] | |
| 72 | } | |
| 73 | ||
| 74 | fail( | |
| 75 | `Cipher must be a number or string. Received ${isNaN(cipher) ? 'NaN' : typeof cipher} (${cipher})` | |
| 76 | ) | |
| 77 | } | |
| 78 | ||
| 79 | exports.toKeyType = function toKeyType(type) { | |
| 80 | if (typeof type === 'number' && !isNaN(type)) return type | |
| 81 | ||
| 82 | if (typeof type === 'string') { | |
| 83 | type = type.replace(/-/g, '') | |
| 84 | ||
| 85 | if (type in exports.keyType === false) { | |
| 86 | type = type.toUpperCase() | |
| 87 | ||
| 88 | if (type in exports.keyType === false) { | |
| 89 | throw errors.UNKNOWN_KEY_TYPE(`Unknown key type '${type}'`) | |
| 90 | } | |
| 91 | } | |
| 92 | ||
| 93 | return exports.keyType[type] | |
| 94 | } | |
| 95 | ||
| 96 | fail( | |
| 97 | `Key type must be a number or string. Received ${isNaN(type) ? 'NaN' : typeof type} (${type})` | |
| 98 | ) | |
| 99 | } |
| … | … | @@ -0,0 +1,11 @@ |
| 1 | declare class CryptoError extends Error { | |
| 2 | static UNKNOWN_HASH(msg: string): CryptoError | |
| 3 | static UNKNOWN_CIPHER(msg: string): CryptoError | |
| 4 | static UNKNOWN_KEY_TYPE(msg: string): CryptoError | |
| 5 | static INVALID_ACCESS(msg: string): CryptoError | |
| 6 | static INVALID_DATA(msg: string): CryptoError | |
| 7 | static OPERATION_ERROR(msg: string): CryptoError | |
| 8 | static NOT_SUPPORTED(msg: string): CryptoError | |
| 9 | } | |
| 10 | ||
| 11 | export = CryptoError |
| … | … | @@ -0,0 +1,42 @@ |
| 1 | module.exports = class CryptoError extends Error { | |
| 2 | constructor(msg, fn = CryptoError, code = fn.name) { | |
| 3 | super(`${code}: ${msg}`) | |
| 4 | this.code = code | |
| 5 | ||
| 6 | if (Error.captureStackTrace) { | |
| 7 | Error.captureStackTrace(this, fn) | |
| 8 | } | |
| 9 | } | |
| 10 | ||
| 11 | get name() { | |
| 12 | return 'CryptoError' | |
| 13 | } | |
| 14 | ||
| 15 | static UNKNOWN_HASH(msg) { | |
| 16 | return new CryptoError(msg, CryptoError.UNKNOWN_HASH) | |
| 17 | } | |
| 18 | ||
| 19 | static UNKNOWN_CIPHER(msg) { | |
| 20 | return new CryptoError(msg, CryptoError.UNKNOWN_CIPHER) | |
| 21 | } | |
| 22 | ||
| 23 | static UNKNOWN_KEY_TYPE(msg) { | |
| 24 | return new CryptoError(msg, CryptoError.UNKNOWN_KEY_TYPE) | |
| 25 | } | |
| 26 | ||
| 27 | static INVALID_ACCESS(msg) { | |
| 28 | return new CryptoError(msg, CryptoError.INVALID_ACCESS) | |
| 29 | } | |
| 30 | ||
| 31 | static INVALID_DATA(msg) { | |
| 32 | return new CryptoError(msg, CryptoError.INVALID_DATA) | |
| 33 | } | |
| 34 | ||
| 35 | static OPERATION_ERROR(msg) { | |
| 36 | return new CryptoError(msg, CryptoError.OPERATION_ERROR) | |
| 37 | } | |
| 38 | ||
| 39 | static NOT_SUPPORTED(msg) { | |
| 40 | return new CryptoError(msg, CryptoError.NOT_SUPPORTED) | |
| 41 | } | |
| 42 | } |
| … | … | @@ -0,0 +1,20 @@ |
| 1 | import { Transform, TransformOptions } from 'bare-stream' | |
| 2 | import Buffer, { BufferEncoding } from 'bare-buffer' | |
| 3 | ||
| 4 | type HashAlgorithm = 'md5' | 'sha-1' | 'sha-256' | 'sha-512' | 'blake2b-256' | 'ripemd-160' | |
| 5 | ||
| 6 | declare class CryptoHash extends Transform { | |
| 7 | constructor(algorithm: HashAlgorithm | number, opts?: TransformOptions<CryptoHash>) | |
| 8 | ||
| 9 | update(data: string, encoding?: BufferEncoding): this | |
| 10 | update(data: Buffer, encoding?: 'buffer'): this | |
| 11 | ||
| 12 | digest(encoding: BufferEncoding): string | |
| 13 | digest(): Buffer | |
| 14 | } | |
| 15 | ||
| 16 | declare namespace CryptoHash { | |
| 17 | export { type HashAlgorithm } | |
| 18 | } | |
| 19 | ||
| 20 | export = CryptoHash |
| … | … | @@ -0,0 +1,102 @@ |
| 1 | const { Transform } = require('bare-stream') | |
| 2 | const assert = require('bare-assert') | |
| 3 | const binding = require('../binding') | |
| 4 | const constants = require('./constants') | |
| 5 | ||
| 6 | const { | |
| 7 | hash: { RIPEMD160 } | |
| 8 | } = constants | |
| 9 | ||
| 10 | class CryptoDigest { | |
| 11 | constructor(algorithm) { | |
| 12 | this._handle = binding.digestInit(algorithm) | |
| 13 | } | |
| 14 | ||
| 15 | update(data) { | |
| 16 | if (this._handle === null) { | |
| 17 | throw new Error('Digest has already been finalized') | |
| 18 | } | |
| 19 | ||
| 20 | binding.digestUpdate(this._handle, data.buffer, data.byteOffset, data.byteLength) | |
| 21 | } | |
| 22 | ||
| 23 | final() { | |
| 24 | if (this._handle === null) { | |
| 25 | throw new Error('Digest has already been finalized') | |
| 26 | } | |
| 27 | ||
| 28 | const result = Buffer.from(binding.digestFinal(this._handle)) | |
| 29 | this._handle = null | |
| 30 | return result | |
| 31 | } | |
| 32 | } | |
| 33 | ||
| 34 | class CryptoRIPEMD160Digest { | |
| 35 | constructor() { | |
| 36 | this._handle = binding.ripemd160Init() | |
| 37 | } | |
| 38 | ||
| 39 | update(data) { | |
| 40 | if (this._handle === null) { | |
| 41 | throw new Error('Digest has already been finalized') | |
| 42 | } | |
| 43 | ||
| 44 | binding.ripemd160Update(this._handle, data.buffer, data.byteOffset, data.byteLength) | |
| 45 | } | |
| 46 | ||
| 47 | final() { | |
| 48 | if (this._handle === null) { | |
| 49 | throw new Error('Digest has already been finalized') | |
| 50 | } | |
| 51 | ||
| 52 | const result = Buffer.from(binding.ripemd160Final(this._handle)) | |
| 53 | this._handle = null | |
| 54 | return result | |
| 55 | } | |
| 56 | } | |
| 57 | ||
| 58 | module.exports = class CryptoHash extends Transform { | |
| 59 | constructor(algorithm, opts = {}) { | |
| 60 | super(opts) | |
| 61 | ||
| 62 | algorithm = constants.toHash(algorithm) | |
| 63 | ||
| 64 | switch (algorithm) { | |
| 65 | case RIPEMD160: | |
| 66 | this._digest = new CryptoRIPEMD160Digest() | |
| 67 | break | |
| 68 | ||
| 69 | default: | |
| 70 | this._digest = new CryptoDigest(algorithm) | |
| 71 | break | |
| 72 | } | |
| 73 | } | |
| 74 | ||
| 75 | update(data, encoding = 'utf8') { | |
| 76 | if (typeof data === 'string') data = Buffer.from(data, encoding) | |
| 77 | ||
| 78 | assert(ArrayBuffer.isView(data)) | |
| 79 | ||
| 80 | this._digest.update(data) | |
| 81 | ||
| 82 | return this | |
| 83 | } | |
| 84 | ||
| 85 | digest(encoding) { | |
| 86 | const digest = this._digest.final() | |
| 87 | ||
| 88 | return encoding && encoding !== 'buffer' ? digest.toString(encoding) : digest | |
| 89 | } | |
| 90 | ||
| 91 | _transform(data, encoding, cb) { | |
| 92 | this.update(data) | |
| 93 | ||
| 94 | cb(null) | |
| 95 | } | |
| 96 | ||
| 97 | _flush(cb) { | |
| 98 | this.push(this.digest()) | |
| 99 | ||
| 100 | cb(null) | |
| 101 | } | |
| 102 | } |
| … | … | @@ -0,0 +1,19 @@ |
| 1 | import { Transform, TransformOptions } from 'bare-stream' | |
| 2 | import Buffer, { BufferEncoding } from 'bare-buffer' | |
| 3 | import { type HashAlgorithm } from './hash' | |
| 4 | ||
| 5 | declare class CryptoHmac extends Transform { | |
| 6 | constructor( | |
| 7 | algorithm: HashAlgorithm | number, | |
| 8 | key: string | Buffer, | |
| 9 | opts?: TransformOptions<CryptoHmac> | |
| 10 | ) | |
| 11 | ||
| 12 | update(data: string, encoding?: BufferEncoding): this | |
| 13 | update(data: Buffer, encoding?: 'buffer'): this | |
| 14 | ||
| 15 | digest(encoding: BufferEncoding): string | |
| 16 | digest(): Buffer | |
| 17 | } | |
| 18 | ||
| 19 | export = CryptoHmac |
| … | … | @@ -0,0 +1,60 @@ |
| 1 | const { Transform } = require('bare-stream') | |
| 2 | const assert = require('bare-assert') | |
| 3 | const binding = require('../binding') | |
| 4 | const constants = require('./constants') | |
| 5 | ||
| 6 | module.exports = class CryptoHmac extends Transform { | |
| 7 | constructor(algorithm, key, opts = {}) { | |
| 8 | super(opts) | |
| 9 | ||
| 10 | const { encoding = 'utf8' } = opts | |
| 11 | ||
| 12 | if (typeof key === 'string') key = Buffer.from(key, encoding) | |
| 13 | ||
| 14 | assert(ArrayBuffer.isView(key)) | |
| 15 | ||
| 16 | this._handle = binding.hmacInit( | |
| 17 | constants.toHash(algorithm), | |
| 18 | key.buffer, | |
| 19 | key.byteOffset, | |
| 20 | key.byteLength | |
| 21 | ) | |
| 22 | } | |
| 23 | ||
| 24 | update(data, encoding = 'utf8') { | |
| 25 | if (this._handle === null) { | |
| 26 | throw new Error('Hmac has already been finalized') | |
| 27 | } | |
| 28 | ||
| 29 | if (typeof data === 'string') data = Buffer.from(data, encoding) | |
| 30 | ||
| 31 | assert(ArrayBuffer.isView(data)) | |
| 32 | ||
| 33 | binding.hmacUpdate(this._handle, data.buffer, data.byteOffset, data.byteLength) | |
| 34 | ||
| 35 | return this | |
| 36 | } | |
| 37 | ||
| 38 | digest(encoding) { | |
| 39 | if (this._handle === null) { | |
| 40 | throw new Error('Hmac has already been finalized') | |
| 41 | } | |
| 42 | ||
| 43 | const digest = Buffer.from(binding.hmacFinal(this._handle)) | |
| 44 | this._handle = null | |
| 45 | ||
| 46 | return encoding && encoding !== 'buffer' ? digest.toString(encoding) : digest | |
| 47 | } | |
| 48 | ||
| 49 | _transform(data, encoding, cb) { | |
| 50 | this.update(data) | |
| 51 | ||
| 52 | cb(null) | |
| 53 | } | |
| 54 | ||
| 55 | _flush(cb) { | |
| 56 | this.push(this.digest()) | |
| 57 | ||
| 58 | cb(null) | |
| 59 | } | |
| 60 | } |
| … | … | @@ -0,0 +1,27 @@ |
| 1 | import { type SignatureAlgorithm } from './signature' | |
| 2 | ||
| 3 | declare class CryptoKey {} | |
| 4 | ||
| 5 | declare class CryptoEd25519Key extends CryptoKey { | |
| 6 | readonly asymmetricKeyType: 'ed25519' | |
| 7 | } | |
| 8 | ||
| 9 | declare class CryptoEd25519PublicKey extends CryptoEd25519Key { | |
| 10 | readonly type: 'public' | |
| 11 | } | |
| 12 | ||
| 13 | declare class CryptoEd25519PrivateKey extends CryptoEd25519Key { | |
| 14 | readonly type: 'private' | |
| 15 | } | |
| 16 | ||
| 17 | declare function generateKeyPair(type: SignatureAlgorithm | Lowercase<SignatureAlgorithm>): { | |
| 18 | publicKey: CryptoKey | |
| 19 | privateKey: CryptoKey | |
| 20 | } | |
| 21 | ||
| 22 | export { | |
| 23 | CryptoKey as Key, | |
| 24 | CryptoEd25519PublicKey as Ed25519PublicKey, | |
| 25 | CryptoEd25519PrivateKey as Ed25519PrivateKey, | |
| 26 | generateKeyPair | |
| 27 | } |
| … | … | @@ -0,0 +1,57 @@ |
| 1 | const binding = require('../binding') | |
| 2 | const constants = require('./constants') | |
| 3 | ||
| 4 | const { | |
| 5 | keyType: { ED25519 } | |
| 6 | } = constants | |
| 7 | ||
| 8 | class CryptoKey { | |
| 9 | constructor(keyType) { | |
| 10 | this._keyType = keyType | |
| 11 | } | |
| 12 | } | |
| 13 | ||
| 14 | exports.Key = CryptoKey | |
| 15 | ||
| 16 | class CryptoEd25519Key extends CryptoKey { | |
| 17 | constructor(key) { | |
| 18 | super(ED25519) | |
| 19 | ||
| 20 | this._key = key | |
| 21 | } | |
| 22 | ||
| 23 | get asymmetricKeyType() { | |
| 24 | return 'ed25519' | |
| 25 | } | |
| 26 | } | |
| 27 | ||
| 28 | class CryptoEd25519PublicKey extends CryptoEd25519Key { | |
| 29 | get type() { | |
| 30 | return 'public' | |
| 31 | } | |
| 32 | } | |
| 33 | ||
| 34 | exports.Ed25519PublicKey = CryptoEd25519PublicKey | |
| 35 | ||
| 36 | class CryptoEd25519PrivateKey extends CryptoEd25519Key { | |
| 37 | get type() { | |
| 38 | return 'private' | |
| 39 | } | |
| 40 | } | |
| 41 | ||
| 42 | exports.Ed25519PrivateKey = CryptoEd25519PrivateKey | |
| 43 | ||
| 44 | exports.generateKeyPair = function generateKeyPair(type, opts = {}) { | |
| 45 | type = constants.toKeyType(type) | |
| 46 | ||
| 47 | switch (type) { | |
| 48 | case ED25519: { | |
| 49 | const { publicKey, privateKey } = binding.ed25519GenerateKeypair() | |
| 50 | ||
| 51 | return { | |
| 52 | publicKey: new CryptoEd25519PublicKey(publicKey), | |
| 53 | privateKey: new CryptoEd25519PrivateKey(privateKey) | |
| 54 | } | |
| 55 | } | |
| 56 | } | |
| 57 | } |
| … | … | @@ -0,0 +1,21 @@ |
| 1 | import Buffer from 'bare-buffer' | |
| 2 | import { type HashAlgorithm } from './hash' | |
| 3 | ||
| 4 | declare function pbkdf2( | |
| 5 | password: string | ArrayBuffer | ArrayBufferView, | |
| 6 | salt: string | ArrayBuffer | ArrayBufferView, | |
| 7 | iterations: number, | |
| 8 | keylen: number, | |
| 9 | digest: HashAlgorithm | number | |
| 10 | ): Buffer | |
| 11 | ||
| 12 | declare function pbkdf2( | |
| 13 | password: string | ArrayBuffer | ArrayBufferView, | |
| 14 | salt: string | ArrayBuffer | ArrayBufferView, | |
| 15 | iterations: number, | |
| 16 | keylen: number, | |
| 17 | digest: HashAlgorithm | number, | |
| 18 | callback: (err: Error | null, buffer: Buffer) => void | |
| 19 | ): void | |
| 20 | ||
| 21 | export = pbkdf2 |
| … | … | @@ -0,0 +1,35 @@ |
| 1 | const assert = require('bare-assert') | |
| 2 | const binding = require('../binding') | |
| 3 | const constants = require('./constants') | |
| 4 | ||
| 5 | module.exports = function pbkdf2(password, salt, iterations, keylen, digest, cb) { | |
| 6 | if (iterations <= 0) { | |
| 7 | throw new RangeError('iterations is out of range') | |
| 8 | } | |
| 9 | ||
| 10 | assert(typeof iterations === 'number' && !isNaN(iterations)) | |
| 11 | assert(typeof keylen === 'number' && !isNaN(keylen)) | |
| 12 | ||
| 13 | if (typeof password === 'string') password = Buffer.from(password) | |
| 14 | if (typeof salt === 'string') salt = Buffer.from(salt) | |
| 15 | ||
| 16 | assert(ArrayBuffer.isView(password)) | |
| 17 | assert(ArrayBuffer.isView(salt)) | |
| 18 | ||
| 19 | const buffer = Buffer.from( | |
| 20 | binding.pbkdf2( | |
| 21 | password.buffer, | |
| 22 | password.byteOffset, | |
| 23 | password.byteLength, | |
| 24 | salt.buffer, | |
| 25 | salt.byteOffset, | |
| 26 | salt.byteLength, | |
| 27 | iterations, | |
| 28 | constants.toHash(digest), | |
| 29 | keylen | |
| 30 | ) | |
| 31 | ) | |
| 32 | ||
| 33 | if (cb) queueMicrotask(() => cb(null, buffer)) | |
| 34 | else return buffer | |
| 35 | } |
| … | … | @@ -0,0 +1,34 @@ |
| 1 | import Buffer from 'bare-buffer' | |
| 2 | ||
| 3 | export function randomBytes(size: number): Buffer | |
| 4 | ||
| 5 | export function randomBytes( | |
| 6 | size: number, | |
| 7 | callback: (err: Error | null, buffer: Buffer) => void | |
| 8 | ): void | |
| 9 | ||
| 10 | export function randomFill<B extends ArrayBuffer | ArrayBufferView>( | |
| 11 | buffer: B, | |
| 12 | offset?: number, | |
| 13 | size?: number | |
| 14 | ): B | |
| 15 | ||
| 16 | export function randomFill<B extends ArrayBuffer | ArrayBufferView>( | |
| 17 | buffer: B, | |
| 18 | callback: (err: Error | null, buffer: B) => void | |
| 19 | ): void | |
| 20 | ||
| 21 | export function randomFill<B extends ArrayBuffer | ArrayBufferView>( | |
| 22 | buffer: B, | |
| 23 | offset: number, | |
| 24 | callback: (err: Error | null, buffer: B) => void | |
| 25 | ): void | |
| 26 | ||
| 27 | export function randomFill<B extends ArrayBuffer | ArrayBufferView>( | |
| 28 | buffer: B, | |
| 29 | offset: number, | |
| 30 | size: number, | |
| 31 | callback: (err: Error | null, buffer: B) => void | |
| 32 | ): void | |
| 33 | ||
| 34 | export function randomUUID(): string |
| … | … | @@ -0,0 +1,78 @@ |
| 1 | const assert = require('bare-assert') | |
| 2 | const binding = require('../binding') | |
| 3 | ||
| 4 | exports.randomBytes = function randomBytes(size, cb) { | |
| 5 | assert(typeof size === 'number' && !isNaN(size)) | |
| 6 | ||
| 7 | const buffer = Buffer.allocUnsafe(size) | |
| 8 | exports.randomFill(buffer) | |
| 9 | if (cb) queueMicrotask(() => cb(null, buffer)) | |
| 10 | else return buffer | |
| 11 | } | |
| 12 | ||
| 13 | exports.randomFill = function randomFill(buffer, offset, size, cb) { | |
| 14 | if (typeof offset === 'function') { | |
| 15 | cb = offset | |
| 16 | offset = undefined | |
| 17 | } else if (typeof size === 'function') { | |
| 18 | cb = size | |
| 19 | size = undefined | |
| 20 | } | |
| 21 | ||
| 22 | assert(buffer instanceof ArrayBuffer || ArrayBuffer.isView(buffer)) | |
| 23 | assert(size === undefined || (typeof size === 'number' && !isNaN(size))) | |
| 24 | assert(offset === undefined || (typeof offset === 'number' && !isNaN(offset))) | |
| 25 | ||
| 26 | const elementSize = buffer.BYTES_PER_ELEMENT || 1 | |
| 27 | ||
| 28 | if (offset === undefined) offset = 0 | |
| 29 | else offset *= elementSize | |
| 30 | ||
| 31 | if (size === undefined) size = buffer.byteLength - offset | |
| 32 | else size *= elementSize | |
| 33 | ||
| 34 | if (offset < 0 || offset > buffer.byteLength) { | |
| 35 | throw new RangeError('offset is out of range') | |
| 36 | } | |
| 37 | ||
| 38 | if (size < 0 || size > buffer.byteLength) { | |
| 39 | throw new RangeError('size is out of range') | |
| 40 | } | |
| 41 | ||
| 42 | if (offset + size > buffer.byteLength) { | |
| 43 | throw new RangeError('offset + size is out of range') | |
| 44 | } | |
| 45 | ||
| 46 | let arraybuffer | |
| 47 | ||
| 48 | if (ArrayBuffer.isView(buffer)) { | |
| 49 | offset += buffer.byteOffset | |
| 50 | arraybuffer = buffer.buffer | |
| 51 | } else { | |
| 52 | arraybuffer = buffer | |
| 53 | } | |
| 54 | ||
| 55 | binding.randomFill(arraybuffer, offset, size) | |
| 56 | ||
| 57 | if (cb) queueMicrotask(() => cb(null, buffer)) | |
| 58 | else return buffer | |
| 59 | } | |
| 60 | ||
| 61 | exports.randomUUID = function randomUUID() { | |
| 62 | const uuid = exports.randomBytes(16) | |
| 63 | ||
| 64 | uuid[6] = (uuid[6] >>> 4) | 0b01000000 | |
| 65 | uuid[8] = (uuid[8] >>> 2) | 0b10000000 | |
| 66 | ||
| 67 | return ( | |
| 68 | uuid.subarray(0, 4).toString('hex') + | |
| 69 | '-' + | |
| 70 | uuid.subarray(4, 6).toString('hex') + | |
| 71 | '-' + | |
| 72 | uuid.subarray(6, 8).toString('hex') + | |
| 73 | '-' + | |
| 74 | uuid.subarray(8, 10).toString('hex') + | |
| 75 | '-' + | |
| 76 | uuid.subarray(10, 16).toString('hex') | |
| 77 | ) | |
| 78 | } |
| … | … | @@ -0,0 +1,13 @@ |
| 1 | import Buffer from 'bare-buffer' | |
| 2 | import { Key as CryptoKey } from './key' | |
| 3 | ||
| 4 | export type SignatureAlgorithm = 'ed25519' | |
| 5 | ||
| 6 | export function sign(algorithm: null, data: ArrayBuffer | ArrayBufferView, key: CryptoKey): Buffer | |
| 7 | ||
| 8 | export function verify( | |
| 9 | algorithm: null, | |
| 10 | data: ArrayBuffer | ArrayBufferView, | |
| 11 | key: CryptoKey, | |
| 12 | signature: Buffer | |
| 13 | ): boolean |
| … | … | @@ -0,0 +1,61 @@ |
| 1 | const assert = require('bare-assert') | |
| 2 | const binding = require('../binding') | |
| 3 | const { Key } = require('./key') | |
| 4 | const constants = require('./constants') | |
| 5 | ||
| 6 | const { | |
| 7 | keyType: { ED25519 } | |
| 8 | } = constants | |
| 9 | ||
| 10 | exports.sign = function sign(algorithm, data, key) { | |
| 11 | assert(data instanceof ArrayBuffer || ArrayBuffer.isView(data)) | |
| 12 | ||
| 13 | if (ArrayBuffer.isView(data)) { | |
| 14 | data = Buffer.coerce(data) | |
| 15 | } else { | |
| 16 | data = Buffer.from(data) | |
| 17 | } | |
| 18 | ||
| 19 | assert(key instanceof Key) | |
| 20 | ||
| 21 | switch (key._keyType) { | |
| 22 | case ED25519: | |
| 23 | return Buffer.from( | |
| 24 | binding.ed25519Sign(data.buffer, data.byteOffset, data.byteLength, key._key) | |
| 25 | ) | |
| 26 | } | |
| 27 | } | |
| 28 | ||
| 29 | exports.verify = function verify(algorithm, data, key, signature) { | |
| 30 | assert(data instanceof ArrayBuffer || ArrayBuffer.isView(data)) | |
| 31 | ||
| 32 | if (ArrayBuffer.isView(data)) { | |
| 33 | data = Buffer.coerce(data) | |
| 34 | } else { | |
| 35 | data = Buffer.from(data) | |
| 36 | } | |
| 37 | ||
| 38 | assert(signature instanceof ArrayBuffer || ArrayBuffer.isView(signature)) | |
| 39 | ||
| 40 | if (ArrayBuffer.isView(signature)) { | |
| 41 | signature = Buffer.coerce(signature) | |
| 42 | } else { | |
| 43 | signature = Buffer.from(signature) | |
| 44 | } | |
| 45 | ||
| 46 | assert(key instanceof Key) | |
| 47 | ||
| 48 | switch (key._keyType) { | |
| 49 | case ED25519: | |
| 50 | assert(signature.byteLength === 64) | |
| 51 | ||
| 52 | return binding.ed25519Verify( | |
| 53 | data.buffer, | |
| 54 | data.byteOffset, | |
| 55 | data.byteLength, | |
| 56 | signature.buffer, | |
| 57 | signature.byteOffset, | |
| 58 | key._key | |
| 59 | ) | |
| 60 | } | |
| 61 | } |
| … | … | @@ -0,0 +1,252 @@ |
| 1 | const crypto = require('../../..') | |
| 2 | const binding = require('../../../binding') | |
| 3 | const errors = require('../../errors') | |
| 4 | const { Ed25519PublicKey, Ed25519PrivateKey } = require('../../key') | |
| 5 | const CryptoKey = require('../crypto-key') | |
| 6 | ||
| 7 | // https://w3c.github.io/webcrypto/#ed25519 | |
| 8 | ||
| 9 | // https://w3c.github.io/webcrypto/#ed25519-operations-sign | |
| 10 | exports.sign = function sign(algorithm, key, data) { | |
| 11 | if (key.type !== 'private') { | |
| 12 | throw errors.INVALID_ACCESS('Must pass private key for Ed25519 signing') | |
| 13 | } | |
| 14 | ||
| 15 | const signature = crypto.sign(null, data, key._handle) | |
| 16 | ||
| 17 | return signature.buffer.slice(0, signature.byteLength) | |
| 18 | } | |
| 19 | ||
| 20 | // https://w3c.github.io/webcrypto/#ed25519-operations-verify | |
| 21 | exports.verify = function verify(algorithm, key, signature, data) { | |
| 22 | if (key.type !== 'public') { | |
| 23 | throw errors.INVALID_ACCESS('Must pass public key for Ed25519 verification') | |
| 24 | } | |
| 25 | ||
| 26 | return crypto.verify(null, data, key._handle, signature) | |
| 27 | } | |
| 28 | ||
| 29 | // https://w3c.github.io/webcrypto/#ed25519-operations-generate-key | |
| 30 | exports.generateKey = function generateKey(algorithm, extractable, usages) { | |
| 31 | for (const usage of usages) { | |
| 32 | if (usage !== 'sign' && usage !== 'verify') { | |
| 33 | throw new SyntaxError( | |
| 34 | `Usage '${usage}' cannot be used for the Ed25519 generateKey() operation` | |
| 35 | ) | |
| 36 | } | |
| 37 | } | |
| 38 | ||
| 39 | const keys = crypto.generateKeyPair('ed25519') | |
| 40 | ||
| 41 | algorithm = { name: 'Ed25519' } | |
| 42 | ||
| 43 | return { | |
| 44 | publicKey: new CryptoKey('public', true, algorithm, ['verify'], keys.publicKey), | |
| 45 | privateKey: new CryptoKey('private', extractable, algorithm, ['sign'], keys.privateKey) | |
| 46 | } | |
| 47 | } | |
| 48 | ||
| 49 | // https://w3c.github.io/webcrypto/#ed25519-operations-import-key | |
| 50 | exports.importKey = function importKey(format, keyData, algorithm, extractable, usages) { | |
| 51 | switch (format) { | |
| 52 | case 'spki': | |
| 53 | for (const usage of usages) { | |
| 54 | if (usage !== 'verify') { | |
| 55 | throw new SyntaxError( | |
| 56 | `Usage '${usage}' cannot be used for the Ed25519 importKey() operation` | |
| 57 | ) | |
| 58 | } | |
| 59 | } | |
| 60 | ||
| 61 | keyData = binding.ed25519FromSPKI(keyData.buffer, keyData.byteOffset, keyData.byteLength) | |
| 62 | ||
| 63 | return new CryptoKey( | |
| 64 | 'public', | |
| 65 | extractable, | |
| 66 | { | |
| 67 | name: 'Ed25519' | |
| 68 | }, | |
| 69 | usages, | |
| 70 | new Ed25519PublicKey(keyData) | |
| 71 | ) | |
| 72 | case 'pkcs8': | |
| 73 | for (const usage of usages) { | |
| 74 | if (usage !== 'sign') { | |
| 75 | throw new SyntaxError( | |
| 76 | `Usage '${usage}' cannot be used for the Ed25519 importKey() operation` | |
| 77 | ) | |
| 78 | } | |
| 79 | } | |
| 80 | ||
| 81 | keyData = binding.ed25519FromPKCS8(keyData.buffer, keyData.byteOffset, keyData.byteLength) | |
| 82 | ||
| 83 | return new CryptoKey( | |
| 84 | 'private', | |
| 85 | extractable, | |
| 86 | { | |
| 87 | name: 'Ed25519' | |
| 88 | }, | |
| 89 | usages, | |
| 90 | new Ed25519PrivateKey(keyData) | |
| 91 | ) | |
| 92 | case 'raw': | |
| 93 | for (const usage of usages) { | |
| 94 | if (usage !== 'verify') { | |
| 95 | throw new SyntaxError( | |
| 96 | `Usage '${usage}' cannot be used for the Ed25519 importKey() operation` | |
| 97 | ) | |
| 98 | } | |
| 99 | } | |
| 100 | ||
| 101 | if (keyData.byteLength * 8 !== 256) { | |
| 102 | throw errors.INVALID_DATA('Key must be 256 bits') | |
| 103 | } | |
| 104 | ||
| 105 | return new CryptoKey( | |
| 106 | 'public', | |
| 107 | extractable, | |
| 108 | { | |
| 109 | name: 'Ed25519' | |
| 110 | }, | |
| 111 | usages, | |
| 112 | new Ed25519PublicKey(keyData.buffer) | |
| 113 | ) | |
| 114 | case 'jwk': | |
| 115 | const jwk = keyData | |
| 116 | ||
| 117 | if ('d' in jwk) { | |
| 118 | if (usages.some((usage) => usage !== 'sign')) { | |
| 119 | throw new SyntaxError('JWK must be valid for signing') | |
| 120 | } | |
| 121 | } else { | |
| 122 | if (usages.some((usage) => usage !== 'verify')) { | |
| 123 | throw new SyntaxError('JWK must be valid for verification') | |
| 124 | } | |
| 125 | } | |
| 126 | ||
| 127 | if (jwk.kty !== 'OKP') { | |
| 128 | throw errors.INVALID_DATA('JWK key must be an octet key-pair') | |
| 129 | } | |
| 130 | ||
| 131 | if (jwk.crv !== 'Ed25519') { | |
| 132 | throw errors.INVALID_DATA('JWK must use the Ed25519 curve') | |
| 133 | } | |
| 134 | ||
| 135 | if ('alg' in jwk && jwk.alg !== 'Ed25519' && jwk.alg !== 'EdDSA') { | |
| 136 | throw errors.INVALID_DATA('JWK must use the Ed25519 curve') | |
| 137 | } | |
| 138 | ||
| 139 | if (usages.length && 'use' in jwk && jwk.use !== 'sig') { | |
| 140 | throw errors.INVALID_DATA('JWK cannot be used for signatures') | |
| 141 | } | |
| 142 | ||
| 143 | if ('ext' in jwk && jwk.ext !== extractable && extractable) { | |
| 144 | throw errors.INVALID_DATA('JWK is not extractable') | |
| 145 | } | |
| 146 | ||
| 147 | if ('d' in jwk) { | |
| 148 | const key = Buffer.concat([ | |
| 149 | Buffer.from(jwk.d, 'base64url'), | |
| 150 | Buffer.from(jwk.x, 'base64url') | |
| 151 | ]) | |
| 152 | ||
| 153 | if (key.byteLength * 8 !== 512) { | |
| 154 | throw errors.INVALID_DATA('Key must be 512 bits') | |
| 155 | } | |
| 156 | ||
| 157 | return new CryptoKey( | |
| 158 | 'private', | |
| 159 | extractable, | |
| 160 | { | |
| 161 | name: 'Ed25519' | |
| 162 | }, | |
| 163 | usages, | |
| 164 | new Ed25519PrivateKey(key.buffer) | |
| 165 | ) | |
| 166 | } | |
| 167 | ||
| 168 | const key = Buffer.from(jwk.x, 'base64url') | |
| 169 | ||
| 170 | if (key.byteLength * 8 !== 256) { | |
| 171 | throw errors.INVALID_DATA('Key must be 256 bits') | |
| 172 | } | |
| 173 | ||
| 174 | return new CryptoKey( | |
| 175 | 'public', | |
| 176 | extractable, | |
| 177 | { | |
| 178 | name: 'Ed25519' | |
| 179 | }, | |
| 180 | usages, | |
| 181 | new Ed25519PublicKey(key.buffer) | |
| 182 | ) | |
| 183 | default: | |
| 184 | throw errors.NOT_SUPPORTED( | |
| 185 | `Format '${format}' cannot be used for the Ed25519 importKey() operation` | |
| 186 | ) | |
| 187 | } | |
| 188 | } | |
| 189 | ||
| 190 | // https://w3c.github.io/webcrypto/#ed25519-operations-export-key | |
| 191 | exports.exportKey = function exportKey(format, key) { | |
| 192 | const data = key._handle | |
| 193 | ||
| 194 | switch (format) { | |
| 195 | case 'spki': | |
| 196 | if (key.type !== 'public') { | |
| 197 | throw errors.INVALID_ACCESS( | |
| 198 | `Key of type '${key.type}' cannot be used for the Ed25519 exportKey() operation` | |
| 199 | ) | |
| 200 | } | |
| 201 | ||
| 202 | return binding.ed25519ToSPKI(data._key) | |
| 203 | case 'pkcs8': | |
| 204 | if (key.type !== 'private') { | |
| 205 | throw errors.INVALID_ACCESS( | |
| 206 | `Key of type '${key.type}' cannot be used for the Ed25519 exportKey() operation` | |
| 207 | ) | |
| 208 | } | |
| 209 | ||
| 210 | return binding.ed25519ToPKCS8(data._key) | |
| 211 | case 'raw': { | |
| 212 | if (key.type !== 'public') { | |
| 213 | throw errors.INVALID_ACCESS( | |
| 214 | `Key of type '${key.type}' cannot be used for the Ed25519 exportKey() operation` | |
| 215 | ) | |
| 216 | } | |
| 217 | ||
| 218 | return data._key.slice() | |
| 219 | } | |
| 220 | case 'jwk': { | |
| 221 | const buffer = Buffer.from(data._key) | |
| 222 | ||
| 223 | if (key.type === 'private') { | |
| 224 | const d = buffer.subarray(0, 32).toString('base64url') | |
| 225 | const x = buffer.subarray(32).toString('base64url') | |
| 226 | ||
| 227 | return { | |
| 228 | kty: 'OKP', | |
| 229 | alg: 'Ed25519', | |
| 230 | crv: 'Ed25519', | |
| 231 | x, | |
| 232 | d, | |
| 233 | key_ops: key.usages, | |
| 234 | ext: key.extractable | |
| 235 | } | |
| 236 | } | |
| 237 | ||
| 238 | return { | |
| 239 | kty: 'OKP', | |
| 240 | alg: 'Ed25519', | |
| 241 | crv: 'Ed25519', | |
| 242 | x: buffer.toString('base64url'), | |
| 243 | key_ops: key.usages, | |
| 244 | ext: key.extractable | |
| 245 | } | |
| 246 | } | |
| 247 | default: | |
| 248 | throw errors.NOT_SUPPORTED( | |
| 249 | `Format '${format}' cannot be used for the HMAC exportKey() operation` | |
| 250 | ) | |
| 251 | } | |
| 252 | } |
| … | … | @@ -0,0 +1,191 @@ |
| 1 | const crypto = require('../../..') | |
| 2 | const errors = require('../../errors') | |
| 3 | const CryptoKey = require('../crypto-key') | |
| 4 | ||
| 5 | // https://w3c.github.io/webcrypto/#hmac | |
| 6 | ||
| 7 | // https://w3c.github.io/webcrypto/#hmac-operations-sign | |
| 8 | exports.sign = function sign(algorithm, key, data) { | |
| 9 | const digest = crypto.createHmac(key.algorithm.hash.name, key._handle).update(data).digest() | |
| 10 | ||
| 11 | return digest.buffer.slice(0, digest.byteLength) | |
| 12 | } | |
| 13 | ||
| 14 | // https://w3c.github.io/webcrypto/#hmac-operations-verify | |
| 15 | exports.verify = function verify(algorithm, key, signature, data) { | |
| 16 | const digest = crypto.createHmac(key.algorithm.hash.name, key._handle).update(data).digest() | |
| 17 | ||
| 18 | if (ArrayBuffer.isView(signature)) { | |
| 19 | signature = Buffer.coerce(signature) | |
| 20 | } else { | |
| 21 | signature = Buffer.from(signature) | |
| 22 | } | |
| 23 | ||
| 24 | return signature.equals(digest) | |
| 25 | } | |
| 26 | ||
| 27 | // https://w3c.github.io/webcrypto/#hmac-operations-generate-key | |
| 28 | exports.generateKey = function generateKey(algorithm, extractable, usages) { | |
| 29 | for (const usage of usages) { | |
| 30 | if (usage !== 'sign' && usage !== 'verify') { | |
| 31 | throw new SyntaxError(`Usage '${usage}' cannot be used for the HMAC generateKey() operation`) | |
| 32 | } | |
| 33 | } | |
| 34 | ||
| 35 | const { length = exports.getKeyLength(algorithm) } = algorithm.length | |
| 36 | ||
| 37 | let hash = algorithm.hash | |
| 38 | ||
| 39 | if (typeof hash === 'string') hash = { name: hash } | |
| 40 | ||
| 41 | const key = crypto.createHmac(hash.name, crypto.randomBytes(length)).digest() | |
| 42 | ||
| 43 | return new CryptoKey( | |
| 44 | 'secret', | |
| 45 | extractable, | |
| 46 | { | |
| 47 | name: 'HMAC', | |
| 48 | length, | |
| 49 | hash: { | |
| 50 | name: hash.name.toUpperCase() | |
| 51 | } | |
| 52 | }, | |
| 53 | usages, | |
| 54 | key | |
| 55 | ) | |
| 56 | } | |
| 57 | ||
| 58 | // https://w3c.github.io/webcrypto/#hmac-operations-import-key | |
| 59 | exports.importKey = function importKey(format, keyData, algorithm, extractable, usages) { | |
| 60 | for (const usage of usages) { | |
| 61 | if (usage !== 'sign' && usage !== 'verify') { | |
| 62 | throw new SyntaxError(`Invalid usage ${usage}`) | |
| 63 | } | |
| 64 | } | |
| 65 | ||
| 66 | let hash = algorithm.hash | |
| 67 | ||
| 68 | if (typeof hash === 'string') hash = { name: hash } | |
| 69 | ||
| 70 | let data | |
| 71 | ||
| 72 | switch (format) { | |
| 73 | case 'raw': | |
| 74 | data = keyData | |
| 75 | break | |
| 76 | case 'jwk': | |
| 77 | const jwk = keyData | |
| 78 | ||
| 79 | if (jwk.kty !== 'oct') { | |
| 80 | throw errors.INVALID_DATA('JWK key must be an octet sequence') | |
| 81 | } | |
| 82 | ||
| 83 | data = Buffer.from(jwk.k, 'base64url') | |
| 84 | ||
| 85 | switch (hash.name.toLowerCase()) { | |
| 86 | case 'sha-1': | |
| 87 | if (jwk.alg === 'HS1') break | |
| 88 | else throw errors.INVALID_DATA('Invalid JWK key algorithm') | |
| 89 | case 'sha-256': | |
| 90 | if (jwk.alg === 'HS256') break | |
| 91 | else throw errors.INVALID_DATA('Invalid JWK key algorithm') | |
| 92 | case 'sha-384': | |
| 93 | if (jwk.alg === 'HS384') break | |
| 94 | else throw errors.INVALID_DATA('Invalid JWK key algorithm') | |
| 95 | case 'sha-512': | |
| 96 | if (jwk.alg === 'HS512') break | |
| 97 | else throw errors.INVALID_DATA('Invalid JWK key algorithm') | |
| 98 | } | |
| 99 | ||
| 100 | if (usages.length && 'use' in jwk && jwk.use !== 'sign') { | |
| 101 | throw errors.INVALID_DATA('JWK cannot be used for signing') | |
| 102 | } | |
| 103 | ||
| 104 | if ('ext' in jwk && jwk.ext !== extractable && extractable) { | |
| 105 | throw errors.INVALID_DATA('JWK is not extractable') | |
| 106 | } | |
| 107 | break | |
| 108 | default: | |
| 109 | throw errors.NOT_SUPPORTED( | |
| 110 | `Format '${format}' cannot be used for the HMAC importKey() operation` | |
| 111 | ) | |
| 112 | } | |
| 113 | ||
| 114 | const length = data.byteLength * 8 | |
| 115 | ||
| 116 | if (length === 0) { | |
| 117 | throw errors.INVALID_DATA('Key cannot be empty') | |
| 118 | } | |
| 119 | ||
| 120 | return new CryptoKey( | |
| 121 | 'secret', | |
| 122 | extractable, | |
| 123 | { | |
| 124 | name: 'HMAC', | |
| 125 | length, | |
| 126 | hash: { | |
| 127 | name: hash.name.toUpperCase() | |
| 128 | } | |
| 129 | }, | |
| 130 | usages, | |
| 131 | data | |
| 132 | ) | |
| 133 | } | |
| 134 | ||
| 135 | // https://w3c.github.io/webcrypto/#hmac-operations-export-key | |
| 136 | exports.exportKey = function exportKey(format, key) { | |
| 137 | const data = key._handle | |
| 138 | ||
| 139 | switch (format) { | |
| 140 | case 'raw': | |
| 141 | return data.buffer.slice(0, data.byteLength) | |
| 142 | case 'jwk': { | |
| 143 | const jwk = { | |
| 144 | kty: 'oct', | |
| 145 | k: data.toString('base64url'), | |
| 146 | alg: null, | |
| 147 | key_ops: key.usages, | |
| 148 | ext: key.extractable | |
| 149 | } | |
| 150 | ||
| 151 | switch (key.algorithm.hash.name) { | |
| 152 | case 'SHA-1': | |
| 153 | jwk.alg = 'HS1' | |
| 154 | break | |
| 155 | case 'SHA-256': | |
| 156 | jwk.alg = 'HS256' | |
| 157 | break | |
| 158 | case 'SHA-384': | |
| 159 | jwk.alg = 'HS384' | |
| 160 | break | |
| 161 | case 'SHA-512': | |
| 162 | jwk.alg = 'HS512' | |
| 163 | break | |
| 164 | } | |
| 165 | ||
| 166 | return jwk | |
| 167 | } | |
| 168 | default: | |
| 169 | throw errors.NOT_SUPPORTED( | |
| 170 | `Format '${format}' cannot be used for the HMAC exportKey() operation` | |
| 171 | ) | |
| 172 | } | |
| 173 | } | |
| 174 | ||
| 175 | // https://w3c.github.io/webcrypto/#hmac-operations-get-key-length | |
| 176 | exports.getKeyLength = function getKeyLength(algorithm) { | |
| 177 | const { length, hash } = algorithm | |
| 178 | ||
| 179 | if (length === undefined) { | |
| 180 | if (hash === 'SHA-1' || hash === 'SHA-256') return 512 | |
| 181 | if (hash === 'SHA-512') return 1024 | |
| 182 | ||
| 183 | throw errors.OPERATION_ERROR(`Invalid hash '${hash}'`) | |
| 184 | } | |
| 185 | ||
| 186 | if (length === 0) { | |
| 187 | throw errors.OPERATION_ERROR(`Invalid length ${length}`) | |
| 188 | } | |
| 189 | ||
| 190 | return length | |
| 191 | } |
| … | … | @@ -0,0 +1,63 @@ |
| 1 | const crypto = require('../../..') | |
| 2 | const errors = require('../../errors') | |
| 3 | const CryptoKey = require('../crypto-key') | |
| 4 | ||
| 5 | // https://w3c.github.io/webcrypto/#pbkdf2 | |
| 6 | ||
| 7 | // https://w3c.github.io/webcrypto/#pbkdf2-operations-derive-bits | |
| 8 | exports.deriveBits = function deriveBits(algorithm, key, length) { | |
| 9 | if (length === undefined || length % 8) { | |
| 10 | throw errors.OPERATION_ERROR('Length must be multiple of 8') | |
| 11 | } | |
| 12 | ||
| 13 | if (algorithm.iterations === 0) { | |
| 14 | throw errors.OPERATION_ERROR('Iterations must be non-0') | |
| 15 | } | |
| 16 | ||
| 17 | if (length === 0) { | |
| 18 | return new ArrayBuffer(0) | |
| 19 | } | |
| 20 | ||
| 21 | let hash = algorithm.hash | |
| 22 | ||
| 23 | if (typeof hash === 'string') hash = { name: hash } | |
| 24 | ||
| 25 | const result = crypto.pbkdf2( | |
| 26 | key._handle, | |
| 27 | algorithm.salt, | |
| 28 | algorithm.iterations, | |
| 29 | length / 8, | |
| 30 | hash.name | |
| 31 | ) | |
| 32 | ||
| 33 | return result.buffer | |
| 34 | } | |
| 35 | ||
| 36 | // https://w3c.github.io/webcrypto/#pbkdf2-operations-import-key | |
| 37 | exports.importKey = function importKey(format, keyData, algorithm, extractable, usages) { | |
| 38 | if (format !== 'raw') { | |
| 39 | throw errors.NOT_SUPPORTED( | |
| 40 | `Format '${format}' cannot be used for the PBKDF2 importKey() operation` | |
| 41 | ) | |
| 42 | } | |
| 43 | ||
| 44 | for (const usage of usages) { | |
| 45 | if (usage !== 'deriveKey' && usage !== 'deriveBits') { | |
| 46 | throw new SyntaxError(`Invalid usage ${usage}`) | |
| 47 | } | |
| 48 | } | |
| 49 | ||
| 50 | if (extractable) { | |
| 51 | throw new SyntaxError('Extractable must be false') | |
| 52 | } | |
| 53 | ||
| 54 | return new CryptoKey( | |
| 55 | 'secret', | |
| 56 | extractable, | |
| 57 | { | |
| 58 | name: 'PBKDF2' | |
| 59 | }, | |
| 60 | usages, | |
| 61 | keyData | |
| 62 | ) | |
| 63 | } |
| … | … | @@ -0,0 +1,10 @@ |
| 1 | const crypto = require('../../..') | |
| 2 | ||
| 3 | // https://w3c.github.io/webcrypto/#sha | |
| 4 | ||
| 5 | // https://w3c.github.io/webcrypto/#sha-operations-digest | |
| 6 | exports.digest = function digest(name, data) { | |
| 7 | const digest = crypto.createHash(name).update(data).digest() | |
| 8 | ||
| 9 | return digest.buffer.slice(0, digest.byteLength) | |
| 10 | } |
| … | … | @@ -0,0 +1,41 @@ |
| 1 | // https://w3c.github.io/webcrypto/#cryptokey-interface | |
| 2 | module.exports = class CryptoKey { | |
| 3 | constructor(type, extractable, algorithm, usages, handle = null) { | |
| 4 | this._type = type | |
| 5 | this._extractable = extractable | |
| 6 | this._algorithm = algorithm | |
| 7 | this._usages = usages | |
| 8 | this._handle = handle | |
| 9 | } | |
| 10 | ||
| 11 | // https://w3c.github.io/webcrypto/#dom-cryptokey-type | |
| 12 | get type() { | |
| 13 | return this._type | |
| 14 | } | |
| 15 | ||
| 16 | // https://w3c.github.io/webcrypto/#dom-cryptokey-extractable | |
| 17 | get extractable() { | |
| 18 | return this._extractable | |
| 19 | } | |
| 20 | ||
| 21 | // https://w3c.github.io/webcrypto/#dom-cryptokey-algorithm | |
| 22 | get algorithm() { | |
| 23 | return this._algorithm | |
| 24 | } | |
| 25 | ||
| 26 | // https://w3c.github.io/webcrypto/#dom-cryptokey-usages | |
| 27 | get usages() { | |
| 28 | return this._usages | |
| 29 | } | |
| 30 | ||
| 31 | [Symbol.for('bare.inspect')]() { | |
| 32 | return { | |
| 33 | __proto__: { constructor: CryptoKey }, | |
| 34 | ||
| 35 | type: this.type, | |
| 36 | extractable: this.extractable, | |
| 37 | algorithm: this.algorithm, | |
| 38 | usages: this.usages | |
| 39 | } | |
| 40 | } | |
| 41 | } |
| … | … | @@ -0,0 +1,69 @@ |
| 1 | { | |
| 2 | "name": "bare-crypto", | |
| 3 | "version": "1.13.4", | |
| 4 | "description": "Cryptographic primitives for JavaScript", | |
| 5 | "exports": { | |
| 6 | "./package": "./package.json", | |
| 7 | ".": { | |
| 8 | "types": "./index.d.ts", | |
| 9 | "default": "./index.js" | |
| 10 | }, | |
| 11 | "./web": { | |
| 12 | "types": "./web.d.ts", | |
| 13 | "default": "./web.js" | |
| 14 | }, | |
| 15 | "./global": { | |
| 16 | "types": "./global.d.ts", | |
| 17 | "default": "./global.js" | |
| 18 | }, | |
| 19 | "./constants": "./lib/constants.js", | |
| 20 | "./errors": "./lib/errors.js" | |
| 21 | }, | |
| 22 | "files": [ | |
| 23 | "index.js", | |
| 24 | "index.d.ts", | |
| 25 | "web.js", | |
| 26 | "web.d.ts", | |
| 27 | "global.js", | |
| 28 | "global.d.ts", | |
| 29 | "binding.c", | |
| 30 | "binding.js", | |
| 31 | "CMakeLists.txt", | |
| 32 | "lib", | |
| 33 | "prebuilds" | |
| 34 | ], | |
| 35 | "addon": true, | |
| 36 | "scripts": { | |
| 37 | "test": "prettier . --check && bare test.js" | |
| 38 | }, | |
| 39 | "repository": { | |
| 40 | "type": "git", | |
| 41 | "url": "git+https://github.com/holepunchto/bare-crypto.git" | |
| 42 | }, | |
| 43 | "author": "Holepunch", | |
| 44 | "license": "Apache-2.0", | |
| 45 | "bugs": { | |
| 46 | "url": "https://github.com/holepunchto/bare-crypto/issues" | |
| 47 | }, | |
| 48 | "homepage": "https://github.com/holepunchto/bare-crypto#readme", | |
| 49 | "dependencies": { | |
| 50 | "bare-assert": "^1.2.0", | |
| 51 | "bare-stream": "^2.6.3" | |
| 52 | }, | |
| 53 | "devDependencies": { | |
| 54 | "bare-buffer": "^3.0.1", | |
| 55 | "brittle": "^3.5.0", | |
| 56 | "cmake-bare": "^1.1.6", | |
| 57 | "cmake-fetch": "^1.0.0", | |
| 58 | "prettier": "^3.4.2", | |
| 59 | "prettier-config-holepunch": "^2.0.0" | |
| 60 | }, | |
| 61 | "peerDependencies": { | |
| 62 | "bare-buffer": "*" | |
| 63 | }, | |
| 64 | "peerDependenciesMeta": { | |
| 65 | "bare-buffer": { | |
| 66 | "optional": true | |
| 67 | } | |
| 68 | } | |
| 69 | } |
| … | … | @@ -0,0 +1 @@ |
| 1 | export function getRandomValues<B extends ArrayBuffer | ArrayBufferView>(array: B): B |
| … | … | @@ -0,0 +1,234 @@ |
| 1 | const crypto = require('.') | |
| 2 | const errors = require('./lib/errors') | |
| 3 | const CryptoKey = require('./lib/web/crypto-key') | |
| 4 | const hmac = require('./lib/web/algorithm/hmac') | |
| 5 | const pbkdf2 = require('./lib/web/algorithm/pbkdf2') | |
| 6 | const ed25519 = require('./lib/web/algorithm/ed25519') | |
| 7 | const sha = require('./lib/web/algorithm/sha') | |
| 8 | ||
| 9 | exports.CryptoKey = CryptoKey | |
| 10 | ||
| 11 | // https://w3c.github.io/webcrypto/#Crypto-method-getRandomValues | |
| 12 | exports.getRandomValues = function getRandomValues(array) { | |
| 13 | return crypto.randomFillSync(array) | |
| 14 | } | |
| 15 | ||
| 16 | // https://w3c.github.io/webcrypto/#dfn-Crypto-method-randomUUID | |
| 17 | exports.randomUUID = crypto.randomUUID | |
| 18 | ||
| 19 | // https://w3c.github.io/webcrypto/#subtlecrypto-interface | |
| 20 | exports.SubtleCrypto = class SubtleCrypto { | |
| 21 | // https://w3c.github.io/webcrypto/#SubtleCrypto-method-generateKey | |
| 22 | async generateKey(algorithm, extractable, usages) { | |
| 23 | if (typeof algorithm === 'string') algorithm = { name: algorithm } | |
| 24 | ||
| 25 | switch (algorithm.name.toLowerCase()) { | |
| 26 | case 'hmac': | |
| 27 | return hmac.generateKey(algorithm, extractable, usages) | |
| 28 | case 'ed25519': | |
| 29 | return ed25519.generateKey(algorithm, extractable, usages) | |
| 30 | default: | |
| 31 | throw errors.NOT_SUPPORTED( | |
| 32 | `Algorithm '${algorithm.name}' does not support the generateKey() operation` | |
| 33 | ) | |
| 34 | } | |
| 35 | } | |
| 36 | ||
| 37 | // https://w3c.github.io/webcrypto/#SubtleCrypto-method-importKey | |
| 38 | async importKey(format, keyData, algorithm, extractable, usages) { | |
| 39 | if (typeof algorithm === 'string') algorithm = { name: algorithm } | |
| 40 | ||
| 41 | switch (format) { | |
| 42 | case 'raw': | |
| 43 | case 'pkcs8': | |
| 44 | case 'spki': | |
| 45 | if (ArrayBuffer.isView(keyData)) { | |
| 46 | keyData = Buffer.from(keyData) | |
| 47 | } else { | |
| 48 | keyData = Buffer.from(keyData.slice()) | |
| 49 | } | |
| 50 | break | |
| 51 | } | |
| 52 | ||
| 53 | switch (algorithm.name.toLowerCase()) { | |
| 54 | case 'hmac': | |
| 55 | return hmac.importKey(format, keyData, algorithm, extractable, usages) | |
| 56 | case 'ed25519': | |
| 57 | return ed25519.importKey(format, keyData, algorithm, extractable, usages) | |
| 58 | case 'pbkdf2': | |
| 59 | return pbkdf2.importKey(format, keyData, algorithm, extractable, usages) | |
| 60 | default: | |
| 61 | throw errors.NOT_SUPPORTED( | |
| 62 | `Algorithm '${algorithm.name}' does not support the importKey() operation` | |
| 63 | ) | |
| 64 | } | |
| 65 | } | |
| 66 | ||
| 67 | // https://w3c.github.io/webcrypto/#SubtleCrypto-method-exportKey | |
| 68 | async exportKey(format, key) { | |
| 69 | if (!key.extractable) { | |
| 70 | throw errors.INVALID_ACCESS('Key is not extractable') | |
| 71 | } | |
| 72 | ||
| 73 | switch (key.algorithm.name.toLowerCase()) { | |
| 74 | case 'hmac': | |
| 75 | return hmac.exportKey(format, key) | |
| 76 | case 'ed25519': | |
| 77 | return ed25519.exportKey(format, key) | |
| 78 | default: | |
| 79 | throw errors.NOT_SUPPORTED( | |
| 80 | `Algorithm '${key.algorithm.name}' does not support the exportKey() operation` | |
| 81 | ) | |
| 82 | } | |
| 83 | } | |
| 84 | ||
| 85 | // https://w3c.github.io/webcrypto/#SubtleCrypto-method-sign | |
| 86 | async sign(algorithm, key, data) { | |
| 87 | if (typeof algorithm === 'string') algorithm = { name: algorithm } | |
| 88 | ||
| 89 | if (algorithm.name.toLowerCase() !== key.algorithm.name.toLowerCase()) { | |
| 90 | throw errors.INVALID_ACCESS(`Algorithm '${algorithm.name}' does not match key'`) | |
| 91 | } | |
| 92 | ||
| 93 | if (!key.usages.includes('sign')) { | |
| 94 | throw errors.INVALID_ACCESS('Key cannot be used for signing') | |
| 95 | } | |
| 96 | ||
| 97 | switch (algorithm.name.toLowerCase()) { | |
| 98 | case 'hmac': | |
| 99 | return hmac.sign(algorithm, key, data) | |
| 100 | case 'ed25519': | |
| 101 | return ed25519.sign(algorithm, key, data) | |
| 102 | default: | |
| 103 | throw errors.NOT_SUPPORTED( | |
| 104 | `Algorithm '${algorithm.name}' does not support the sign() operation` | |
| 105 | ) | |
| 106 | } | |
| 107 | } | |
| 108 | ||
| 109 | // https://w3c.github.io/webcrypto/#SubtleCrypto-method-verify | |
| 110 | async verify(algorithm, key, signature, data) { | |
| 111 | if (typeof algorithm === 'string') algorithm = { name: algorithm } | |
| 112 | ||
| 113 | if (algorithm.name.toLowerCase() !== key.algorithm.name.toLowerCase()) { | |
| 114 | throw errors.INVALID_ACCESS(`Algorithm '${algorithm.name}' does not match key'`) | |
| 115 | } | |
| 116 | ||
| 117 | if (!key.usages.includes('verify')) { | |
| 118 | throw errors.INVALID_ACCESS('Key cannot be used for verification') | |
| 119 | } | |
| 120 | ||
| 121 | switch (algorithm.name.toLowerCase()) { | |
| 122 | case 'hmac': | |
| 123 | return hmac.verify(algorithm, key, signature, data) | |
| 124 | case 'ed25519': | |
| 125 | return ed25519.verify(algorithm, key, signature, data) | |
| 126 | default: | |
| 127 | throw errors.NOT_SUPPORTED( | |
| 128 | `Algorithm '${algorithm.name}' does not support the verify() operation` | |
| 129 | ) | |
| 130 | } | |
| 131 | } | |
| 132 | ||
| 133 | // https://w3c.github.io/webcrypto/#SubtleCrypto-method-deriveBits | |
| 134 | async deriveBits(algorithm, key, length) { | |
| 135 | if (typeof algorithm === 'string') algorithm = { name: algorithm } | |
| 136 | ||
| 137 | if (algorithm.name.toLowerCase() !== key.algorithm.name.toLowerCase()) { | |
| 138 | throw errors.INVALID_ACCESS(`Algorithm '${algorithm.name}' does not match key'`) | |
| 139 | } | |
| 140 | ||
| 141 | if (!key.usages.includes('deriveBits')) { | |
| 142 | throw errors.INVALID_ACCESS('Key cannot be used to derive bits') | |
| 143 | } | |
| 144 | ||
| 145 | switch (algorithm.name.toLowerCase()) { | |
| 146 | case 'pbkdf2': | |
| 147 | return pbkdf2.deriveBits(algorithm, key, length) | |
| 148 | default: | |
| 149 | throw errors.NOT_SUPPORTED( | |
| 150 | `Algorithm '${algorithm.name}' does not support the deriveBits() operation` | |
| 151 | ) | |
| 152 | } | |
| 153 | } | |
| 154 | ||
| 155 | // https://w3c.github.io/webcrypto/#SubtleCrypto-method-deriveKey | |
| 156 | async deriveKey(algorithm, baseKey, derivedKeyType, extractable, usages) { | |
| 157 | if (typeof algorithm === 'string') algorithm = { name: algorithm } | |
| 158 | ||
| 159 | if (typeof derivedKeyType === 'string') { | |
| 160 | derivedKeyType = { name: derivedKeyType } | |
| 161 | } | |
| 162 | ||
| 163 | if (algorithm.name.toLowerCase() !== baseKey.algorithm.name.toLowerCase()) { | |
| 164 | throw errors.INVALID_ACCESS(`Algorithm '${algorithm.name}' does not match key'`) | |
| 165 | } | |
| 166 | ||
| 167 | if (!baseKey.usages.includes('deriveKey')) { | |
| 168 | throw errors.INVALID_ACCESS('Key cannot be used to derive key') | |
| 169 | } | |
| 170 | ||
| 171 | let length | |
| 172 | ||
| 173 | switch (derivedKeyType.name.toLowerCase()) { | |
| 174 | case 'hmac': | |
| 175 | length = hmac.getKeyLength(derivedKeyType) | |
| 176 | break | |
| 177 | default: | |
| 178 | throw errors.NOT_SUPPORTED( | |
| 179 | `Algorithm '${derivedKeyType.name}' does not support the getKeyLength() operation` | |
| 180 | ) | |
| 181 | } | |
| 182 | ||
| 183 | let secret | |
| 184 | ||
| 185 | switch (algorithm.name.toLowerCase()) { | |
| 186 | case 'pbkdf2': | |
| 187 | secret = pbkdf2.deriveBits(algorithm, baseKey, length) | |
| 188 | break | |
| 189 | default: | |
| 190 | throw errors.NOT_SUPPORTED( | |
| 191 | `Algorithm '${algorithm.name}' does not support the deriveBits() operation` | |
| 192 | ) | |
| 193 | } | |
| 194 | ||
| 195 | return this.importKey('raw', secret, derivedKeyType, extractable, usages) | |
| 196 | } | |
| 197 | ||
| 198 | // https://w3c.github.io/webcrypto/#SubtleCrypto-method-digest | |
| 199 | async digest(algorithm, data) { | |
| 200 | if (typeof algorithm === 'string') algorithm = { name: algorithm } | |
| 201 | ||
| 202 | switch (algorithm.name.toLowerCase()) { | |
| 203 | case 'sha-1': | |
| 204 | return sha.digest(crypto.constants.hash.SHA1, data) | |
| 205 | case 'sha-256': | |
| 206 | return sha.digest(crypto.constants.hash.SHA256, data) | |
| 207 | case 'sha-384': | |
| 208 | return sha.digest(crypto.constants.hash.SHA384, data) | |
| 209 | case 'sha-512': | |
| 210 | return sha.digest(crypto.constants.hash.SHA512, data) | |
| 211 | default: | |
| 212 | throw errors.NOT_SUPPORTED( | |
| 213 | `Algorithm '${algorithm.name}' does not support the digest() operation` | |
| 214 | ) | |
| 215 | } | |
| 216 | } | |
| 217 | } | |
| 218 | ||
| 219 | exports.subtle = new exports.SubtleCrypto() | |
| 220 | ||
| 221 | // https://w3c.github.io/webcrypto/#crypto-interface | |
| 222 | exports.Crypto = class Crypto { | |
| 223 | get subtle() { | |
| 224 | return exports.subtle | |
| 225 | } | |
| 226 | ||
| 227 | getRandomValues(array) { | |
| 228 | return exports.getRandomValues(array) | |
| 229 | } | |
| 230 | ||
| 231 | randomUUID() { | |
| 232 | return exports.randomUUID() | |
| 233 | } | |
| 234 | } |
| … | … | @@ -0,0 +1,201 @@ |
| 1 | Apache License | |
| 2 | Version 2.0, January 2004 | |
| 3 | http://www.apache.org/licenses/ | |
| 4 | ||
| 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | |
| 6 | ||
| 7 | 1. Definitions. | |
| 8 | ||
| 9 | "License" shall mean the terms and conditions for use, reproduction, | |
| 10 | and distribution as defined by Sections 1 through 9 of this document. | |
| 11 | ||
| 12 | "Licensor" shall mean the copyright owner or entity authorized by | |
| 13 | the copyright owner that is granting the License. | |
| 14 | ||
| 15 | "Legal Entity" shall mean the union of the acting entity and all | |
| 16 | other entities that control, are controlled by, or are under common | |
| 17 | control with that entity. For the purposes of this definition, | |
| 18 | "control" means (i) the power, direct or indirect, to cause the | |
| 19 | direction or management of such entity, whether by contract or | |
| 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the | |
| 21 | outstanding shares, or (iii) beneficial ownership of such entity. | |
| 22 | ||
| 23 | "You" (or "Your") shall mean an individual or Legal Entity | |
| 24 | exercising permissions granted by this License. | |
| 25 | ||
| 26 | "Source" form shall mean the preferred form for making modifications, | |
| 27 | including but not limited to software source code, documentation | |
| 28 | source, and configuration files. | |
| 29 | ||
| 30 | "Object" form shall mean any form resulting from mechanical | |
| 31 | transformation or translation of a Source form, including but | |
| 32 | not limited to compiled object code, generated documentation, | |
| 33 | and conversions to other media types. | |
| 34 | ||
| 35 | "Work" shall mean the work of authorship, whether in Source or | |
| 36 | Object form, made available under the License, as indicated by a | |
| 37 | copyright notice that is included in or attached to the work | |
| 38 | (an example is provided in the Appendix below). | |
| 39 | ||
| 40 | "Derivative Works" shall mean any work, whether in Source or Object | |
| 41 | form, that is based on (or derived from) the Work and for which the | |
| 42 | editorial revisions, annotations, elaborations, or other modifications | |
| 43 | represent, as a whole, an original work of authorship. For the purposes | |
| 44 | of this License, Derivative Works shall not include works that remain | |
| 45 | separable from, or merely link (or bind by name) to the interfaces of, | |
| 46 | the Work and Derivative Works thereof. | |
| 47 | ||
| 48 | "Contribution" shall mean any work of authorship, including | |
| 49 | the original version of the Work and any modifications or additions | |
| 50 | to that Work or Derivative Works thereof, that is intentionally | |
| 51 | submitted to Licensor for inclusion in the Work by the copyright owner | |
| 52 | or by an individual or Legal Entity authorized to submit on behalf of | |
| 53 | the copyright owner. For the purposes of this definition, "submitted" | |
| 54 | means any form of electronic, verbal, or written communication sent | |
| 55 | to the Licensor or its representatives, including but not limited to | |
| 56 | communication on electronic mailing lists, source code control systems, | |
| 57 | and issue tracking systems that are managed by, or on behalf of, the | |
| 58 | Licensor for the purpose of discussing and improving the Work, but | |
| 59 | excluding communication that is conspicuously marked or otherwise | |
| 60 | designated in writing by the copyright owner as "Not a Contribution." | |
| 61 | ||
| 62 | "Contributor" shall mean Licensor and any individual or Legal Entity | |
| 63 | on behalf of whom a Contribution has been received by Licensor and | |
| 64 | subsequently incorporated within the Work. | |
| 65 | ||
| 66 | 2. Grant of Copyright License. Subject to the terms and conditions of | |
| 67 | this License, each Contributor hereby grants to You a perpetual, | |
| 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 69 | copyright license to reproduce, prepare Derivative Works of, | |
| 70 | publicly display, publicly perform, sublicense, and distribute the | |
| 71 | Work and such Derivative Works in Source or Object form. | |
| 72 | ||
| 73 | 3. Grant of Patent License. Subject to the terms and conditions of | |
| 74 | this License, each Contributor hereby grants to You a perpetual, | |
| 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 76 | (except as stated in this section) patent license to make, have made, | |
| 77 | use, offer to sell, sell, import, and otherwise transfer the Work, | |
| 78 | where such license applies only to those patent claims licensable | |
| 79 | by such Contributor that are necessarily infringed by their | |
| 80 | Contribution(s) alone or by combination of their Contribution(s) | |
| 81 | with the Work to which such Contribution(s) was submitted. If You | |
| 82 | institute patent litigation against any entity (including a | |
| 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work | |
| 84 | or a Contribution incorporated within the Work constitutes direct | |
| 85 | or contributory patent infringement, then any patent licenses | |
| 86 | granted to You under this License for that Work shall terminate | |
| 87 | as of the date such litigation is filed. | |
| 88 | ||
| 89 | 4. Redistribution. You may reproduce and distribute copies of the | |
| 90 | Work or Derivative Works thereof in any medium, with or without | |
| 91 | modifications, and in Source or Object form, provided that You | |
| 92 | meet the following conditions: | |
| 93 | ||
| 94 | (a) You must give any other recipients of the Work or | |
| 95 | Derivative Works a copy of this License; and | |
| 96 | ||
| 97 | (b) You must cause any modified files to carry prominent notices | |
| 98 | stating that You changed the files; and | |
| 99 | ||
| 100 | (c) You must retain, in the Source form of any Derivative Works | |
| 101 | that You distribute, all copyright, patent, trademark, and | |
| 102 | attribution notices from the Source form of the Work, | |
| 103 | excluding those notices that do not pertain to any part of | |
| 104 | the Derivative Works; and | |
| 105 | ||
| 106 | (d) If the Work includes a "NOTICE" text file as part of its | |
| 107 | distribution, then any Derivative Works that You distribute must | |
| 108 | include a readable copy of the attribution notices contained | |
| 109 | within such NOTICE file, excluding those notices that do not | |
| 110 | pertain to any part of the Derivative Works, in at least one | |
| 111 | of the following places: within a NOTICE text file distributed | |
| 112 | as part of the Derivative Works; within the Source form or | |
| 113 | documentation, if provided along with the Derivative Works; or, | |
| 114 | within a display generated by the Derivative Works, if and | |
| 115 | wherever such third-party notices normally appear. The contents | |
| 116 | of the NOTICE file are for informational purposes only and | |
| 117 | do not modify the License. You may add Your own attribution | |
| 118 | notices within Derivative Works that You distribute, alongside | |
| 119 | or as an addendum to the NOTICE text from the Work, provided | |
| 120 | that such additional attribution notices cannot be construed | |
| 121 | as modifying the License. | |
| 122 | ||
| 123 | You may add Your own copyright statement to Your modifications and | |
| 124 | may provide additional or different license terms and conditions | |
| 125 | for use, reproduction, or distribution of Your modifications, or | |
| 126 | for any such Derivative Works as a whole, provided Your use, | |
| 127 | reproduction, and distribution of the Work otherwise complies with | |
| 128 | the conditions stated in this License. | |
| 129 | ||
| 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, | |
| 131 | any Contribution intentionally submitted for inclusion in the Work | |
| 132 | by You to the Licensor shall be under the terms and conditions of | |
| 133 | this License, without any additional terms or conditions. | |
| 134 | Notwithstanding the above, nothing herein shall supersede or modify | |
| 135 | the terms of any separate license agreement you may have executed | |
| 136 | with Licensor regarding such Contributions. | |
| 137 | ||
| 138 | 6. Trademarks. This License does not grant permission to use the trade | |
| 139 | names, trademarks, service marks, or product names of the Licensor, | |
| 140 | except as required for reasonable and customary use in describing the | |
| 141 | origin of the Work and reproducing the content of the NOTICE file. | |
| 142 | ||
| 143 | 7. Disclaimer of Warranty. Unless required by applicable law or | |
| 144 | agreed to in writing, Licensor provides the Work (and each | |
| 145 | Contributor provides its Contributions) on an "AS IS" BASIS, | |
| 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |
| 147 | implied, including, without limitation, any warranties or conditions | |
| 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | |
| 149 | PARTICULAR PURPOSE. You are solely responsible for determining the | |
| 150 | appropriateness of using or redistributing the Work and assume any | |
| 151 | risks associated with Your exercise of permissions under this License. | |
| 152 | ||
| 153 | 8. Limitation of Liability. In no event and under no legal theory, | |
| 154 | whether in tort (including negligence), contract, or otherwise, | |
| 155 | unless required by applicable law (such as deliberate and grossly | |
| 156 | negligent acts) or agreed to in writing, shall any Contributor be | |
| 157 | liable to You for damages, including any direct, indirect, special, | |
| 158 | incidental, or consequential damages of any character arising as a | |
| 159 | result of this License or out of the use or inability to use the | |
| 160 | Work (including but not limited to damages for loss of goodwill, | |
| 161 | work stoppage, computer failure or malfunction, or any and all | |
| 162 | other commercial damages or losses), even if such Contributor | |
| 163 | has been advised of the possibility of such damages. | |
| 164 | ||
| 165 | 9. Accepting Warranty or Additional Liability. While redistributing | |
| 166 | the Work or Derivative Works thereof, You may choose to offer, | |
| 167 | and charge a fee for, acceptance of support, warranty, indemnity, | |
| 168 | or other liability obligations and/or rights consistent with this | |
| 169 | License. However, in accepting such obligations, You may act only | |
| 170 | on Your own behalf and on Your sole responsibility, not on behalf | |
| 171 | of any other Contributor, and only if You agree to indemnify, | |
| 172 | defend, and hold each Contributor harmless for any liability | |
| 173 | incurred by, or claims asserted against, such Contributor by reason | |
| 174 | of your accepting any such warranty or additional liability. | |
| 175 | ||
| 176 | END OF TERMS AND CONDITIONS | |
| 177 | ||
| 178 | APPENDIX: How to apply the Apache License to your work. | |
| 179 | ||
| 180 | To apply the Apache License to your work, attach the following | |
| 181 | boilerplate notice, with the fields enclosed by brackets "[]" | |
| 182 | replaced with your own identifying information. (Don't include | |
| 183 | the brackets!) The text should be enclosed in the appropriate | |
| 184 | comment syntax for the file format. We also recommend that a | |
| 185 | file or class name and description of purpose be included on the | |
| 186 | same "printed page" as the copyright notice for easier | |
| 187 | identification within third-party archives. | |
| 188 | ||
| 189 | Copyright [yyyy] [name of copyright owner] | |
| 190 | ||
| 191 | Licensed under the Apache License, Version 2.0 (the "License"); | |
| 192 | you may not use this file except in compliance with the License. | |
| 193 | You may obtain a copy of the License at | |
| 194 | ||
| 195 | http://www.apache.org/licenses/LICENSE-2.0 | |
| 196 | ||
| 197 | Unless required by applicable law or agreed to in writing, software | |
| 198 | distributed under the License is distributed on an "AS IS" BASIS, | |
| 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 200 | See the License for the specific language governing permissions and | |
| 201 | limitations under the License. |
| … | … | @@ -0,0 +1,24 @@ |
| 1 | # bare-debug-log | |
| 2 | ||
| 3 | Simple debug log for JavaScript. | |
| 4 | ||
| 5 | ``` | |
| 6 | npm i bare-debug-log | |
| 7 | ``` | |
| 8 | ||
| 9 | ## Usage | |
| 10 | ||
| 11 | ```js | |
| 12 | const log = require('bare-debug-log')('foo') | |
| 13 | ||
| 14 | log('hello from foo [%d]', 123) | |
| 15 | ``` | |
| 16 | ||
| 17 | ```sh | |
| 18 | $ BARE_DEBUG=foo bare index.js | |
| 19 | FOO 3245: hello from foo [123] | |
| 20 | ``` | |
| 21 | ||
| 22 | ## License | |
| 23 | ||
| 24 | Apache-2.0 |
| … | … | @@ -0,0 +1,23 @@ |
| 1 | const os = require('bare-os') | |
| 2 | ||
| 3 | const pid = os.pid() | |
| 4 | const env = os.getEnv('BARE_DEBUG') || os.getEnv('NODE_DEBUG') || '' | |
| 5 | ||
| 6 | const test = new RegExp( | |
| 7 | `^${env.replace(/[|\\{}()[\]^$+?.]/g, '\\$&').replace(/\*/g, '.*').replace(/,/g, '$|^')}$`, | |
| 8 | 'i' | |
| 9 | ) | |
| 10 | ||
| 11 | module.exports = function debug (section) { | |
| 12 | section = section.toUpperCase() | |
| 13 | ||
| 14 | const enabled = test.test(section) | |
| 15 | ||
| 16 | function debug (...args) { | |
| 17 | if (enabled) console.error('%s %s:', section, pid, ...args) | |
| 18 | } | |
| 19 | ||
| 20 | debug.enabled = enabled | |
| 21 | ||
| 22 | return debug | |
| 23 | } |
| … | … | @@ -0,0 +1,31 @@ |
| 1 | { | |
| 2 | "name": "bare-debug-log", | |
| 3 | "version": "2.0.0", | |
| 4 | "description": "Simple debug log for JavaScript", | |
| 5 | "exports": { | |
| 6 | ".": "./index.js", | |
| 7 | "./package": "./package.json" | |
| 8 | }, | |
| 9 | "files": [ | |
| 10 | "index.js" | |
| 11 | ], | |
| 12 | "scripts": { | |
| 13 | "test": "standard" | |
| 14 | }, | |
| 15 | "repository": { | |
| 16 | "type": "git", | |
| 17 | "url": "git+https://github.com/holepunchto/bare-debug-log.git" | |
| 18 | }, | |
| 19 | "author": "Holepunch", | |
| 20 | "license": "Apache-2.0", | |
| 21 | "bugs": { | |
| 22 | "url": "https://github.com/holepunchto/bare-debug-log/issues" | |
| 23 | }, | |
| 24 | "homepage": "https://github.com/holepunchto/bare-debug-log#readme", | |
| 25 | "dependencies": { | |
| 26 | "bare-os": "^3.0.1" | |
| 27 | }, | |
| 28 | "devDependencies": { | |
| 29 | "standard": "^17.0.0" | |
| 30 | } | |
| 31 | } |
| … | … | @@ -0,0 +1,31 @@ |
| 1 | cmake_minimum_required(VERSION 3.31) | |
| 2 | ||
| 3 | find_package(cmake-bare REQUIRED PATHS node_modules/cmake-bare) | |
| 4 | find_package(cmake-fetch REQUIRED PATHS node_modules/cmake-fetch) | |
| 5 | ||
| 6 | project(bare_dns C) | |
| 7 | ||
| 8 | set(CARES_STATIC ON CACHE BOOL "Build the static c-ares library" FORCE) | |
| 9 | set(CARES_STATIC_PIC ON CACHE BOOL "Enable PIC for the static c-ares library" FORCE) | |
| 10 | set(CARES_SHARED OFF CACHE BOOL "Build the shared c-ares library" FORCE) | |
| 11 | set(CARES_INSTALL OFF CACHE BOOL "Enable c-ares installation scripts" FORCE) | |
| 12 | set(CARES_BUILD_TOOLS OFF CACHE BOOL "Build the c-ares utility tools" FORCE) | |
| 13 | ||
| 14 | fetch_package("github:c-ares/c-ares@1.34.4") | |
| 15 | fetch_package("github:holepunchto/libintrusive#3903632") | |
| 16 | ||
| 17 | add_bare_module(bare_dns) | |
| 18 | ||
| 19 | target_sources( | |
| 20 | ${bare_dns} | |
| 21 | PRIVATE | |
| 22 | binding.c | |
| 23 | ) | |
| 24 | ||
| 25 | target_link_libraries( | |
| 26 | ${bare_dns} | |
| 27 | PUBLIC | |
| 28 | intrusive | |
| 29 | PRIVATE | |
| 30 | c-ares::cares | |
| 31 | ) |
| … | … | @@ -0,0 +1,201 @@ |
| 1 | Apache License | |
| 2 | Version 2.0, January 2004 | |
| 3 | http://www.apache.org/licenses/ | |
| 4 | ||
| 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | |
| 6 | ||
| 7 | 1. Definitions. | |
| 8 | ||
| 9 | "License" shall mean the terms and conditions for use, reproduction, | |
| 10 | and distribution as defined by Sections 1 through 9 of this document. | |
| 11 | ||
| 12 | "Licensor" shall mean the copyright owner or entity authorized by | |
| 13 | the copyright owner that is granting the License. | |
| 14 | ||
| 15 | "Legal Entity" shall mean the union of the acting entity and all | |
| 16 | other entities that control, are controlled by, or are under common | |
| 17 | control with that entity. For the purposes of this definition, | |
| 18 | "control" means (i) the power, direct or indirect, to cause the | |
| 19 | direction or management of such entity, whether by contract or | |
| 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the | |
| 21 | outstanding shares, or (iii) beneficial ownership of such entity. | |
| 22 | ||
| 23 | "You" (or "Your") shall mean an individual or Legal Entity | |
| 24 | exercising permissions granted by this License. | |
| 25 | ||
| 26 | "Source" form shall mean the preferred form for making modifications, | |
| 27 | including but not limited to software source code, documentation | |
| 28 | source, and configuration files. | |
| 29 | ||
| 30 | "Object" form shall mean any form resulting from mechanical | |
| 31 | transformation or translation of a Source form, including but | |
| 32 | not limited to compiled object code, generated documentation, | |
| 33 | and conversions to other media types. | |
| 34 | ||
| 35 | "Work" shall mean the work of authorship, whether in Source or | |
| 36 | Object form, made available under the License, as indicated by a | |
| 37 | copyright notice that is included in or attached to the work | |
| 38 | (an example is provided in the Appendix below). | |
| 39 | ||
| 40 | "Derivative Works" shall mean any work, whether in Source or Object | |
| 41 | form, that is based on (or derived from) the Work and for which the | |
| 42 | editorial revisions, annotations, elaborations, or other modifications | |
| 43 | represent, as a whole, an original work of authorship. For the purposes | |
| 44 | of this License, Derivative Works shall not include works that remain | |
| 45 | separable from, or merely link (or bind by name) to the interfaces of, | |
| 46 | the Work and Derivative Works thereof. | |
| 47 | ||
| 48 | "Contribution" shall mean any work of authorship, including | |
| 49 | the original version of the Work and any modifications or additions | |
| 50 | to that Work or Derivative Works thereof, that is intentionally | |
| 51 | submitted to Licensor for inclusion in the Work by the copyright owner | |
| 52 | or by an individual or Legal Entity authorized to submit on behalf of | |
| 53 | the copyright owner. For the purposes of this definition, "submitted" | |
| 54 | means any form of electronic, verbal, or written communication sent | |
| 55 | to the Licensor or its representatives, including but not limited to | |
| 56 | communication on electronic mailing lists, source code control systems, | |
| 57 | and issue tracking systems that are managed by, or on behalf of, the | |
| 58 | Licensor for the purpose of discussing and improving the Work, but | |
| 59 | excluding communication that is conspicuously marked or otherwise | |
| 60 | designated in writing by the copyright owner as "Not a Contribution." | |
| 61 | ||
| 62 | "Contributor" shall mean Licensor and any individual or Legal Entity | |
| 63 | on behalf of whom a Contribution has been received by Licensor and | |
| 64 | subsequently incorporated within the Work. | |
| 65 | ||
| 66 | 2. Grant of Copyright License. Subject to the terms and conditions of | |
| 67 | this License, each Contributor hereby grants to You a perpetual, | |
| 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 69 | copyright license to reproduce, prepare Derivative Works of, | |
| 70 | publicly display, publicly perform, sublicense, and distribute the | |
| 71 | Work and such Derivative Works in Source or Object form. | |
| 72 | ||
| 73 | 3. Grant of Patent License. Subject to the terms and conditions of | |
| 74 | this License, each Contributor hereby grants to You a perpetual, | |
| 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 76 | (except as stated in this section) patent license to make, have made, | |
| 77 | use, offer to sell, sell, import, and otherwise transfer the Work, | |
| 78 | where such license applies only to those patent claims licensable | |
| 79 | by such Contributor that are necessarily infringed by their | |
| 80 | Contribution(s) alone or by combination of their Contribution(s) | |
| 81 | with the Work to which such Contribution(s) was submitted. If You | |
| 82 | institute patent litigation against any entity (including a | |
| 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work | |
| 84 | or a Contribution incorporated within the Work constitutes direct | |
| 85 | or contributory patent infringement, then any patent licenses | |
| 86 | granted to You under this License for that Work shall terminate | |
| 87 | as of the date such litigation is filed. | |
| 88 | ||
| 89 | 4. Redistribution. You may reproduce and distribute copies of the | |
| 90 | Work or Derivative Works thereof in any medium, with or without | |
| 91 | modifications, and in Source or Object form, provided that You | |
| 92 | meet the following conditions: | |
| 93 | ||
| 94 | (a) You must give any other recipients of the Work or | |
| 95 | Derivative Works a copy of this License; and | |
| 96 | ||
| 97 | (b) You must cause any modified files to carry prominent notices | |
| 98 | stating that You changed the files; and | |
| 99 | ||
| 100 | (c) You must retain, in the Source form of any Derivative Works | |
| 101 | that You distribute, all copyright, patent, trademark, and | |
| 102 | attribution notices from the Source form of the Work, | |
| 103 | excluding those notices that do not pertain to any part of | |
| 104 | the Derivative Works; and | |
| 105 | ||
| 106 | (d) If the Work includes a "NOTICE" text file as part of its | |
| 107 | distribution, then any Derivative Works that You distribute must | |
| 108 | include a readable copy of the attribution notices contained | |
| 109 | within such NOTICE file, excluding those notices that do not | |
| 110 | pertain to any part of the Derivative Works, in at least one | |
| 111 | of the following places: within a NOTICE text file distributed | |
| 112 | as part of the Derivative Works; within the Source form or | |
| 113 | documentation, if provided along with the Derivative Works; or, | |
| 114 | within a display generated by the Derivative Works, if and | |
| 115 | wherever such third-party notices normally appear. The contents | |
| 116 | of the NOTICE file are for informational purposes only and | |
| 117 | do not modify the License. You may add Your own attribution | |
| 118 | notices within Derivative Works that You distribute, alongside | |
| 119 | or as an addendum to the NOTICE text from the Work, provided | |
| 120 | that such additional attribution notices cannot be construed | |
| 121 | as modifying the License. | |
| 122 | ||
| 123 | You may add Your own copyright statement to Your modifications and | |
| 124 | may provide additional or different license terms and conditions | |
| 125 | for use, reproduction, or distribution of Your modifications, or | |
| 126 | for any such Derivative Works as a whole, provided Your use, | |
| 127 | reproduction, and distribution of the Work otherwise complies with | |
| 128 | the conditions stated in this License. | |
| 129 | ||
| 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, | |
| 131 | any Contribution intentionally submitted for inclusion in the Work | |
| 132 | by You to the Licensor shall be under the terms and conditions of | |
| 133 | this License, without any additional terms or conditions. | |
| 134 | Notwithstanding the above, nothing herein shall supersede or modify | |
| 135 | the terms of any separate license agreement you may have executed | |
| 136 | with Licensor regarding such Contributions. | |
| 137 | ||
| 138 | 6. Trademarks. This License does not grant permission to use the trade | |
| 139 | names, trademarks, service marks, or product names of the Licensor, | |
| 140 | except as required for reasonable and customary use in describing the | |
| 141 | origin of the Work and reproducing the content of the NOTICE file. | |
| 142 | ||
| 143 | 7. Disclaimer of Warranty. Unless required by applicable law or | |
| 144 | agreed to in writing, Licensor provides the Work (and each | |
| 145 | Contributor provides its Contributions) on an "AS IS" BASIS, | |
| 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |
| 147 | implied, including, without limitation, any warranties or conditions | |
| 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | |
| 149 | PARTICULAR PURPOSE. You are solely responsible for determining the | |
| 150 | appropriateness of using or redistributing the Work and assume any | |
| 151 | risks associated with Your exercise of permissions under this License. | |
| 152 | ||
| 153 | 8. Limitation of Liability. In no event and under no legal theory, | |
| 154 | whether in tort (including negligence), contract, or otherwise, | |
| 155 | unless required by applicable law (such as deliberate and grossly | |
| 156 | negligent acts) or agreed to in writing, shall any Contributor be | |
| 157 | liable to You for damages, including any direct, indirect, special, | |
| 158 | incidental, or consequential damages of any character arising as a | |
| 159 | result of this License or out of the use or inability to use the | |
| 160 | Work (including but not limited to damages for loss of goodwill, | |
| 161 | work stoppage, computer failure or malfunction, or any and all | |
| 162 | other commercial damages or losses), even if such Contributor | |
| 163 | has been advised of the possibility of such damages. | |
| 164 | ||
| 165 | 9. Accepting Warranty or Additional Liability. While redistributing | |
| 166 | the Work or Derivative Works thereof, You may choose to offer, | |
| 167 | and charge a fee for, acceptance of support, warranty, indemnity, | |
| 168 | or other liability obligations and/or rights consistent with this | |
| 169 | License. However, in accepting such obligations, You may act only | |
| 170 | on Your own behalf and on Your sole responsibility, not on behalf | |
| 171 | of any other Contributor, and only if You agree to indemnify, | |
| 172 | defend, and hold each Contributor harmless for any liability | |
| 173 | incurred by, or claims asserted against, such Contributor by reason | |
| 174 | of your accepting any such warranty or additional liability. | |
| 175 | ||
| 176 | END OF TERMS AND CONDITIONS | |
| 177 | ||
| 178 | APPENDIX: How to apply the Apache License to your work. | |
| 179 | ||
| 180 | To apply the Apache License to your work, attach the following | |
| 181 | boilerplate notice, with the fields enclosed by brackets "[]" | |
| 182 | replaced with your own identifying information. (Don't include | |
| 183 | the brackets!) The text should be enclosed in the appropriate | |
| 184 | comment syntax for the file format. We also recommend that a | |
| 185 | file or class name and description of purpose be included on the | |
| 186 | same "printed page" as the copyright notice for easier | |
| 187 | identification within third-party archives. | |
| 188 | ||
| 189 | Copyright [yyyy] [name of copyright owner] | |
| 190 | ||
| 191 | Licensed under the Apache License, Version 2.0 (the "License"); | |
| 192 | you may not use this file except in compliance with the License. | |
| 193 | You may obtain a copy of the License at | |
| 194 | ||
| 195 | http://www.apache.org/licenses/LICENSE-2.0 | |
| 196 | ||
| 197 | Unless required by applicable law or agreed to in writing, software | |
| 198 | distributed under the License is distributed on an "AS IS" BASIS, | |
| 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 200 | See the License for the specific language governing permissions and | |
| 201 | limitations under the License. |
| … | … | @@ -0,0 +1,21 @@ |
| 1 | # bare-dns | |
| 2 | ||
| 3 | Domain name resolution for JavaScript. | |
| 4 | ||
| 5 | ``` | |
| 6 | npm i bare-dns | |
| 7 | ``` | |
| 8 | ||
| 9 | ## Usage | |
| 10 | ||
| 11 | ```js | |
| 12 | const dns = require('bare-dns') | |
| 13 | ||
| 14 | dns.lookup('github.com', (err, address, family) => { | |
| 15 | console.log(address, family) | |
| 16 | }) | |
| 17 | ``` | |
| 18 | ||
| 19 | ## License | |
| 20 | ||
| 21 | Apache-2.0 |
| … | … | @@ -0,0 +1,607 @@ |
| 1 | #include <ares.h> | |
| 2 | #include <assert.h> | |
| 3 | #include <bare.h> | |
| 4 | #include <intrusive.h> | |
| 5 | #include <intrusive/list.h> | |
| 6 | #include <js.h> | |
| 7 | #include <stddef.h> | |
| 8 | #include <stdlib.h> | |
| 9 | #include <string.h> | |
| 10 | #include <utf.h> | |
| 11 | #include <uv.h> | |
| 12 | ||
| 13 | typedef struct { | |
| 14 | uv_getaddrinfo_t handle; | |
| 15 | ||
| 16 | js_env_t *env; | |
| 17 | js_ref_t *ctx; | |
| 18 | js_ref_t *cb; | |
| 19 | ||
| 20 | bool all; | |
| 21 | bool exiting; | |
| 22 | ||
| 23 | js_deferred_teardown_t *teardown; | |
| 24 | } bare_dns_lookup_t; | |
| 25 | ||
| 26 | typedef struct { | |
| 27 | ares_channel channel; | |
| 28 | intrusive_list_t tasks; | |
| 29 | ||
| 30 | bool exiting; | |
| 31 | ||
| 32 | js_env_t *env; | |
| 33 | js_deferred_teardown_t *teardown; | |
| 34 | } bare_dns_resolver_t; | |
| 35 | ||
| 36 | typedef struct { | |
| 37 | bare_dns_resolver_t *resolver; | |
| 38 | ||
| 39 | ares_socket_t socket; | |
| 40 | uv_poll_t poll; | |
| 41 | ||
| 42 | intrusive_list_node_t node; | |
| 43 | } bare_dns_resolve_task_t; | |
| 44 | ||
| 45 | typedef struct { | |
| 46 | bare_dns_resolver_t *resolver; | |
| 47 | ||
| 48 | js_env_t *env; | |
| 49 | js_ref_t *ctx; | |
| 50 | js_ref_t *cb; | |
| 51 | } bare_dns_resolve_req_t; | |
| 52 | ||
| 53 | static uv_once_t bare_dns__init_guard = UV_ONCE_INIT; | |
| 54 | ||
| 55 | static void | |
| 56 | bare_dns__on_lookup(uv_getaddrinfo_t *handle, int status, struct addrinfo *res) { | |
| 57 | int err; | |
| 58 | ||
| 59 | bare_dns_lookup_t *req = (bare_dns_lookup_t *) handle; | |
| 60 | ||
| 61 | js_deferred_teardown_t *teardown = req->teardown; | |
| 62 | ||
| 63 | js_env_t *env = req->env; | |
| 64 | ||
| 65 | js_handle_scope_t *scope; | |
| 66 | err = js_open_handle_scope(env, &scope); | |
| 67 | assert(err == 0); | |
| 68 | ||
| 69 | js_value_t *ctx; | |
| 70 | err = js_get_reference_value(env, req->ctx, &ctx); | |
| 71 | assert(err == 0); | |
| 72 | ||
| 73 | js_value_t *cb; | |
| 74 | err = js_get_reference_value(env, req->cb, &cb); | |
| 75 | assert(err == 0); | |
| 76 | ||
| 77 | err = js_delete_reference(env, req->cb); | |
| 78 | assert(err == 0); | |
| 79 | ||
| 80 | err = js_delete_reference(env, req->ctx); | |
| 81 | assert(err == 0); | |
| 82 | ||
| 83 | js_value_t *args[2]; | |
| 84 | ||
| 85 | if (status < 0) { | |
| 86 | js_value_t *code; | |
| 87 | err = js_create_string_utf8(env, (utf8_t *) uv_err_name(status), -1, &code); | |
| 88 | assert(err == 0); | |
| 89 | ||
| 90 | js_value_t *message; | |
| 91 | err = js_create_string_utf8(env, (utf8_t *) uv_strerror(status), -1, &message); | |
| 92 | assert(err == 0); | |
| 93 | ||
| 94 | err = js_create_error(env, code, message, &args[0]); | |
| 95 | assert(err == 0); | |
| 96 | ||
| 97 | err = js_get_null(env, &args[1]); | |
| 98 | assert(err == 0); | |
| 99 | } else { | |
| 100 | err = js_get_null(env, &args[0]); | |
| 101 | assert(err == 0); | |
| 102 | ||
| 103 | js_value_t *result; | |
| 104 | err = js_create_array(env, &result); | |
| 105 | assert(err == 0); | |
| 106 | ||
| 107 | uint32_t i = 0; | |
| 108 | ||
| 109 | for (struct addrinfo *next = res; next != NULL; next = next->ai_next) { | |
| 110 | assert(next->ai_socktype == SOCK_STREAM); | |
| 111 | ||
| 112 | int family; | |
| 113 | ||
| 114 | char ip[INET6_ADDRSTRLEN]; | |
| 115 | ||
| 116 | if (next->ai_family == AF_INET) { | |
| 117 | family = 4; | |
| 118 | err = uv_ip4_name((struct sockaddr_in *) next->ai_addr, ip, sizeof(ip)); | |
| 119 | } else if (next->ai_family == AF_INET6) { | |
| 120 | family = 6; | |
| 121 | err = uv_ip6_name((struct sockaddr_in6 *) next->ai_addr, ip, sizeof(ip)); | |
| 122 | } else { | |
| 123 | continue; | |
| 124 | } | |
| 125 | ||
| 126 | assert(err == 0); | |
| 127 | ||
| 128 | js_value_t *address; | |
| 129 | err = js_create_object(env, &address); | |
| 130 | assert(err == 0); | |
| 131 | ||
| 132 | err = js_set_element(env, result, i++, address); | |
| 133 | assert(err == 0); | |
| 134 | ||
| 135 | js_value_t *value; | |
| 136 | ||
| 137 | err = js_create_string_utf8(env, (utf8_t *) ip, -1, &value); | |
| 138 | assert(err == 0); | |
| 139 | ||
| 140 | err = js_set_named_property(env, address, "address", value); | |
| 141 | assert(err == 0); | |
| 142 | ||
| 143 | err = js_create_uint32(env, family, &value); | |
| 144 | assert(err == 0); | |
| 145 | ||
| 146 | err = js_set_named_property(env, address, "family", value); | |
| 147 | assert(err == 0); | |
| 148 | ||
| 149 | if (!req->all) break; | |
| 150 | } | |
| 151 | ||
| 152 | if (i > 0) args[1] = result; | |
| 153 | else { | |
| 154 | js_value_t *code; | |
| 155 | err = js_create_string_utf8(env, (utf8_t *) uv_err_name(UV_EAI_NODATA), -1, &code); | |
| 156 | assert(err == 0); | |
| 157 | ||
| 158 | js_value_t *message; | |
| 159 | err = js_create_string_utf8(env, (utf8_t *) uv_strerror(UV_EAI_NODATA), -1, &message); | |
| 160 | assert(err == 0); | |
| 161 | ||
| 162 | err = js_create_error(env, code, message, &args[0]); | |
| 163 | assert(err == 0); | |
| 164 | ||
| 165 | err = js_get_null(env, &args[1]); | |
| 166 | assert(err == 0); | |
| 167 | } | |
| 168 | } | |
| 169 | ||
| 170 | uv_freeaddrinfo(res); | |
| 171 | ||
| 172 | if (!req->exiting) js_call_function(env, ctx, cb, 2, args, NULL); | |
| 173 | ||
| 174 | err = js_close_handle_scope(env, scope); | |
| 175 | assert(err == 0); | |
| 176 | ||
| 177 | err = js_finish_deferred_teardown_callback(teardown); | |
| 178 | assert(err == 0); | |
| 179 | } | |
| 180 | ||
| 181 | static void | |
| 182 | bare_dns__on_lookup_teardown(js_deferred_teardown_t *handle, void *data) { | |
| 183 | bare_dns_lookup_t *req = (bare_dns_lookup_t *) data; | |
| 184 | ||
| 185 | req->exiting = true; | |
| 186 | ||
| 187 | uv_cancel((uv_req_t *) &req->handle); | |
| 188 | } | |
| 189 | ||
| 190 | static js_value_t * | |
| 191 | bare_dns_lookup(js_env_t *env, js_callback_info_t *info) { | |
| 192 | int err; | |
| 193 | ||
| 194 | size_t argc = 5; | |
| 195 | js_value_t *argv[5]; | |
| 196 | ||
| 197 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 198 | assert(err == 0); | |
| 199 | ||
| 200 | assert(argc == 5); | |
| 201 | ||
| 202 | size_t len; | |
| 203 | err = js_get_value_string_utf8(env, argv[0], NULL, 0, &len); | |
| 204 | assert(err == 0); | |
| 205 | ||
| 206 | len += 1 /* NULL */; | |
| 207 | ||
| 208 | utf8_t *hostname = malloc(len); | |
| 209 | err = js_get_value_string_utf8(env, argv[0], hostname, len, NULL); | |
| 210 | assert(err == 0); | |
| 211 | ||
| 212 | uint32_t family; | |
| 213 | err = js_get_value_uint32(env, argv[1], &family); | |
| 214 | assert(err == 0); | |
| 215 | ||
| 216 | bool all; | |
| 217 | err = js_get_value_bool(env, argv[2], &all); | |
| 218 | assert(err == 0); | |
| 219 | ||
| 220 | struct addrinfo hints = { | |
| 221 | .ai_family = family == 4 | |
| 222 | ? AF_INET | |
| 223 | : family == 6 ? AF_INET6 | |
| 224 | : AF_UNSPEC, | |
| 225 | .ai_socktype = SOCK_STREAM, | |
| 226 | .ai_flags = 0, | |
| 227 | }; | |
| 228 | ||
| 229 | js_value_t *handle; | |
| 230 | ||
| 231 | bare_dns_lookup_t *req; | |
| 232 | err = js_create_arraybuffer(env, sizeof(bare_dns_lookup_t), (void **) &req, &handle); | |
| 233 | assert(err == 0); | |
| 234 | ||
| 235 | req->env = env; | |
| 236 | req->all = all; | |
| 237 | req->exiting = false; | |
| 238 | ||
| 239 | err = js_create_reference(env, argv[3], 1, &req->ctx); | |
| 240 | assert(err == 0); | |
| 241 | ||
| 242 | err = js_create_reference(env, argv[4], 1, &req->cb); | |
| 243 | assert(err == 0); | |
| 244 | ||
| 245 | uv_loop_t *loop; | |
| 246 | err = js_get_env_loop(env, &loop); | |
| 247 | assert(err == 0); | |
| 248 | ||
| 249 | err = uv_getaddrinfo(loop, &req->handle, bare_dns__on_lookup, (const char *) hostname, NULL, &hints); | |
| 250 | ||
| 251 | free(hostname); | |
| 252 | ||
| 253 | if (err < 0) { | |
| 254 | err = js_throw_error(env, uv_err_name(err), uv_strerror(err)); | |
| 255 | assert(err == 0); | |
| 256 | ||
| 257 | return NULL; | |
| 258 | } | |
| 259 | ||
| 260 | err = js_add_deferred_teardown_callback(env, bare_dns__on_lookup_teardown, (void *) req, &req->teardown); | |
| 261 | assert(err == 0); | |
| 262 | ||
| 263 | return handle; | |
| 264 | } | |
| 265 | ||
| 266 | static void | |
| 267 | bare_dns__on_poll_close(uv_handle_t *handle) { | |
| 268 | int err; | |
| 269 | ||
| 270 | uv_poll_t *poll = (uv_poll_t *) handle; | |
| 271 | ||
| 272 | bare_dns_resolve_task_t *task = intrusive_entry(poll, bare_dns_resolve_task_t, poll); | |
| 273 | ||
| 274 | bare_dns_resolver_t *resolver = task->resolver; | |
| 275 | ||
| 276 | intrusive_list_remove(&resolver->tasks, &task->node); | |
| 277 | ||
| 278 | free(task); | |
| 279 | ||
| 280 | if (resolver->exiting && intrusive_list_empty(&resolver->tasks)) { | |
| 281 | ares_destroy(resolver->channel); | |
| 282 | ||
| 283 | err = js_finish_deferred_teardown_callback(resolver->teardown); | |
| 284 | assert(err == 0); | |
| 285 | } | |
| 286 | } | |
| 287 | ||
| 288 | static void | |
| 289 | bare_dns__on_poll_update(uv_poll_t *poll, int status, int events) { | |
| 290 | assert(status == 0); | |
| 291 | ||
| 292 | bare_dns_resolve_task_t *task = intrusive_entry(poll, bare_dns_resolve_task_t, poll); | |
| 293 | ||
| 294 | ares_process_fd( | |
| 295 | task->resolver->channel, | |
| 296 | events & UV_READABLE ? task->socket : ARES_SOCKET_BAD, | |
| 297 | events & UV_WRITABLE ? task->socket : ARES_SOCKET_BAD | |
| 298 | ); | |
| 299 | } | |
| 300 | ||
| 301 | static void | |
| 302 | bare_dns__on_resolver_teardown(js_deferred_teardown_t *handle, void *data) { | |
| 303 | int err; | |
| 304 | ||
| 305 | bare_dns_resolver_t *resolver = (bare_dns_resolver_t *) data; | |
| 306 | ||
| 307 | if (resolver->exiting) return; | |
| 308 | ||
| 309 | resolver->exiting = true; | |
| 310 | ||
| 311 | if (intrusive_list_empty(&resolver->tasks)) { | |
| 312 | ares_destroy(resolver->channel); | |
| 313 | ||
| 314 | err = js_finish_deferred_teardown_callback(resolver->teardown); | |
| 315 | assert(err == 0); | |
| 316 | } else { | |
| 317 | intrusive_list_for_each(next, &resolver->tasks) { | |
| 318 | bare_dns_resolve_task_t *task = intrusive_entry(next, bare_dns_resolve_task_t, node); | |
| 319 | ||
| 320 | uv_close((uv_handle_t *) &task->poll, bare_dns__on_poll_close); | |
| 321 | } | |
| 322 | } | |
| 323 | } | |
| 324 | ||
| 325 | static void | |
| 326 | bare_dns__on_socket_change(void *data, ares_socket_t socket, int read, int write) { | |
| 327 | int err; | |
| 328 | ||
| 329 | bare_dns_resolver_t *resolver = (bare_dns_resolver_t *) data; | |
| 330 | ||
| 331 | if (resolver->exiting) return; | |
| 332 | ||
| 333 | bare_dns_resolve_task_t *task = NULL; | |
| 334 | ||
| 335 | intrusive_list_for_each(next, &resolver->tasks) { | |
| 336 | bare_dns_resolve_task_t *candidate = intrusive_entry(next, bare_dns_resolve_task_t, node); | |
| 337 | ||
| 338 | if (candidate->socket == socket) { | |
| 339 | task = candidate; | |
| 340 | break; | |
| 341 | } | |
| 342 | } | |
| 343 | ||
| 344 | if (task == NULL) { | |
| 345 | task = malloc(sizeof(bare_dns_resolve_task_t)); | |
| 346 | ||
| 347 | task->resolver = resolver; | |
| 348 | task->socket = socket; | |
| 349 | ||
| 350 | intrusive_list_append(&resolver->tasks, &task->node); | |
| 351 | } | |
| 352 | ||
| 353 | if (read || write) { | |
| 354 | uv_loop_t *loop; | |
| 355 | err = js_get_env_loop(resolver->env, &loop); | |
| 356 | assert(err == 0); | |
| 357 | ||
| 358 | err = uv_poll_init_socket(loop, &task->poll, task->socket); | |
| 359 | assert(err == 0); | |
| 360 | ||
| 361 | int events = (read ? UV_READABLE : 0) | (write ? UV_WRITABLE : 0); | |
| 362 | ||
| 363 | err = uv_poll_start(&task->poll, events, bare_dns__on_poll_update); | |
| 364 | assert(err == 0); | |
| 365 | } else { | |
| 366 | uv_close((uv_handle_t *) &task->poll, bare_dns__on_poll_close); | |
| 367 | } | |
| 368 | } | |
| 369 | ||
| 370 | static js_value_t * | |
| 371 | bare_dns_init_resolver(js_env_t *env, js_callback_info_t *info) { | |
| 372 | int err; | |
| 373 | ||
| 374 | js_value_t *handle; | |
| 375 | ||
| 376 | bare_dns_resolver_t *resolver; | |
| 377 | err = js_create_arraybuffer(env, sizeof(bare_dns_resolver_t), (void **) &resolver, &handle); | |
| 378 | assert(err == 0); | |
| 379 | ||
| 380 | intrusive_list_init(&resolver->tasks); | |
| 381 | ||
| 382 | struct ares_options opts; | |
| 383 | opts.sock_state_cb = bare_dns__on_socket_change; | |
| 384 | opts.sock_state_cb_data = resolver; | |
| 385 | ||
| 386 | err = ares_init_options(&resolver->channel, &opts, ARES_OPT_SOCK_STATE_CB); | |
| 387 | ||
| 388 | if (err != ARES_SUCCESS) { | |
| 389 | err = js_throw_error(env, NULL, ares_strerror(err)); | |
| 390 | assert(err == 0); | |
| 391 | ||
| 392 | return NULL; | |
| 393 | } | |
| 394 | ||
| 395 | resolver->env = env; | |
| 396 | ||
| 397 | err = js_add_deferred_teardown_callback(env, bare_dns__on_resolver_teardown, (void *) resolver, &resolver->teardown); | |
| 398 | assert(err == 0); | |
| 399 | ||
| 400 | return handle; | |
| 401 | } | |
| 402 | ||
| 403 | static js_value_t * | |
| 404 | bare_dns_destroy_resolver(js_env_t *env, js_callback_info_t *info) { | |
| 405 | int err; | |
| 406 | ||
| 407 | size_t argc = 1; | |
| 408 | js_value_t *argv[1]; | |
| 409 | ||
| 410 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 411 | assert(err == 0); | |
| 412 | ||
| 413 | assert(argc == 1); | |
| 414 | ||
| 415 | bare_dns_resolver_t *resolver; | |
| 416 | err = js_get_arraybuffer_info(env, argv[0], (void **) &resolver, NULL); | |
| 417 | assert(err == 0); | |
| 418 | ||
| 419 | if (resolver->exiting) return NULL; | |
| 420 | ||
| 421 | resolver->exiting = true; | |
| 422 | ||
| 423 | if (intrusive_list_empty(&resolver->tasks)) { | |
| 424 | ares_destroy(resolver->channel); | |
| 425 | ||
| 426 | err = js_finish_deferred_teardown_callback(resolver->teardown); | |
| 427 | assert(err == 0); | |
| 428 | } else { | |
| 429 | intrusive_list_for_each(next, &resolver->tasks) { | |
| 430 | bare_dns_resolve_task_t *task = intrusive_entry(next, bare_dns_resolve_task_t, node); | |
| 431 | ||
| 432 | uv_close((uv_handle_t *) &task->poll, bare_dns__on_poll_close); | |
| 433 | } | |
| 434 | } | |
| 435 | ||
| 436 | return NULL; | |
| 437 | } | |
| 438 | ||
| 439 | static void | |
| 440 | bare_dns__on_resolve_txt(void *data, ares_status_t status, size_t timeouts, const ares_dns_record_t *dnsrec) { | |
| 441 | int err; | |
| 442 | ||
| 443 | bare_dns_resolve_req_t *req = (bare_dns_resolve_req_t *) data; | |
| 444 | ||
| 445 | if (req->resolver->exiting) return; | |
| 446 | ||
| 447 | js_env_t *env = req->env; | |
| 448 | ||
| 449 | js_handle_scope_t *scope; | |
| 450 | err = js_open_handle_scope(env, &scope); | |
| 451 | assert(err == 0); | |
| 452 | ||
| 453 | js_value_t *ctx; | |
| 454 | err = js_get_reference_value(env, req->ctx, &ctx); | |
| 455 | assert(err == 0); | |
| 456 | ||
| 457 | js_value_t *cb; | |
| 458 | err = js_get_reference_value(env, req->cb, &cb); | |
| 459 | assert(err == 0); | |
| 460 | ||
| 461 | err = js_delete_reference(env, req->cb); | |
| 462 | assert(err == 0); | |
| 463 | ||
| 464 | err = js_delete_reference(env, req->ctx); | |
| 465 | assert(err == 0); | |
| 466 | ||
| 467 | js_value_t *args[2]; | |
| 468 | ||
| 469 | if (status == ARES_SUCCESS) { | |
| 470 | err = js_get_null(env, &args[0]); | |
| 471 | assert(err == 0); | |
| 472 | ||
| 473 | js_value_t *result; | |
| 474 | err = js_create_array(env, &result); | |
| 475 | assert(err == 0); | |
| 476 | ||
| 477 | for (size_t i = 0, n = ares_dns_record_rr_cnt(dnsrec, ARES_SECTION_ANSWER); i < n; i++) { | |
| 478 | const ares_dns_rr_t *rr = ares_dns_record_rr_get_const(dnsrec, ARES_SECTION_ANSWER, i); | |
| 479 | ||
| 480 | if (ares_dns_rr_get_type(rr) != ARES_REC_TYPE_TXT) continue; | |
| 481 | ||
| 482 | js_value_t *record; | |
| 483 | err = js_create_array(env, &record); | |
| 484 | assert(err == 0); | |
| 485 | ||
| 486 | err = js_set_element(env, result, (uint32_t) i, record); | |
| 487 | assert(err == 0); | |
| 488 | ||
| 489 | for (size_t i = 0, n = ares_dns_rr_get_abin_cnt(rr, ARES_RR_TXT_DATA); i < n; i++) { | |
| 490 | size_t len; | |
| 491 | ||
| 492 | const uint8_t *data = ares_dns_rr_get_abin(rr, ARES_RR_TXT_DATA, i, &len); | |
| 493 | ||
| 494 | js_value_t *chunk; | |
| 495 | err = js_create_string_utf8(env, (const utf8_t *) data, len, &chunk); | |
| 496 | assert(err == 0); | |
| 497 | ||
| 498 | err = js_set_element(env, record, (uint32_t) i, chunk); | |
| 499 | assert(err == 0); | |
| 500 | } | |
| 501 | } | |
| 502 | ||
| 503 | args[1] = result; | |
| 504 | } else { | |
| 505 | js_value_t *message; | |
| 506 | err = js_create_string_utf8(env, (utf8_t *) ares_strerror(status), -1, &message); | |
| 507 | assert(err == 0); | |
| 508 | ||
| 509 | err = js_create_error(env, NULL, message, &args[0]); | |
| 510 | assert(err == 0); | |
| 511 | ||
| 512 | err = js_get_null(env, &args[1]); | |
| 513 | assert(err == 0); | |
| 514 | } | |
| 515 | ||
| 516 | js_call_function(env, ctx, cb, 2, args, NULL); | |
| 517 | ||
| 518 | err = js_close_handle_scope(env, scope); | |
| 519 | assert(err == 0); | |
| 520 | } | |
| 521 | ||
| 522 | static js_value_t * | |
| 523 | bare_dns_resolve_txt(js_env_t *env, js_callback_info_t *info) { | |
| 524 | int err; | |
| 525 | ||
| 526 | size_t argc = 4; | |
| 527 | js_value_t *argv[4]; | |
| 528 | ||
| 529 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 530 | assert(err == 0); | |
| 531 | ||
| 532 | assert(argc == 4); | |
| 533 | ||
| 534 | bare_dns_resolver_t *resolver; | |
| 535 | err = js_get_arraybuffer_info(env, argv[0], (void **) &resolver, NULL); | |
| 536 | assert(err == 0); | |
| 537 | ||
| 538 | size_t len; | |
| 539 | err = js_get_value_string_utf8(env, argv[1], NULL, 0, &len); | |
| 540 | assert(err == 0); | |
| 541 | ||
| 542 | len += 1 /* NULL */; | |
| 543 | ||
| 544 | utf8_t *hostname = malloc(len); | |
| 545 | err = js_get_value_string_utf8(env, argv[1], hostname, len, NULL); | |
| 546 | assert(err == 0); | |
| 547 | ||
| 548 | js_value_t *handle; | |
| 549 | ||
| 550 | bare_dns_resolve_req_t *req; | |
| 551 | err = js_create_arraybuffer(env, sizeof(bare_dns_resolve_req_t), (void **) &req, &handle); | |
| 552 | assert(err == 0); | |
| 553 | ||
| 554 | req->resolver = resolver; | |
| 555 | req->env = env; | |
| 556 | ||
| 557 | err = js_create_reference(env, argv[2], 1, &req->cb); | |
| 558 | assert(err == 0); | |
| 559 | ||
| 560 | err = js_create_reference(env, argv[3], 1, &req->ctx); | |
| 561 | assert(err == 0); | |
| 562 | ||
| 563 | err = ares_query_dnsrec(req->resolver->channel, (char *) hostname, ARES_CLASS_IN, ARES_REC_TYPE_TXT, bare_dns__on_resolve_txt, req, NULL); | |
| 564 | ||
| 565 | free(hostname); | |
| 566 | ||
| 567 | if (err != ARES_SUCCESS) { | |
| 568 | err = js_throw_error(env, NULL, ares_strerror(err)); | |
| 569 | assert(err == 0); | |
| 570 | ||
| 571 | return NULL; | |
| 572 | } | |
| 573 | ||
| 574 | return NULL; | |
| 575 | } | |
| 576 | ||
| 577 | static void | |
| 578 | bare_dns__on_init(void) { | |
| 579 | ares_library_init(ARES_LIB_INIT_ALL); | |
| 580 | } | |
| 581 | ||
| 582 | static js_value_t * | |
| 583 | bare_dns_exports(js_env_t *env, js_value_t *exports) { | |
| 584 | uv_once(&bare_dns__init_guard, bare_dns__on_init); | |
| 585 | ||
| 586 | int err; | |
| 587 | ||
| 588 | #define V(name, fn) \ | |
| 589 | { \ | |
| 590 | js_value_t *val; \ | |
| 591 | err = js_create_function(env, name, -1, fn, NULL, &val); \ | |
| 592 | assert(err == 0); \ | |
| 593 | err = js_set_named_property(env, exports, name, val); \ | |
| 594 | assert(err == 0); \ | |
| 595 | } | |
| 596 | ||
| 597 | V("lookup", bare_dns_lookup) | |
| 598 | ||
| 599 | V("initResolver", bare_dns_init_resolver) | |
| 600 | V("destroyResolver", bare_dns_destroy_resolver) | |
| 601 | V("resolveTxt", bare_dns_resolve_txt) | |
| 602 | #undef V | |
| 603 | ||
| 604 | return exports; | |
| 605 | } | |
| 606 | ||
| 607 | BARE_MODULE(bare_dns, bare_dns_exports) |
| … | … | @@ -0,0 +1 @@ |
| 1 | module.exports = require.addon() |
| … | … | @@ -0,0 +1,55 @@ |
| 1 | type IPFamily = 4 | 6 | |
| 2 | ||
| 3 | interface LookupOptions { | |
| 4 | family?: `IPv${IPFamily}` | IPFamily | 0 | |
| 5 | hints?: number | |
| 6 | all?: boolean | |
| 7 | } | |
| 8 | ||
| 9 | declare class DNSResolver { | |
| 10 | resolveTxt( | |
| 11 | hostname: string, | |
| 12 | cb: (err: Error | null, records: string[][]) => void | |
| 13 | ): void | |
| 14 | ||
| 15 | destroy(): void | |
| 16 | } | |
| 17 | ||
| 18 | declare namespace dns { | |
| 19 | export function lookup( | |
| 20 | hostname: string, | |
| 21 | cb: ( | |
| 22 | err: Error | null, | |
| 23 | address: string | null, | |
| 24 | family: IPFamily | 0 | |
| 25 | ) => void | |
| 26 | ): void | |
| 27 | ||
| 28 | export function lookup( | |
| 29 | hostname: string, | |
| 30 | opts: LookupOptions & { all?: false }, | |
| 31 | cb: ( | |
| 32 | err: Error | null, | |
| 33 | address: string | null, | |
| 34 | family: IPFamily | 0 | |
| 35 | ) => void | |
| 36 | ): void | |
| 37 | ||
| 38 | export function lookup( | |
| 39 | hostname: string, | |
| 40 | opts: LookupOptions & { all: true }, | |
| 41 | cb: ( | |
| 42 | err: Error | null, | |
| 43 | addresses: { address: string; family: IPFamily }[] | null | |
| 44 | ) => void | |
| 45 | ): void | |
| 46 | ||
| 47 | export function resolveTxt( | |
| 48 | hostname: string, | |
| 49 | cb: (err: Error | null, records: string[][]) => void | |
| 50 | ): void | |
| 51 | ||
| 52 | export { type IPFamily, type LookupOptions, DNSResolver as Resolver } | |
| 53 | } | |
| 54 | ||
| 55 | export = dns |
| … | … | @@ -0,0 +1,77 @@ |
| 1 | const binding = require('./binding') | |
| 2 | ||
| 3 | exports.Resolver = class DNSResolver { | |
| 4 | constructor() { | |
| 5 | this._handle = binding.initResolver() | |
| 6 | } | |
| 7 | ||
| 8 | resolveTxt(hostname, cb = noop) { | |
| 9 | binding.resolveTxt(this._handle, hostname, cb, this) | |
| 10 | } | |
| 11 | ||
| 12 | destroy() { | |
| 13 | binding.destroyResolver(this._handle) | |
| 14 | this._handle = null | |
| 15 | } | |
| 16 | ||
| 17 | static global = new this() | |
| 18 | } | |
| 19 | ||
| 20 | function onlookup(err, addresses) { | |
| 21 | const req = this | |
| 22 | ||
| 23 | if (err) return req.cb(err, null, 0) | |
| 24 | ||
| 25 | const { address, family } = addresses[0] | |
| 26 | ||
| 27 | return req.cb(null, address, family) | |
| 28 | } | |
| 29 | ||
| 30 | function onlookupall(err, addresses) { | |
| 31 | const req = this | |
| 32 | ||
| 33 | if (err) return req.cb(err, null) | |
| 34 | ||
| 35 | return req.cb(null, addresses) | |
| 36 | } | |
| 37 | ||
| 38 | exports.lookup = function lookup(hostname, opts = {}, cb) { | |
| 39 | if (typeof opts === 'function') { | |
| 40 | cb = opts | |
| 41 | opts = {} | |
| 42 | } | |
| 43 | ||
| 44 | let { family = 0, all = false } = opts | |
| 45 | ||
| 46 | if (typeof family === 'string') { | |
| 47 | switch (family) { | |
| 48 | case 'IPv4': | |
| 49 | family = 4 | |
| 50 | break | |
| 51 | case 'IPv6': | |
| 52 | family = 6 | |
| 53 | break | |
| 54 | default: | |
| 55 | family = 0 | |
| 56 | } | |
| 57 | } | |
| 58 | ||
| 59 | const req = { | |
| 60 | cb, | |
| 61 | handle: null | |
| 62 | } | |
| 63 | ||
| 64 | req.handle = binding.lookup( | |
| 65 | hostname, | |
| 66 | family || 0, | |
| 67 | all, | |
| 68 | req, | |
| 69 | all ? onlookupall : onlookup | |
| 70 | ) | |
| 71 | } | |
| 72 | ||
| 73 | exports.resolveTxt = function resolveTxt(hostname, cb) { | |
| 74 | exports.Resolver.global.resolveTxt(hostname, cb) | |
| 75 | } | |
| 76 | ||
| 77 | function noop() {} |
| … | … | @@ -0,0 +1,45 @@ |
| 1 | { | |
| 2 | "name": "bare-dns", | |
| 3 | "version": "2.1.4", | |
| 4 | "description": "Domain name resolution for JavaScript", | |
| 5 | "exports": { | |
| 6 | ".": { | |
| 7 | "types": "./index.d.ts", | |
| 8 | "default": "./index.js" | |
| 9 | }, | |
| 10 | "./package": "./package.json" | |
| 11 | }, | |
| 12 | "files": [ | |
| 13 | "index.js", | |
| 14 | "index.d.ts", | |
| 15 | "binding.c", | |
| 16 | "binding.js", | |
| 17 | "CMakeLists.txt", | |
| 18 | "lib", | |
| 19 | "prebuilds" | |
| 20 | ], | |
| 21 | "addon": true, | |
| 22 | "scripts": { | |
| 23 | "test": "prettier . --check && bare test.js" | |
| 24 | }, | |
| 25 | "repository": { | |
| 26 | "type": "git", | |
| 27 | "url": "git+https://github.com/holepunchto/bare-dns.git" | |
| 28 | }, | |
| 29 | "author": "Holepunch", | |
| 30 | "license": "Apache-2.0", | |
| 31 | "bugs": { | |
| 32 | "url": "https://github.com/holepunchto/bare-dns/issues" | |
| 33 | }, | |
| 34 | "homepage": "https://github.com/holepunchto/bare-dns", | |
| 35 | "engines": { | |
| 36 | "bare": ">=1.7.0" | |
| 37 | }, | |
| 38 | "devDependencies": { | |
| 39 | "brittle": "^3.3.2", | |
| 40 | "cmake-bare": "^1.1.6", | |
| 41 | "cmake-fetch": "^1.3.0", | |
| 42 | "prettier": "^3.4.1", | |
| 43 | "prettier-config-standard": "^7.0.0" | |
| 44 | } | |
| 45 | } |
| … | … | @@ -0,0 +1,201 @@ |
| 1 | Apache License | |
| 2 | Version 2.0, January 2004 | |
| 3 | http://www.apache.org/licenses/ | |
| 4 | ||
| 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | |
| 6 | ||
| 7 | 1. Definitions. | |
| 8 | ||
| 9 | "License" shall mean the terms and conditions for use, reproduction, | |
| 10 | and distribution as defined by Sections 1 through 9 of this document. | |
| 11 | ||
| 12 | "Licensor" shall mean the copyright owner or entity authorized by | |
| 13 | the copyright owner that is granting the License. | |
| 14 | ||
| 15 | "Legal Entity" shall mean the union of the acting entity and all | |
| 16 | other entities that control, are controlled by, or are under common | |
| 17 | control with that entity. For the purposes of this definition, | |
| 18 | "control" means (i) the power, direct or indirect, to cause the | |
| 19 | direction or management of such entity, whether by contract or | |
| 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the | |
| 21 | outstanding shares, or (iii) beneficial ownership of such entity. | |
| 22 | ||
| 23 | "You" (or "Your") shall mean an individual or Legal Entity | |
| 24 | exercising permissions granted by this License. | |
| 25 | ||
| 26 | "Source" form shall mean the preferred form for making modifications, | |
| 27 | including but not limited to software source code, documentation | |
| 28 | source, and configuration files. | |
| 29 | ||
| 30 | "Object" form shall mean any form resulting from mechanical | |
| 31 | transformation or translation of a Source form, including but | |
| 32 | not limited to compiled object code, generated documentation, | |
| 33 | and conversions to other media types. | |
| 34 | ||
| 35 | "Work" shall mean the work of authorship, whether in Source or | |
| 36 | Object form, made available under the License, as indicated by a | |
| 37 | copyright notice that is included in or attached to the work | |
| 38 | (an example is provided in the Appendix below). | |
| 39 | ||
| 40 | "Derivative Works" shall mean any work, whether in Source or Object | |
| 41 | form, that is based on (or derived from) the Work and for which the | |
| 42 | editorial revisions, annotations, elaborations, or other modifications | |
| 43 | represent, as a whole, an original work of authorship. For the purposes | |
| 44 | of this License, Derivative Works shall not include works that remain | |
| 45 | separable from, or merely link (or bind by name) to the interfaces of, | |
| 46 | the Work and Derivative Works thereof. | |
| 47 | ||
| 48 | "Contribution" shall mean any work of authorship, including | |
| 49 | the original version of the Work and any modifications or additions | |
| 50 | to that Work or Derivative Works thereof, that is intentionally | |
| 51 | submitted to Licensor for inclusion in the Work by the copyright owner | |
| 52 | or by an individual or Legal Entity authorized to submit on behalf of | |
| 53 | the copyright owner. For the purposes of this definition, "submitted" | |
| 54 | means any form of electronic, verbal, or written communication sent | |
| 55 | to the Licensor or its representatives, including but not limited to | |
| 56 | communication on electronic mailing lists, source code control systems, | |
| 57 | and issue tracking systems that are managed by, or on behalf of, the | |
| 58 | Licensor for the purpose of discussing and improving the Work, but | |
| 59 | excluding communication that is conspicuously marked or otherwise | |
| 60 | designated in writing by the copyright owner as "Not a Contribution." | |
| 61 | ||
| 62 | "Contributor" shall mean Licensor and any individual or Legal Entity | |
| 63 | on behalf of whom a Contribution has been received by Licensor and | |
| 64 | subsequently incorporated within the Work. | |
| 65 | ||
| 66 | 2. Grant of Copyright License. Subject to the terms and conditions of | |
| 67 | this License, each Contributor hereby grants to You a perpetual, | |
| 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 69 | copyright license to reproduce, prepare Derivative Works of, | |
| 70 | publicly display, publicly perform, sublicense, and distribute the | |
| 71 | Work and such Derivative Works in Source or Object form. | |
| 72 | ||
| 73 | 3. Grant of Patent License. Subject to the terms and conditions of | |
| 74 | this License, each Contributor hereby grants to You a perpetual, | |
| 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 76 | (except as stated in this section) patent license to make, have made, | |
| 77 | use, offer to sell, sell, import, and otherwise transfer the Work, | |
| 78 | where such license applies only to those patent claims licensable | |
| 79 | by such Contributor that are necessarily infringed by their | |
| 80 | Contribution(s) alone or by combination of their Contribution(s) | |
| 81 | with the Work to which such Contribution(s) was submitted. If You | |
| 82 | institute patent litigation against any entity (including a | |
| 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work | |
| 84 | or a Contribution incorporated within the Work constitutes direct | |
| 85 | or contributory patent infringement, then any patent licenses | |
| 86 | granted to You under this License for that Work shall terminate | |
| 87 | as of the date such litigation is filed. | |
| 88 | ||
| 89 | 4. Redistribution. You may reproduce and distribute copies of the | |
| 90 | Work or Derivative Works thereof in any medium, with or without | |
| 91 | modifications, and in Source or Object form, provided that You | |
| 92 | meet the following conditions: | |
| 93 | ||
| 94 | (a) You must give any other recipients of the Work or | |
| 95 | Derivative Works a copy of this License; and | |
| 96 | ||
| 97 | (b) You must cause any modified files to carry prominent notices | |
| 98 | stating that You changed the files; and | |
| 99 | ||
| 100 | (c) You must retain, in the Source form of any Derivative Works | |
| 101 | that You distribute, all copyright, patent, trademark, and | |
| 102 | attribution notices from the Source form of the Work, | |
| 103 | excluding those notices that do not pertain to any part of | |
| 104 | the Derivative Works; and | |
| 105 | ||
| 106 | (d) If the Work includes a "NOTICE" text file as part of its | |
| 107 | distribution, then any Derivative Works that You distribute must | |
| 108 | include a readable copy of the attribution notices contained | |
| 109 | within such NOTICE file, excluding those notices that do not | |
| 110 | pertain to any part of the Derivative Works, in at least one | |
| 111 | of the following places: within a NOTICE text file distributed | |
| 112 | as part of the Derivative Works; within the Source form or | |
| 113 | documentation, if provided along with the Derivative Works; or, | |
| 114 | within a display generated by the Derivative Works, if and | |
| 115 | wherever such third-party notices normally appear. The contents | |
| 116 | of the NOTICE file are for informational purposes only and | |
| 117 | do not modify the License. You may add Your own attribution | |
| 118 | notices within Derivative Works that You distribute, alongside | |
| 119 | or as an addendum to the NOTICE text from the Work, provided | |
| 120 | that such additional attribution notices cannot be construed | |
| 121 | as modifying the License. | |
| 122 | ||
| 123 | You may add Your own copyright statement to Your modifications and | |
| 124 | may provide additional or different license terms and conditions | |
| 125 | for use, reproduction, or distribution of Your modifications, or | |
| 126 | for any such Derivative Works as a whole, provided Your use, | |
| 127 | reproduction, and distribution of the Work otherwise complies with | |
| 128 | the conditions stated in this License. | |
| 129 | ||
| 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, | |
| 131 | any Contribution intentionally submitted for inclusion in the Work | |
| 132 | by You to the Licensor shall be under the terms and conditions of | |
| 133 | this License, without any additional terms or conditions. | |
| 134 | Notwithstanding the above, nothing herein shall supersede or modify | |
| 135 | the terms of any separate license agreement you may have executed | |
| 136 | with Licensor regarding such Contributions. | |
| 137 | ||
| 138 | 6. Trademarks. This License does not grant permission to use the trade | |
| 139 | names, trademarks, service marks, or product names of the Licensor, | |
| 140 | except as required for reasonable and customary use in describing the | |
| 141 | origin of the Work and reproducing the content of the NOTICE file. | |
| 142 | ||
| 143 | 7. Disclaimer of Warranty. Unless required by applicable law or | |
| 144 | agreed to in writing, Licensor provides the Work (and each | |
| 145 | Contributor provides its Contributions) on an "AS IS" BASIS, | |
| 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |
| 147 | implied, including, without limitation, any warranties or conditions | |
| 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | |
| 149 | PARTICULAR PURPOSE. You are solely responsible for determining the | |
| 150 | appropriateness of using or redistributing the Work and assume any | |
| 151 | risks associated with Your exercise of permissions under this License. | |
| 152 | ||
| 153 | 8. Limitation of Liability. In no event and under no legal theory, | |
| 154 | whether in tort (including negligence), contract, or otherwise, | |
| 155 | unless required by applicable law (such as deliberate and grossly | |
| 156 | negligent acts) or agreed to in writing, shall any Contributor be | |
| 157 | liable to You for damages, including any direct, indirect, special, | |
| 158 | incidental, or consequential damages of any character arising as a | |
| 159 | result of this License or out of the use or inability to use the | |
| 160 | Work (including but not limited to damages for loss of goodwill, | |
| 161 | work stoppage, computer failure or malfunction, or any and all | |
| 162 | other commercial damages or losses), even if such Contributor | |
| 163 | has been advised of the possibility of such damages. | |
| 164 | ||
| 165 | 9. Accepting Warranty or Additional Liability. While redistributing | |
| 166 | the Work or Derivative Works thereof, You may choose to offer, | |
| 167 | and charge a fee for, acceptance of support, warranty, indemnity, | |
| 168 | or other liability obligations and/or rights consistent with this | |
| 169 | License. However, in accepting such obligations, You may act only | |
| 170 | on Your own behalf and on Your sole responsibility, not on behalf | |
| 171 | of any other Contributor, and only if You agree to indemnify, | |
| 172 | defend, and hold each Contributor harmless for any liability | |
| 173 | incurred by, or claims asserted against, such Contributor by reason | |
| 174 | of your accepting any such warranty or additional liability. | |
| 175 | ||
| 176 | END OF TERMS AND CONDITIONS | |
| 177 | ||
| 178 | APPENDIX: How to apply the Apache License to your work. | |
| 179 | ||
| 180 | To apply the Apache License to your work, attach the following | |
| 181 | boilerplate notice, with the fields enclosed by brackets "[]" | |
| 182 | replaced with your own identifying information. (Don't include | |
| 183 | the brackets!) The text should be enclosed in the appropriate | |
| 184 | comment syntax for the file format. We also recommend that a | |
| 185 | file or class name and description of purpose be included on the | |
| 186 | same "printed page" as the copyright notice for easier | |
| 187 | identification within third-party archives. | |
| 188 | ||
| 189 | Copyright [yyyy] [name of copyright owner] | |
| 190 | ||
| 191 | Licensed under the Apache License, Version 2.0 (the "License"); | |
| 192 | you may not use this file except in compliance with the License. | |
| 193 | You may obtain a copy of the License at | |
| 194 | ||
| 195 | http://www.apache.org/licenses/LICENSE-2.0 | |
| 196 | ||
| 197 | Unless required by applicable law or agreed to in writing, software | |
| 198 | distributed under the License is distributed on an "AS IS" BASIS, | |
| 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 200 | See the License for the specific language governing permissions and | |
| 201 | limitations under the License. |
| … | … | @@ -0,0 +1,17 @@ |
| 1 | # bare-encoding | |
| 2 | ||
| 3 | WHATWG text encoding interfaces for JavaScript. | |
| 4 | ||
| 5 | ``` | |
| 6 | npm i bare-encoding | |
| 7 | ``` | |
| 8 | ||
| 9 | ## Usage | |
| 10 | ||
| 11 | ```js | |
| 12 | const { TextEncoder, TextDecoder } = require('bare-encoding') | |
| 13 | ``` | |
| 14 | ||
| 15 | ## License | |
| 16 | ||
| 17 | Apache-2.0 |
| … | … | @@ -0,0 +1,12 @@ |
| 1 | import * as encoding from '.' | |
| 2 | ||
| 3 | type TextEncoderConstructor = typeof encoding.TextEncoder | |
| 4 | type TextDecoderConstructor = typeof encoding.TextDecoder | |
| 5 | ||
| 6 | declare global { | |
| 7 | type TextEncoder = encoding.TextEncoder | |
| 8 | type TextDecoder = encoding.TextDecoder | |
| 9 | ||
| 10 | const TextEncoder: TextEncoderConstructor | |
| 11 | const TextDecoder: TextDecoderConstructor | |
| 12 | } |
| … | … | @@ -0,0 +1,4 @@ |
| 1 | const encoding = require('.') | |
| 2 | ||
| 3 | global.TextDecoder = encoding.TextDecoder | |
| 4 | global.TextEncoder = encoding.TextEncoder |
| … | … | @@ -0,0 +1,26 @@ |
| 1 | import Buffer from 'bare-buffer' | |
| 2 | ||
| 3 | export interface TextEncoder { | |
| 4 | readonly encoding: 'utf-8' | |
| 5 | ||
| 6 | encode(input: string): Buffer | |
| 7 | encodeInto( | |
| 8 | input: string, | |
| 9 | destination: ArrayBufferView | ArrayBuffer | SharedArrayBuffer | |
| 10 | ): { read: number; written: number } | |
| 11 | } | |
| 12 | ||
| 13 | export class TextEncoder {} | |
| 14 | ||
| 15 | export interface TextDecoder { | |
| 16 | readonly encoding: 'utf-8' | |
| 17 | ||
| 18 | decode( | |
| 19 | input: ArrayBufferView | ArrayBuffer | SharedArrayBuffer, | |
| 20 | options?: { stream: boolean } | |
| 21 | ): string | |
| 22 | } | |
| 23 | ||
| 24 | export class TextDecoder { | |
| 25 | constructor(label?: string) | |
| 26 | } |
| … | … | @@ -0,0 +1,74 @@ |
| 1 | const UTF8Decoder = require('./lib/utf8-decoder') | |
| 2 | const PassThroughDecoder = require('./lib/pass-through-decoder') | |
| 3 | const errors = require('./lib/errors') | |
| 4 | ||
| 5 | exports.TextEncoder = class TextEncoder { | |
| 6 | // https://encoding.spec.whatwg.org/#dom-textencoder-encoding | |
| 7 | get encoding() { | |
| 8 | return 'utf-8' | |
| 9 | } | |
| 10 | ||
| 11 | // https://encoding.spec.whatwg.org/#dom-textencoder-encode | |
| 12 | encode(input) { | |
| 13 | return Buffer.from(input) | |
| 14 | } | |
| 15 | ||
| 16 | // https://encoding.spec.whatwg.org/#dom-textencoder-encodeinto | |
| 17 | encodeInto(input, destination) { | |
| 18 | if (ArrayBuffer.isView(destination)) { | |
| 19 | destination = Buffer.from( | |
| 20 | destination.buffer, | |
| 21 | destination.byteOffset, | |
| 22 | destination.byteLength | |
| 23 | ) | |
| 24 | } else { | |
| 25 | destination = Buffer.from(destination) | |
| 26 | } | |
| 27 | ||
| 28 | return { | |
| 29 | read: input.length, | |
| 30 | written: destination.write(input) | |
| 31 | } | |
| 32 | } | |
| 33 | } | |
| 34 | ||
| 35 | exports.TextDecoder = class TextDecoder { | |
| 36 | // https://encoding.spec.whatwg.org/#dom-textdecoder | |
| 37 | constructor(label = 'utf-8') { | |
| 38 | this.encoding = getEncoding(label) | |
| 39 | ||
| 40 | switch (this.encoding) { | |
| 41 | case 'utf-8': | |
| 42 | this.decoder = new UTF8Decoder() | |
| 43 | break | |
| 44 | default: | |
| 45 | this.decoder = new PassThroughDecoder(this.encoding) | |
| 46 | break | |
| 47 | } | |
| 48 | } | |
| 49 | ||
| 50 | // https://encoding.spec.whatwg.org/#dom-textdecoder-decode | |
| 51 | decode(input, options = {}) { | |
| 52 | const result = this.decoder.decode(input) | |
| 53 | ||
| 54 | if (options.stream) return result | |
| 55 | ||
| 56 | return result + this.decoder.flush() | |
| 57 | } | |
| 58 | } | |
| 59 | ||
| 60 | // https://encoding.spec.whatwg.org/#concept-encoding-get | |
| 61 | function getEncoding(label) { | |
| 62 | switch (label.trim().toLowerCase()) { | |
| 63 | case 'unicode-1-1-utf-8': | |
| 64 | case 'unicode11utf8': | |
| 65 | case 'unicode11utf8': | |
| 66 | case 'unicode20utf8': | |
| 67 | case 'utf-8': | |
| 68 | case 'utf8': | |
| 69 | case 'x-unicode20utf8': | |
| 70 | return 'utf-8' | |
| 71 | default: | |
| 72 | throw errors.INVALID_LABEL(`The label '${label}' is not a valid encoding`) | |
| 73 | } | |
| 74 | } |
| … | … | @@ -0,0 +1,7 @@ |
| 1 | declare class EncodingError extends Error { | |
| 2 | readonly code: string | |
| 3 | ||
| 4 | static INVALID_LABEL(msg: string): EncodingError | |
| 5 | } | |
| 6 | ||
| 7 | export = EncodingError |
| … | … | @@ -0,0 +1,18 @@ |
| 1 | module.exports = class EncodingError extends Error { | |
| 2 | constructor(msg, code, fn = EncodingError) { | |
| 3 | super(`${code}: ${msg}`) | |
| 4 | this.code = code | |
| 5 | ||
| 6 | if (Error.captureStackTrace) { | |
| 7 | Error.captureStackTrace(this, fn) | |
| 8 | } | |
| 9 | } | |
| 10 | ||
| 11 | get name() { | |
| 12 | return 'EncodingError' | |
| 13 | } | |
| 14 | ||
| 15 | static INVALID_LABEL(msg) { | |
| 16 | return new EncodingError(msg, 'INVALID_LABEL', EncodingError.INVALID_LABEL) | |
| 17 | } | |
| 18 | } |
| … | … | @@ -0,0 +1,23 @@ |
| 1 | module.exports = class PassThroughDecoder { | |
| 2 | constructor(encoding) { | |
| 3 | this.encoding = encoding | |
| 4 | } | |
| 5 | ||
| 6 | get remaining() { | |
| 7 | return 0 | |
| 8 | } | |
| 9 | ||
| 10 | decode(data) { | |
| 11 | if (ArrayBuffer.isView(data)) { | |
| 12 | data = Buffer.from(data.buffer, data.byteOffset, data.byteLength) | |
| 13 | } else { | |
| 14 | data = Buffer.from(data) | |
| 15 | } | |
| 16 | ||
| 17 | return data.toString(this.encoding) | |
| 18 | } | |
| 19 | ||
| 20 | flush() { | |
| 21 | return '' | |
| 22 | } | |
| 23 | } |
| … | … | @@ -0,0 +1,112 @@ |
| 1 | // https://encoding.spec.whatwg.org/#utf-8-decoder | |
| 2 | module.exports = class UTF8Decoder { | |
| 3 | constructor() { | |
| 4 | this.codePoint = 0 | |
| 5 | this.bytesSeen = 0 | |
| 6 | this.bytesNeeded = 0 | |
| 7 | this.lowerBoundary = 0x80 | |
| 8 | this.upperBoundary = 0xbf | |
| 9 | } | |
| 10 | ||
| 11 | get remaining() { | |
| 12 | return this.bytesSeen | |
| 13 | } | |
| 14 | ||
| 15 | decode(data) { | |
| 16 | if (ArrayBuffer.isView(data)) { | |
| 17 | data = Buffer.from(data.buffer, data.byteOffset, data.byteLength) | |
| 18 | } else { | |
| 19 | data = Buffer.from(data) | |
| 20 | } | |
| 21 | ||
| 22 | if (this.bytesNeeded === 0) { | |
| 23 | let isBoundary = true | |
| 24 | ||
| 25 | for ( | |
| 26 | let i = Math.max(0, data.byteLength - 4), n = data.byteLength; | |
| 27 | i < n && isBoundary; | |
| 28 | i++ | |
| 29 | ) { | |
| 30 | isBoundary = data[i] <= 0x7f | |
| 31 | } | |
| 32 | ||
| 33 | if (isBoundary) return data.toString('utf8') | |
| 34 | } | |
| 35 | ||
| 36 | let result = '' | |
| 37 | ||
| 38 | for (let i = 0, n = data.byteLength; i < n; i++) { | |
| 39 | const byte = data[i] | |
| 40 | ||
| 41 | if (this.bytesNeeded === 0) { | |
| 42 | if (byte <= 0x7f) { | |
| 43 | this.bytesSeen = 0 | |
| 44 | ||
| 45 | result += String.fromCharCode(byte) | |
| 46 | } else { | |
| 47 | this.bytesSeen = 1 | |
| 48 | ||
| 49 | if (byte >= 0xc2 && byte <= 0xdf) { | |
| 50 | this.bytesNeeded = 2 | |
| 51 | this.codePoint = byte & 0x1f | |
| 52 | } else if (byte >= 0xe0 && byte <= 0xef) { | |
| 53 | if (byte === 0xe0) this.lowerBoundary = 0xa0 | |
| 54 | else if (byte === 0xed) this.upperBoundary = 0x9f | |
| 55 | this.bytesNeeded = 3 | |
| 56 | this.codePoint = byte & 0xf | |
| 57 | } else if (byte >= 0xf0 && byte <= 0xf4) { | |
| 58 | if (byte === 0xf0) this.lowerBoundary = 0x90 | |
| 59 | if (byte === 0xf4) this.upperBoundary = 0x8f | |
| 60 | this.bytesNeeded = 4 | |
| 61 | this.codePoint = byte & 0x7 | |
| 62 | } else { | |
| 63 | result += '\ufffd' | |
| 64 | } | |
| 65 | } | |
| 66 | ||
| 67 | continue | |
| 68 | } | |
| 69 | ||
| 70 | if (byte < this.lowerBoundary || byte > this.upperBoundary) { | |
| 71 | this.codePoint = 0 | |
| 72 | this.bytesNeeded = 0 | |
| 73 | this.bytesSeen = 0 | |
| 74 | this.lowerBoundary = 0x80 | |
| 75 | this.upperBoundary = 0xbf | |
| 76 | ||
| 77 | result += '\ufffd' | |
| 78 | i-- | |
| 79 | ||
| 80 | continue | |
| 81 | } | |
| 82 | ||
| 83 | this.lowerBoundary = 0x80 | |
| 84 | this.upperBoundary = 0xbf | |
| 85 | ||
| 86 | this.codePoint = (this.codePoint << 6) | (byte & 0x3f) | |
| 87 | this.bytesSeen++ | |
| 88 | ||
| 89 | if (this.bytesSeen !== this.bytesNeeded) continue | |
| 90 | ||
| 91 | result += String.fromCodePoint(this.codePoint) | |
| 92 | ||
| 93 | this.codePoint = 0 | |
| 94 | this.bytesNeeded = 0 | |
| 95 | this.bytesSeen = 0 | |
| 96 | } | |
| 97 | ||
| 98 | return result | |
| 99 | } | |
| 100 | ||
| 101 | flush() { | |
| 102 | const result = this.bytesNeeded > 0 ? '\ufffd' : '' | |
| 103 | ||
| 104 | this.codePoint = 0 | |
| 105 | this.bytesNeeded = 0 | |
| 106 | this.bytesSeen = 0 | |
| 107 | this.lowerBoundary = 0x80 | |
| 108 | this.upperBoundary = 0xbf | |
| 109 | ||
| 110 | return result | |
| 111 | } | |
| 112 | } |
| … | … | @@ -0,0 +1,53 @@ |
| 1 | { | |
| 2 | "name": "bare-encoding", | |
| 3 | "version": "1.0.3", | |
| 4 | "description": "WHATWG text encoding interfaces for JavaScript", | |
| 5 | "exports": { | |
| 6 | "./package": "./package.json", | |
| 7 | ".": { | |
| 8 | "types": "./index.d.ts", | |
| 9 | "default": "./index.js" | |
| 10 | }, | |
| 11 | "./global": { | |
| 12 | "types": "./global.d.ts", | |
| 13 | "default": "./global.js" | |
| 14 | }, | |
| 15 | "./errors": { | |
| 16 | "types": "./lib/errors.d.ts", | |
| 17 | "default": "./lib/errors.js" | |
| 18 | } | |
| 19 | }, | |
| 20 | "files": [ | |
| 21 | "index.js", | |
| 22 | "index.d.ts", | |
| 23 | "global.js", | |
| 24 | "global.d.ts", | |
| 25 | "lib" | |
| 26 | ], | |
| 27 | "scripts": { | |
| 28 | "test": "prettier . --check && bare test.js" | |
| 29 | }, | |
| 30 | "repository": { | |
| 31 | "type": "git", | |
| 32 | "url": "git+https://github.com/holepunchto/bare-encoding.git" | |
| 33 | }, | |
| 34 | "author": "Holepunch", | |
| 35 | "license": "Apache-2.0", | |
| 36 | "bugs": { | |
| 37 | "url": "https://github.com/holepunchto/bare-encoding/issues" | |
| 38 | }, | |
| 39 | "homepage": "https://github.com/holepunchto/bare-encoding#readme", | |
| 40 | "devDependencies": { | |
| 41 | "brittle": "^3.1.1", | |
| 42 | "prettier": "^3.3.3", | |
| 43 | "prettier-config-standard": "^7.0.0" | |
| 44 | }, | |
| 45 | "peerDependencies": { | |
| 46 | "bare-buffer": "*" | |
| 47 | }, | |
| 48 | "peerDependenciesMeta": { | |
| 49 | "bare-buffer": { | |
| 50 | "optional": true | |
| 51 | } | |
| 52 | } | |
| 53 | } |
| … | … | @@ -0,0 +1,201 @@ |
| 1 | Apache License | |
| 2 | Version 2.0, January 2004 | |
| 3 | http://www.apache.org/licenses/ | |
| 4 | ||
| 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | |
| 6 | ||
| 7 | 1. Definitions. | |
| 8 | ||
| 9 | "License" shall mean the terms and conditions for use, reproduction, | |
| 10 | and distribution as defined by Sections 1 through 9 of this document. | |
| 11 | ||
| 12 | "Licensor" shall mean the copyright owner or entity authorized by | |
| 13 | the copyright owner that is granting the License. | |
| 14 | ||
| 15 | "Legal Entity" shall mean the union of the acting entity and all | |
| 16 | other entities that control, are controlled by, or are under common | |
| 17 | control with that entity. For the purposes of this definition, | |
| 18 | "control" means (i) the power, direct or indirect, to cause the | |
| 19 | direction or management of such entity, whether by contract or | |
| 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the | |
| 21 | outstanding shares, or (iii) beneficial ownership of such entity. | |
| 22 | ||
| 23 | "You" (or "Your") shall mean an individual or Legal Entity | |
| 24 | exercising permissions granted by this License. | |
| 25 | ||
| 26 | "Source" form shall mean the preferred form for making modifications, | |
| 27 | including but not limited to software source code, documentation | |
| 28 | source, and configuration files. | |
| 29 | ||
| 30 | "Object" form shall mean any form resulting from mechanical | |
| 31 | transformation or translation of a Source form, including but | |
| 32 | not limited to compiled object code, generated documentation, | |
| 33 | and conversions to other media types. | |
| 34 | ||
| 35 | "Work" shall mean the work of authorship, whether in Source or | |
| 36 | Object form, made available under the License, as indicated by a | |
| 37 | copyright notice that is included in or attached to the work | |
| 38 | (an example is provided in the Appendix below). | |
| 39 | ||
| 40 | "Derivative Works" shall mean any work, whether in Source or Object | |
| 41 | form, that is based on (or derived from) the Work and for which the | |
| 42 | editorial revisions, annotations, elaborations, or other modifications | |
| 43 | represent, as a whole, an original work of authorship. For the purposes | |
| 44 | of this License, Derivative Works shall not include works that remain | |
| 45 | separable from, or merely link (or bind by name) to the interfaces of, | |
| 46 | the Work and Derivative Works thereof. | |
| 47 | ||
| 48 | "Contribution" shall mean any work of authorship, including | |
| 49 | the original version of the Work and any modifications or additions | |
| 50 | to that Work or Derivative Works thereof, that is intentionally | |
| 51 | submitted to Licensor for inclusion in the Work by the copyright owner | |
| 52 | or by an individual or Legal Entity authorized to submit on behalf of | |
| 53 | the copyright owner. For the purposes of this definition, "submitted" | |
| 54 | means any form of electronic, verbal, or written communication sent | |
| 55 | to the Licensor or its representatives, including but not limited to | |
| 56 | communication on electronic mailing lists, source code control systems, | |
| 57 | and issue tracking systems that are managed by, or on behalf of, the | |
| 58 | Licensor for the purpose of discussing and improving the Work, but | |
| 59 | excluding communication that is conspicuously marked or otherwise | |
| 60 | designated in writing by the copyright owner as "Not a Contribution." | |
| 61 | ||
| 62 | "Contributor" shall mean Licensor and any individual or Legal Entity | |
| 63 | on behalf of whom a Contribution has been received by Licensor and | |
| 64 | subsequently incorporated within the Work. | |
| 65 | ||
| 66 | 2. Grant of Copyright License. Subject to the terms and conditions of | |
| 67 | this License, each Contributor hereby grants to You a perpetual, | |
| 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 69 | copyright license to reproduce, prepare Derivative Works of, | |
| 70 | publicly display, publicly perform, sublicense, and distribute the | |
| 71 | Work and such Derivative Works in Source or Object form. | |
| 72 | ||
| 73 | 3. Grant of Patent License. Subject to the terms and conditions of | |
| 74 | this License, each Contributor hereby grants to You a perpetual, | |
| 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 76 | (except as stated in this section) patent license to make, have made, | |
| 77 | use, offer to sell, sell, import, and otherwise transfer the Work, | |
| 78 | where such license applies only to those patent claims licensable | |
| 79 | by such Contributor that are necessarily infringed by their | |
| 80 | Contribution(s) alone or by combination of their Contribution(s) | |
| 81 | with the Work to which such Contribution(s) was submitted. If You | |
| 82 | institute patent litigation against any entity (including a | |
| 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work | |
| 84 | or a Contribution incorporated within the Work constitutes direct | |
| 85 | or contributory patent infringement, then any patent licenses | |
| 86 | granted to You under this License for that Work shall terminate | |
| 87 | as of the date such litigation is filed. | |
| 88 | ||
| 89 | 4. Redistribution. You may reproduce and distribute copies of the | |
| 90 | Work or Derivative Works thereof in any medium, with or without | |
| 91 | modifications, and in Source or Object form, provided that You | |
| 92 | meet the following conditions: | |
| 93 | ||
| 94 | (a) You must give any other recipients of the Work or | |
| 95 | Derivative Works a copy of this License; and | |
| 96 | ||
| 97 | (b) You must cause any modified files to carry prominent notices | |
| 98 | stating that You changed the files; and | |
| 99 | ||
| 100 | (c) You must retain, in the Source form of any Derivative Works | |
| 101 | that You distribute, all copyright, patent, trademark, and | |
| 102 | attribution notices from the Source form of the Work, | |
| 103 | excluding those notices that do not pertain to any part of | |
| 104 | the Derivative Works; and | |
| 105 | ||
| 106 | (d) If the Work includes a "NOTICE" text file as part of its | |
| 107 | distribution, then any Derivative Works that You distribute must | |
| 108 | include a readable copy of the attribution notices contained | |
| 109 | within such NOTICE file, excluding those notices that do not | |
| 110 | pertain to any part of the Derivative Works, in at least one | |
| 111 | of the following places: within a NOTICE text file distributed | |
| 112 | as part of the Derivative Works; within the Source form or | |
| 113 | documentation, if provided along with the Derivative Works; or, | |
| 114 | within a display generated by the Derivative Works, if and | |
| 115 | wherever such third-party notices normally appear. The contents | |
| 116 | of the NOTICE file are for informational purposes only and | |
| 117 | do not modify the License. You may add Your own attribution | |
| 118 | notices within Derivative Works that You distribute, alongside | |
| 119 | or as an addendum to the NOTICE text from the Work, provided | |
| 120 | that such additional attribution notices cannot be construed | |
| 121 | as modifying the License. | |
| 122 | ||
| 123 | You may add Your own copyright statement to Your modifications and | |
| 124 | may provide additional or different license terms and conditions | |
| 125 | for use, reproduction, or distribution of Your modifications, or | |
| 126 | for any such Derivative Works as a whole, provided Your use, | |
| 127 | reproduction, and distribution of the Work otherwise complies with | |
| 128 | the conditions stated in this License. | |
| 129 | ||
| 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, | |
| 131 | any Contribution intentionally submitted for inclusion in the Work | |
| 132 | by You to the Licensor shall be under the terms and conditions of | |
| 133 | this License, without any additional terms or conditions. | |
| 134 | Notwithstanding the above, nothing herein shall supersede or modify | |
| 135 | the terms of any separate license agreement you may have executed | |
| 136 | with Licensor regarding such Contributions. | |
| 137 | ||
| 138 | 6. Trademarks. This License does not grant permission to use the trade | |
| 139 | names, trademarks, service marks, or product names of the Licensor, | |
| 140 | except as required for reasonable and customary use in describing the | |
| 141 | origin of the Work and reproducing the content of the NOTICE file. | |
| 142 | ||
| 143 | 7. Disclaimer of Warranty. Unless required by applicable law or | |
| 144 | agreed to in writing, Licensor provides the Work (and each | |
| 145 | Contributor provides its Contributions) on an "AS IS" BASIS, | |
| 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |
| 147 | implied, including, without limitation, any warranties or conditions | |
| 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | |
| 149 | PARTICULAR PURPOSE. You are solely responsible for determining the | |
| 150 | appropriateness of using or redistributing the Work and assume any | |
| 151 | risks associated with Your exercise of permissions under this License. | |
| 152 | ||
| 153 | 8. Limitation of Liability. In no event and under no legal theory, | |
| 154 | whether in tort (including negligence), contract, or otherwise, | |
| 155 | unless required by applicable law (such as deliberate and grossly | |
| 156 | negligent acts) or agreed to in writing, shall any Contributor be | |
| 157 | liable to You for damages, including any direct, indirect, special, | |
| 158 | incidental, or consequential damages of any character arising as a | |
| 159 | result of this License or out of the use or inability to use the | |
| 160 | Work (including but not limited to damages for loss of goodwill, | |
| 161 | work stoppage, computer failure or malfunction, or any and all | |
| 162 | other commercial damages or losses), even if such Contributor | |
| 163 | has been advised of the possibility of such damages. | |
| 164 | ||
| 165 | 9. Accepting Warranty or Additional Liability. While redistributing | |
| 166 | the Work or Derivative Works thereof, You may choose to offer, | |
| 167 | and charge a fee for, acceptance of support, warranty, indemnity, | |
| 168 | or other liability obligations and/or rights consistent with this | |
| 169 | License. However, in accepting such obligations, You may act only | |
| 170 | on Your own behalf and on Your sole responsibility, not on behalf | |
| 171 | of any other Contributor, and only if You agree to indemnify, | |
| 172 | defend, and hold each Contributor harmless for any liability | |
| 173 | incurred by, or claims asserted against, such Contributor by reason | |
| 174 | of your accepting any such warranty or additional liability. | |
| 175 | ||
| 176 | END OF TERMS AND CONDITIONS | |
| 177 | ||
| 178 | APPENDIX: How to apply the Apache License to your work. | |
| 179 | ||
| 180 | To apply the Apache License to your work, attach the following | |
| 181 | boilerplate notice, with the fields enclosed by brackets "[]" | |
| 182 | replaced with your own identifying information. (Don't include | |
| 183 | the brackets!) The text should be enclosed in the appropriate | |
| 184 | comment syntax for the file format. We also recommend that a | |
| 185 | file or class name and description of purpose be included on the | |
| 186 | same "printed page" as the copyright notice for easier | |
| 187 | identification within third-party archives. | |
| 188 | ||
| 189 | Copyright [yyyy] [name of copyright owner] | |
| 190 | ||
| 191 | Licensed under the Apache License, Version 2.0 (the "License"); | |
| 192 | you may not use this file except in compliance with the License. | |
| 193 | You may obtain a copy of the License at | |
| 194 | ||
| 195 | http://www.apache.org/licenses/LICENSE-2.0 | |
| 196 | ||
| 197 | Unless required by applicable law or agreed to in writing, software | |
| 198 | distributed under the License is distributed on an "AS IS" BASIS, | |
| 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 200 | See the License for the specific language governing permissions and | |
| 201 | limitations under the License. |
| … | … | @@ -0,0 +1,20 @@ |
| 1 | # bare-format | |
| 2 | ||
| 3 | String formatting for JavaScript. | |
| 4 | ||
| 5 | ``` | |
| 6 | npm i bare-format | |
| 7 | ``` | |
| 8 | ||
| 9 | ## Usage | |
| 10 | ||
| 11 | ```js | |
| 12 | const format = require('bare-format') | |
| 13 | ||
| 14 | format('Hello %s', 'world!') | |
| 15 | // Hello world! | |
| 16 | ``` | |
| 17 | ||
| 18 | ## License | |
| 19 | ||
| 20 | Apache-2.0 |
| … | … | @@ -0,0 +1,9 @@ |
| 1 | declare function format(...args: unknown[]): string | |
| 2 | ||
| 3 | declare namespace format { | |
| 4 | export function format(...args: unknown[]): string | |
| 5 | ||
| 6 | export function formatWithOptions(opts: {}, ...args: unknown[]): string | |
| 7 | } | |
| 8 | ||
| 9 | export = format |
| … | … | @@ -0,0 +1,154 @@ |
| 1 | const inspect = require('bare-inspect') | |
| 2 | ||
| 3 | module.exports = exports = function format(...args) { | |
| 4 | return exports.formatWithOptions({}, ...args) | |
| 5 | } | |
| 6 | ||
| 7 | exports.format = exports | |
| 8 | ||
| 9 | exports.formatWithOptions = function formatWithOptions(opts, ...args) { | |
| 10 | const format = args[0] | |
| 11 | ||
| 12 | let result = '' | |
| 13 | let join = '' | |
| 14 | let i = 0 | |
| 15 | ||
| 16 | if (typeof format === 'string') { | |
| 17 | if (args.length === 1) return format | |
| 18 | ||
| 19 | let tmp | |
| 20 | let j = 0 | |
| 21 | let c | |
| 22 | ||
| 23 | for (let k = 0, n = format.length; k < n; k++) { | |
| 24 | c = format.charCodeAt(k) | |
| 25 | ||
| 26 | if (c === 0x25 /* % */) { | |
| 27 | c = format.charCodeAt(++k) | |
| 28 | ||
| 29 | if (i < args.length) { | |
| 30 | switch (c) { | |
| 31 | case 0x73 /* s */: { | |
| 32 | const value = args[++i] | |
| 33 | ||
| 34 | switch (typeof value) { | |
| 35 | case 'string': | |
| 36 | tmp = value | |
| 37 | break | |
| 38 | case 'bigint': | |
| 39 | tmp = inspect(value, { ...opts, colors: false }) | |
| 40 | break | |
| 41 | default: | |
| 42 | tmp = String(value) | |
| 43 | break | |
| 44 | } | |
| 45 | ||
| 46 | break | |
| 47 | } | |
| 48 | ||
| 49 | case 0x64 /* d */: { | |
| 50 | const value = args[++i] | |
| 51 | ||
| 52 | switch (typeof value) { | |
| 53 | case 'symbol': | |
| 54 | tmp = NaN | |
| 55 | break | |
| 56 | case 'bigint': | |
| 57 | tmp = inspect(value, { ...opts, colors: false }) | |
| 58 | break | |
| 59 | default: | |
| 60 | tmp = inspect(Number(value), { ...opts, colors: false }) | |
| 61 | break | |
| 62 | } | |
| 63 | ||
| 64 | break | |
| 65 | } | |
| 66 | ||
| 67 | case 0x69 /* i */: { | |
| 68 | const value = args[++i] | |
| 69 | ||
| 70 | switch (typeof value) { | |
| 71 | case 'symbol': | |
| 72 | tmp = NaN | |
| 73 | break | |
| 74 | case 'bigint': | |
| 75 | tmp = inspect(value, { ...opts, colors: false }) | |
| 76 | break | |
| 77 | default: | |
| 78 | tmp = inspect(parseInt(value, 10), { ...opts, colors: false }) | |
| 79 | break | |
| 80 | } | |
| 81 | ||
| 82 | break | |
| 83 | } | |
| 84 | ||
| 85 | case 0x66 /* f */: { | |
| 86 | const value = args[++i] | |
| 87 | ||
| 88 | switch (typeof value) { | |
| 89 | case 'symbol': | |
| 90 | tmp = NaN | |
| 91 | break | |
| 92 | default: | |
| 93 | tmp = inspect(parseFloat(value), { ...opts, colors: false }) | |
| 94 | break | |
| 95 | } | |
| 96 | ||
| 97 | break | |
| 98 | } | |
| 99 | ||
| 100 | case 0x4f /* O */: | |
| 101 | case 0x6f /* o */: | |
| 102 | tmp = inspect(args[++i], opts) | |
| 103 | break | |
| 104 | ||
| 105 | case 0x6a /* j */: | |
| 106 | tmp = JSON.stringify(args[++i]) | |
| 107 | break | |
| 108 | ||
| 109 | case 0x63 /* c */: | |
| 110 | i++ | |
| 111 | tmp = '' | |
| 112 | break | |
| 113 | ||
| 114 | case 0x25 /* % */: | |
| 115 | result += format.substring(j, k) | |
| 116 | j = k + 1 | |
| 117 | continue | |
| 118 | ||
| 119 | default: | |
| 120 | continue | |
| 121 | } | |
| 122 | ||
| 123 | if (j !== k - 1) result += format.substring(j, k - 1) | |
| 124 | ||
| 125 | result += tmp | |
| 126 | j = k + 1 | |
| 127 | } else if (c === 0x25 /* % */) { | |
| 128 | result += format.substring(j, k) | |
| 129 | j = k + 1 | |
| 130 | } | |
| 131 | } | |
| 132 | } | |
| 133 | ||
| 134 | if (j !== 0) { | |
| 135 | i++ | |
| 136 | join = ' ' | |
| 137 | ||
| 138 | if (j < format.length) { | |
| 139 | result += format.substring(j) | |
| 140 | } | |
| 141 | } | |
| 142 | } | |
| 143 | ||
| 144 | while (i < args.length) { | |
| 145 | const value = args[i++] | |
| 146 | ||
| 147 | result += join | |
| 148 | result += typeof value === 'string' ? value : inspect(value, opts) | |
| 149 | ||
| 150 | join = ' ' | |
| 151 | } | |
| 152 | ||
| 153 | return result | |
| 154 | } |
| … | … | @@ -0,0 +1,37 @@ |
| 1 | { | |
| 2 | "name": "bare-format", | |
| 3 | "version": "1.0.2", | |
| 4 | "description": "String formatting for JavaScript", | |
| 5 | "exports": { | |
| 6 | ".": { | |
| 7 | "types": "./index.d.ts", | |
| 8 | "default": "./index.js" | |
| 9 | }, | |
| 10 | "./package": "./package.json" | |
| 11 | }, | |
| 12 | "files": [ | |
| 13 | "index.js", | |
| 14 | "index.d.ts" | |
| 15 | ], | |
| 16 | "scripts": { | |
| 17 | "test": "prettier . --check && bare test.js" | |
| 18 | }, | |
| 19 | "repository": { | |
| 20 | "type": "git", | |
| 21 | "url": "git+https://github.com/holepunchto/bare-format.git" | |
| 22 | }, | |
| 23 | "author": "Holepunch", | |
| 24 | "license": "Apache-2.0", | |
| 25 | "bugs": { | |
| 26 | "url": "https://github.com/holepunchto/bare-format/issues" | |
| 27 | }, | |
| 28 | "homepage": "https://github.com/holepunchto/bare-format#readme", | |
| 29 | "dependencies": { | |
| 30 | "bare-inspect": "^3.0.0" | |
| 31 | }, | |
| 32 | "devDependencies": { | |
| 33 | "brittle": "^3.5.0", | |
| 34 | "prettier": "^3.4.2", | |
| 35 | "prettier-config-holepunch": "^2.0.0" | |
| 36 | } | |
| 37 | } |
| … | … | @@ -0,0 +1,201 @@ |
| 1 | Apache License | |
| 2 | Version 2.0, January 2004 | |
| 3 | http://www.apache.org/licenses/ | |
| 4 | ||
| 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | |
| 6 | ||
| 7 | 1. Definitions. | |
| 8 | ||
| 9 | "License" shall mean the terms and conditions for use, reproduction, | |
| 10 | and distribution as defined by Sections 1 through 9 of this document. | |
| 11 | ||
| 12 | "Licensor" shall mean the copyright owner or entity authorized by | |
| 13 | the copyright owner that is granting the License. | |
| 14 | ||
| 15 | "Legal Entity" shall mean the union of the acting entity and all | |
| 16 | other entities that control, are controlled by, or are under common | |
| 17 | control with that entity. For the purposes of this definition, | |
| 18 | "control" means (i) the power, direct or indirect, to cause the | |
| 19 | direction or management of such entity, whether by contract or | |
| 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the | |
| 21 | outstanding shares, or (iii) beneficial ownership of such entity. | |
| 22 | ||
| 23 | "You" (or "Your") shall mean an individual or Legal Entity | |
| 24 | exercising permissions granted by this License. | |
| 25 | ||
| 26 | "Source" form shall mean the preferred form for making modifications, | |
| 27 | including but not limited to software source code, documentation | |
| 28 | source, and configuration files. | |
| 29 | ||
| 30 | "Object" form shall mean any form resulting from mechanical | |
| 31 | transformation or translation of a Source form, including but | |
| 32 | not limited to compiled object code, generated documentation, | |
| 33 | and conversions to other media types. | |
| 34 | ||
| 35 | "Work" shall mean the work of authorship, whether in Source or | |
| 36 | Object form, made available under the License, as indicated by a | |
| 37 | copyright notice that is included in or attached to the work | |
| 38 | (an example is provided in the Appendix below). | |
| 39 | ||
| 40 | "Derivative Works" shall mean any work, whether in Source or Object | |
| 41 | form, that is based on (or derived from) the Work and for which the | |
| 42 | editorial revisions, annotations, elaborations, or other modifications | |
| 43 | represent, as a whole, an original work of authorship. For the purposes | |
| 44 | of this License, Derivative Works shall not include works that remain | |
| 45 | separable from, or merely link (or bind by name) to the interfaces of, | |
| 46 | the Work and Derivative Works thereof. | |
| 47 | ||
| 48 | "Contribution" shall mean any work of authorship, including | |
| 49 | the original version of the Work and any modifications or additions | |
| 50 | to that Work or Derivative Works thereof, that is intentionally | |
| 51 | submitted to Licensor for inclusion in the Work by the copyright owner | |
| 52 | or by an individual or Legal Entity authorized to submit on behalf of | |
| 53 | the copyright owner. For the purposes of this definition, "submitted" | |
| 54 | means any form of electronic, verbal, or written communication sent | |
| 55 | to the Licensor or its representatives, including but not limited to | |
| 56 | communication on electronic mailing lists, source code control systems, | |
| 57 | and issue tracking systems that are managed by, or on behalf of, the | |
| 58 | Licensor for the purpose of discussing and improving the Work, but | |
| 59 | excluding communication that is conspicuously marked or otherwise | |
| 60 | designated in writing by the copyright owner as "Not a Contribution." | |
| 61 | ||
| 62 | "Contributor" shall mean Licensor and any individual or Legal Entity | |
| 63 | on behalf of whom a Contribution has been received by Licensor and | |
| 64 | subsequently incorporated within the Work. | |
| 65 | ||
| 66 | 2. Grant of Copyright License. Subject to the terms and conditions of | |
| 67 | this License, each Contributor hereby grants to You a perpetual, | |
| 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 69 | copyright license to reproduce, prepare Derivative Works of, | |
| 70 | publicly display, publicly perform, sublicense, and distribute the | |
| 71 | Work and such Derivative Works in Source or Object form. | |
| 72 | ||
| 73 | 3. Grant of Patent License. Subject to the terms and conditions of | |
| 74 | this License, each Contributor hereby grants to You a perpetual, | |
| 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 76 | (except as stated in this section) patent license to make, have made, | |
| 77 | use, offer to sell, sell, import, and otherwise transfer the Work, | |
| 78 | where such license applies only to those patent claims licensable | |
| 79 | by such Contributor that are necessarily infringed by their | |
| 80 | Contribution(s) alone or by combination of their Contribution(s) | |
| 81 | with the Work to which such Contribution(s) was submitted. If You | |
| 82 | institute patent litigation against any entity (including a | |
| 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work | |
| 84 | or a Contribution incorporated within the Work constitutes direct | |
| 85 | or contributory patent infringement, then any patent licenses | |
| 86 | granted to You under this License for that Work shall terminate | |
| 87 | as of the date such litigation is filed. | |
| 88 | ||
| 89 | 4. Redistribution. You may reproduce and distribute copies of the | |
| 90 | Work or Derivative Works thereof in any medium, with or without | |
| 91 | modifications, and in Source or Object form, provided that You | |
| 92 | meet the following conditions: | |
| 93 | ||
| 94 | (a) You must give any other recipients of the Work or | |
| 95 | Derivative Works a copy of this License; and | |
| 96 | ||
| 97 | (b) You must cause any modified files to carry prominent notices | |
| 98 | stating that You changed the files; and | |
| 99 | ||
| 100 | (c) You must retain, in the Source form of any Derivative Works | |
| 101 | that You distribute, all copyright, patent, trademark, and | |
| 102 | attribution notices from the Source form of the Work, | |
| 103 | excluding those notices that do not pertain to any part of | |
| 104 | the Derivative Works; and | |
| 105 | ||
| 106 | (d) If the Work includes a "NOTICE" text file as part of its | |
| 107 | distribution, then any Derivative Works that You distribute must | |
| 108 | include a readable copy of the attribution notices contained | |
| 109 | within such NOTICE file, excluding those notices that do not | |
| 110 | pertain to any part of the Derivative Works, in at least one | |
| 111 | of the following places: within a NOTICE text file distributed | |
| 112 | as part of the Derivative Works; within the Source form or | |
| 113 | documentation, if provided along with the Derivative Works; or, | |
| 114 | within a display generated by the Derivative Works, if and | |
| 115 | wherever such third-party notices normally appear. The contents | |
| 116 | of the NOTICE file are for informational purposes only and | |
| 117 | do not modify the License. You may add Your own attribution | |
| 118 | notices within Derivative Works that You distribute, alongside | |
| 119 | or as an addendum to the NOTICE text from the Work, provided | |
| 120 | that such additional attribution notices cannot be construed | |
| 121 | as modifying the License. | |
| 122 | ||
| 123 | You may add Your own copyright statement to Your modifications and | |
| 124 | may provide additional or different license terms and conditions | |
| 125 | for use, reproduction, or distribution of Your modifications, or | |
| 126 | for any such Derivative Works as a whole, provided Your use, | |
| 127 | reproduction, and distribution of the Work otherwise complies with | |
| 128 | the conditions stated in this License. | |
| 129 | ||
| 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, | |
| 131 | any Contribution intentionally submitted for inclusion in the Work | |
| 132 | by You to the Licensor shall be under the terms and conditions of | |
| 133 | this License, without any additional terms or conditions. | |
| 134 | Notwithstanding the above, nothing herein shall supersede or modify | |
| 135 | the terms of any separate license agreement you may have executed | |
| 136 | with Licensor regarding such Contributions. | |
| 137 | ||
| 138 | 6. Trademarks. This License does not grant permission to use the trade | |
| 139 | names, trademarks, service marks, or product names of the Licensor, | |
| 140 | except as required for reasonable and customary use in describing the | |
| 141 | origin of the Work and reproducing the content of the NOTICE file. | |
| 142 | ||
| 143 | 7. Disclaimer of Warranty. Unless required by applicable law or | |
| 144 | agreed to in writing, Licensor provides the Work (and each | |
| 145 | Contributor provides its Contributions) on an "AS IS" BASIS, | |
| 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |
| 147 | implied, including, without limitation, any warranties or conditions | |
| 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | |
| 149 | PARTICULAR PURPOSE. You are solely responsible for determining the | |
| 150 | appropriateness of using or redistributing the Work and assume any | |
| 151 | risks associated with Your exercise of permissions under this License. | |
| 152 | ||
| 153 | 8. Limitation of Liability. In no event and under no legal theory, | |
| 154 | whether in tort (including negligence), contract, or otherwise, | |
| 155 | unless required by applicable law (such as deliberate and grossly | |
| 156 | negligent acts) or agreed to in writing, shall any Contributor be | |
| 157 | liable to You for damages, including any direct, indirect, special, | |
| 158 | incidental, or consequential damages of any character arising as a | |
| 159 | result of this License or out of the use or inability to use the | |
| 160 | Work (including but not limited to damages for loss of goodwill, | |
| 161 | work stoppage, computer failure or malfunction, or any and all | |
| 162 | other commercial damages or losses), even if such Contributor | |
| 163 | has been advised of the possibility of such damages. | |
| 164 | ||
| 165 | 9. Accepting Warranty or Additional Liability. While redistributing | |
| 166 | the Work or Derivative Works thereof, You may choose to offer, | |
| 167 | and charge a fee for, acceptance of support, warranty, indemnity, | |
| 168 | or other liability obligations and/or rights consistent with this | |
| 169 | License. However, in accepting such obligations, You may act only | |
| 170 | on Your own behalf and on Your sole responsibility, not on behalf | |
| 171 | of any other Contributor, and only if You agree to indemnify, | |
| 172 | defend, and hold each Contributor harmless for any liability | |
| 173 | incurred by, or claims asserted against, such Contributor by reason | |
| 174 | of your accepting any such warranty or additional liability. | |
| 175 | ||
| 176 | END OF TERMS AND CONDITIONS | |
| 177 | ||
| 178 | APPENDIX: How to apply the Apache License to your work. | |
| 179 | ||
| 180 | To apply the Apache License to your work, attach the following | |
| 181 | boilerplate notice, with the fields enclosed by brackets "[]" | |
| 182 | replaced with your own identifying information. (Don't include | |
| 183 | the brackets!) The text should be enclosed in the appropriate | |
| 184 | comment syntax for the file format. We also recommend that a | |
| 185 | file or class name and description of purpose be included on the | |
| 186 | same "printed page" as the copyright notice for easier | |
| 187 | identification within third-party archives. | |
| 188 | ||
| 189 | Copyright [yyyy] [name of copyright owner] | |
| 190 | ||
| 191 | Licensed under the Apache License, Version 2.0 (the "License"); | |
| 192 | you may not use this file except in compliance with the License. | |
| 193 | You may obtain a copy of the License at | |
| 194 | ||
| 195 | http://www.apache.org/licenses/LICENSE-2.0 | |
| 196 | ||
| 197 | Unless required by applicable law or agreed to in writing, software | |
| 198 | distributed under the License is distributed on an "AS IS" BASIS, | |
| 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 200 | See the License for the specific language governing permissions and | |
| 201 | limitations under the License. |
| … | … | @@ -0,0 +1,11 @@ |
| 1 | # bare-http-parser | |
| 2 | ||
| 3 | Streaming HTTP request and response parser for Bare. | |
| 4 | ||
| 5 | ``` | |
| 6 | npm i bare-http-parser | |
| 7 | ``` | |
| 8 | ||
| 9 | ## License | |
| 10 | ||
| 11 | Apache-2.0 |
| … | … | @@ -0,0 +1,694 @@ |
| 1 | const errors = require('./lib/errors') | |
| 2 | ||
| 3 | const constants = { | |
| 4 | REQUEST: 1, | |
| 5 | RESPONSE: 2, | |
| 6 | DATA: 3, | |
| 7 | END: 4 | |
| 8 | } | |
| 9 | ||
| 10 | const TAB = 0x09 | |
| 11 | const LF = 0x0a | |
| 12 | const CR = 0x0d | |
| 13 | const SP = 0x20 | |
| 14 | const ZERO = 0x30 | |
| 15 | const NINE = 0x39 | |
| 16 | const UPPER_A = 0x41 | |
| 17 | const UPPER_F = 0x46 | |
| 18 | const UPPER_Z = 0x5a | |
| 19 | const LOWER_A = 0x61 | |
| 20 | const LOWER_F = 0x66 | |
| 21 | const COLON = 0x3a | |
| 22 | ||
| 23 | const MAX_CHUNK_SIZE_LENGTH = 16 | |
| 24 | ||
| 25 | // Header states | |
| 26 | const FIRST_TOKEN = 0 | |
| 27 | const REQUEST_URL = 1 | |
| 28 | const REQUEST_VERSION = 2 | |
| 29 | const STATUS_CODE = 3 | |
| 30 | const STATUS_REASON = 4 | |
| 31 | const FIRST_LINE_LF = 5 | |
| 32 | const HEADER_START = 6 | |
| 33 | const HEADER_NAME = 7 | |
| 34 | const HEADER_VALUE_WS = 8 | |
| 35 | const HEADER_VALUE = 9 | |
| 36 | const HEADER_LINE_LF = 10 | |
| 37 | const HEADER_END_LF = 11 | |
| 38 | ||
| 39 | // Body states | |
| 40 | const BODY = 12 | |
| 41 | const CHUNK_SIZE = 13 | |
| 42 | const CHUNK_SIZE_LF = 14 | |
| 43 | const CHUNK_DATA = 15 | |
| 44 | const CHUNK_EXTENSION = 16 | |
| 45 | ||
| 46 | // Trailing states | |
| 47 | const LAST_CHUNK_LF = 17 | |
| 48 | const TRAILER_CR = 18 | |
| 49 | const TRAILER_LF = 19 | |
| 50 | ||
| 51 | module.exports = exports = class HTTPParser { | |
| 52 | constructor(opts = {}) { | |
| 53 | const { maxHeaderSize = 16384, maxHeadersCount = 2000 } = opts | |
| 54 | ||
| 55 | this._maxHeaderSize = maxHeaderSize | |
| 56 | this._maxHeadersCount = maxHeadersCount | |
| 57 | this._state = FIRST_TOKEN | |
| 58 | this._buffer = [] | |
| 59 | this._bufferIndex = 0 | |
| 60 | this._byteIndex = 0 | |
| 61 | this._buffered = 0 | |
| 62 | this._accumulator = [] | |
| 63 | ||
| 64 | this._isResponse = false | |
| 65 | this._method = '' | |
| 66 | this._url = '' | |
| 67 | this._version = '' | |
| 68 | this._code = 0 | |
| 69 | this._reason = '' | |
| 70 | ||
| 71 | this._headerName = '' | |
| 72 | this._headers = {} | |
| 73 | this._headerCount = 0 | |
| 74 | this._headerSize = 0 | |
| 75 | ||
| 76 | this._remaining = 0 | |
| 77 | } | |
| 78 | ||
| 79 | *push(data, encoding) { | |
| 80 | if (typeof data === 'string') data = Buffer.from(data, encoding) | |
| 81 | ||
| 82 | this._buffer.push(data) | |
| 83 | this._buffered += data.byteLength | |
| 84 | ||
| 85 | yield* this._parse() | |
| 86 | ||
| 87 | this._compact() | |
| 88 | } | |
| 89 | ||
| 90 | end() { | |
| 91 | const buffers = this._buffer | |
| 92 | const bufferIndex = this._bufferIndex | |
| 93 | const byteIndex = this._byteIndex | |
| 94 | ||
| 95 | this._buffer = [] | |
| 96 | this._bufferIndex = 0 | |
| 97 | this._byteIndex = 0 | |
| 98 | this._buffered = 0 | |
| 99 | ||
| 100 | if (bufferIndex >= buffers.length) return Buffer.alloc(0) | |
| 101 | ||
| 102 | buffers[bufferIndex] = buffers[bufferIndex].subarray(byteIndex) | |
| 103 | ||
| 104 | const remaining = buffers.slice(bufferIndex) | |
| 105 | ||
| 106 | if (remaining.length === 0) return Buffer.alloc(0) | |
| 107 | if (remaining.length === 1) return remaining[0] | |
| 108 | ||
| 109 | return Buffer.concat(remaining) | |
| 110 | } | |
| 111 | ||
| 112 | _compact() { | |
| 113 | if (this._bufferIndex > 0) { | |
| 114 | this._buffer = this._buffer.slice(this._bufferIndex) | |
| 115 | this._bufferIndex = 0 | |
| 116 | } | |
| 117 | ||
| 118 | if (this._byteIndex > 0 && this._buffer.length > 0) { | |
| 119 | this._buffer[0] = this._buffer[0].subarray(this._byteIndex) | |
| 120 | this._byteIndex = 0 | |
| 121 | } | |
| 122 | } | |
| 123 | ||
| 124 | _consume(n) { | |
| 125 | this._buffered -= n | |
| 126 | ||
| 127 | const current = this._buffer[this._bufferIndex] | |
| 128 | ||
| 129 | if (this._byteIndex + n <= current.byteLength) { | |
| 130 | const slice = current.subarray(this._byteIndex, this._byteIndex + n) | |
| 131 | ||
| 132 | this._byteIndex += n | |
| 133 | ||
| 134 | if (this._byteIndex >= current.byteLength) { | |
| 135 | this._bufferIndex++ | |
| 136 | this._byteIndex = 0 | |
| 137 | } | |
| 138 | ||
| 139 | return slice | |
| 140 | } | |
| 141 | ||
| 142 | const result = Buffer.allocUnsafe(n) | |
| 143 | ||
| 144 | let written = 0 | |
| 145 | ||
| 146 | while (written < n) { | |
| 147 | const buffer = this._buffer[this._bufferIndex] | |
| 148 | const available = buffer.byteLength - this._byteIndex | |
| 149 | const take = Math.min(available, n - written) | |
| 150 | ||
| 151 | buffer.copy(result, written, this._byteIndex, this._byteIndex + take) | |
| 152 | ||
| 153 | written += take | |
| 154 | ||
| 155 | this._byteIndex += take | |
| 156 | ||
| 157 | if (this._byteIndex >= buffer.byteLength) { | |
| 158 | this._bufferIndex++ | |
| 159 | this._byteIndex = 0 | |
| 160 | } | |
| 161 | } | |
| 162 | ||
| 163 | return result | |
| 164 | } | |
| 165 | ||
| 166 | _buildString() { | |
| 167 | const string = String.fromCharCode.apply(null, this._accumulator) | |
| 168 | ||
| 169 | this._accumulator = [] | |
| 170 | ||
| 171 | return string | |
| 172 | } | |
| 173 | ||
| 174 | _checkHeaderSize() { | |
| 175 | if (++this._headerSize > this._maxHeaderSize) { | |
| 176 | throw errors.INVALID_MESSAGE('Header exceeds limit of ' + this._maxHeaderSize + ' bytes') | |
| 177 | } | |
| 178 | } | |
| 179 | ||
| 180 | _storeHeader(name, value) { | |
| 181 | let end = value.length | |
| 182 | ||
| 183 | while (end > 0 && (value.charCodeAt(end - 1) === SP || value.charCodeAt(end - 1) === TAB)) { | |
| 184 | end-- | |
| 185 | } | |
| 186 | ||
| 187 | if (end < value.length) value = value.substring(0, end) | |
| 188 | ||
| 189 | this._headerCount++ | |
| 190 | ||
| 191 | if (this._headerCount > this._maxHeadersCount) { | |
| 192 | throw errors.INVALID_MESSAGE('Header count exceeds limit of ' + this._maxHeadersCount) | |
| 193 | } | |
| 194 | ||
| 195 | switch (name) { | |
| 196 | case '__proto__': | |
| 197 | case 'constructor': | |
| 198 | case 'prototype': | |
| 199 | throw errors.INVALID_HEADER("Unsafe header name '" + name + "'") | |
| 200 | ||
| 201 | case 'host': | |
| 202 | case 'content-length': | |
| 203 | case 'transfer-encoding': | |
| 204 | if (name in this._headers) { | |
| 205 | throw errors.INVALID_HEADER("Duplicate header '" + name + "'") | |
| 206 | } | |
| 207 | ||
| 208 | this._headers[name] = value | |
| 209 | break | |
| 210 | ||
| 211 | default: | |
| 212 | const delimiter = name === 'cookie' ? '; ' : ', ' | |
| 213 | ||
| 214 | if (name in this._headers) { | |
| 215 | this._headers[name] += delimiter + value | |
| 216 | } else { | |
| 217 | this._headers[name] = value | |
| 218 | } | |
| 219 | } | |
| 220 | } | |
| 221 | ||
| 222 | *_parse() { | |
| 223 | while (true) { | |
| 224 | if (this._state === BODY) { | |
| 225 | if (this._buffered === 0) return | |
| 226 | ||
| 227 | const available = Math.min(this._buffered, this._remaining) | |
| 228 | const data = this._consume(available) | |
| 229 | ||
| 230 | this._remaining -= available | |
| 231 | ||
| 232 | const ended = this._remaining === 0 | |
| 233 | ||
| 234 | if (ended) this._state = FIRST_TOKEN | |
| 235 | ||
| 236 | yield { type: constants.DATA, data } | |
| 237 | ||
| 238 | if (ended) yield { type: constants.END } | |
| 239 | ||
| 240 | continue | |
| 241 | } | |
| 242 | ||
| 243 | if (this._state === CHUNK_DATA) { | |
| 244 | if (this._buffered < this._remaining) return | |
| 245 | ||
| 246 | const consumed = this._consume(this._remaining) | |
| 247 | ||
| 248 | if (consumed[this._remaining - 2] !== CR || consumed[this._remaining - 1] !== LF) { | |
| 249 | throw errors.INVALID_MESSAGE('Expected CRLF after chunk data') | |
| 250 | } | |
| 251 | ||
| 252 | const data = consumed.subarray(0, this._remaining - 2) | |
| 253 | ||
| 254 | this._remaining = 0 | |
| 255 | this._state = CHUNK_SIZE | |
| 256 | ||
| 257 | yield { type: constants.DATA, data } | |
| 258 | ||
| 259 | continue | |
| 260 | } | |
| 261 | ||
| 262 | if (this._buffered === 0) return | |
| 263 | ||
| 264 | const byte = this._buffer[this._bufferIndex][this._byteIndex++] | |
| 265 | ||
| 266 | this._buffered-- | |
| 267 | ||
| 268 | if (this._byteIndex >= this._buffer[this._bufferIndex].byteLength) { | |
| 269 | this._bufferIndex++ | |
| 270 | this._byteIndex = 0 | |
| 271 | } | |
| 272 | ||
| 273 | switch (this._state) { | |
| 274 | case FIRST_TOKEN: { | |
| 275 | this._checkHeaderSize() | |
| 276 | ||
| 277 | if (byte === SP) { | |
| 278 | const token = this._buildString() | |
| 279 | ||
| 280 | if (token.length === 0) throw errors.INVALID_MESSAGE() | |
| 281 | ||
| 282 | this._isResponse = token.startsWith('HTTP/') | |
| 283 | ||
| 284 | if (this._isResponse) { | |
| 285 | if (token !== 'HTTP/1.0' && token !== 'HTTP/1.1') { | |
| 286 | throw errors.INVALID_MESSAGE() | |
| 287 | } | |
| 288 | ||
| 289 | this._version = token | |
| 290 | this._state = STATUS_CODE | |
| 291 | } else { | |
| 292 | this._method = token | |
| 293 | this._state = REQUEST_URL | |
| 294 | } | |
| 295 | } else if (byte === CR) { | |
| 296 | throw errors.INVALID_MESSAGE() | |
| 297 | } else if (isTokenByte(byte)) { | |
| 298 | this._accumulator.push(byte) | |
| 299 | } else if ( | |
| 300 | byte === 0x2f && | |
| 301 | this._accumulator.length === 4 && | |
| 302 | this._accumulator[0] === 0x48 && | |
| 303 | this._accumulator[1] === 0x54 && | |
| 304 | this._accumulator[2] === 0x54 && | |
| 305 | this._accumulator[3] === 0x50 | |
| 306 | ) { | |
| 307 | this._accumulator.push(byte) | |
| 308 | } else { | |
| 309 | throw errors.INVALID_MESSAGE() | |
| 310 | } | |
| 311 | ||
| 312 | break | |
| 313 | } | |
| 314 | ||
| 315 | case REQUEST_URL: { | |
| 316 | this._checkHeaderSize() | |
| 317 | ||
| 318 | if (byte === SP) { | |
| 319 | this._url = this._buildString() | |
| 320 | ||
| 321 | if (this._url.length === 0) throw errors.INVALID_MESSAGE() | |
| 322 | ||
| 323 | this._state = REQUEST_VERSION | |
| 324 | } else if (byte === CR) { | |
| 325 | throw errors.INVALID_MESSAGE() | |
| 326 | } else if (byte >= 0x21 && byte !== 0x7f) { | |
| 327 | this._accumulator.push(byte) | |
| 328 | } else { | |
| 329 | throw errors.INVALID_MESSAGE() | |
| 330 | } | |
| 331 | ||
| 332 | break | |
| 333 | } | |
| 334 | ||
| 335 | case REQUEST_VERSION: { | |
| 336 | this._checkHeaderSize() | |
| 337 | ||
| 338 | if (byte === CR) { | |
| 339 | this._version = this._buildString() | |
| 340 | ||
| 341 | if (this._version !== 'HTTP/1.0' && this._version !== 'HTTP/1.1') { | |
| 342 | throw errors.INVALID_MESSAGE() | |
| 343 | } | |
| 344 | ||
| 345 | this._state = FIRST_LINE_LF | |
| 346 | } else if (byte >= 0x21 && byte !== 0x7f) { | |
| 347 | this._accumulator.push(byte) | |
| 348 | } else { | |
| 349 | throw errors.INVALID_MESSAGE() | |
| 350 | } | |
| 351 | ||
| 352 | break | |
| 353 | } | |
| 354 | ||
| 355 | case STATUS_CODE: { | |
| 356 | this._checkHeaderSize() | |
| 357 | ||
| 358 | if (byte === SP) { | |
| 359 | if (this._accumulator.length === 0) throw errors.INVALID_MESSAGE() | |
| 360 | ||
| 361 | let code = 0 | |
| 362 | ||
| 363 | for (let i = 0, n = this._accumulator.length; i < n; i++) { | |
| 364 | code = code * 10 + this._accumulator[i] | |
| 365 | } | |
| 366 | ||
| 367 | this._accumulator = [] | |
| 368 | ||
| 369 | if (code < 100 || code > 999) throw errors.INVALID_MESSAGE() | |
| 370 | ||
| 371 | this._code = code | |
| 372 | this._state = STATUS_REASON | |
| 373 | } else if (byte >= ZERO && byte <= NINE) { | |
| 374 | this._accumulator.push(byte - ZERO) | |
| 375 | } else { | |
| 376 | throw errors.INVALID_MESSAGE() | |
| 377 | } | |
| 378 | ||
| 379 | break | |
| 380 | } | |
| 381 | ||
| 382 | case STATUS_REASON: { | |
| 383 | this._checkHeaderSize() | |
| 384 | ||
| 385 | if (byte === CR) { | |
| 386 | this._reason = this._buildString() | |
| 387 | this._state = FIRST_LINE_LF | |
| 388 | } else if (isFieldByte(byte)) { | |
| 389 | this._accumulator.push(byte) | |
| 390 | } else { | |
| 391 | throw errors.INVALID_MESSAGE() | |
| 392 | } | |
| 393 | ||
| 394 | break | |
| 395 | } | |
| 396 | ||
| 397 | case FIRST_LINE_LF: { | |
| 398 | if (byte !== LF) throw errors.INVALID_MESSAGE() | |
| 399 | ||
| 400 | this._headers = {} | |
| 401 | this._headerCount = 0 | |
| 402 | this._state = HEADER_START | |
| 403 | ||
| 404 | break | |
| 405 | } | |
| 406 | ||
| 407 | case HEADER_START: { | |
| 408 | this._checkHeaderSize() | |
| 409 | ||
| 410 | if (byte === CR) { | |
| 411 | this._state = HEADER_END_LF | |
| 412 | } else if (byte !== COLON && isTokenByte(byte)) { | |
| 413 | this._accumulator.push(byte >= UPPER_A && byte <= UPPER_Z ? byte + 0x20 : byte) | |
| 414 | this._state = HEADER_NAME | |
| 415 | } else { | |
| 416 | throw errors.INVALID_HEADER() | |
| 417 | } | |
| 418 | ||
| 419 | break | |
| 420 | } | |
| 421 | ||
| 422 | case HEADER_NAME: { | |
| 423 | this._checkHeaderSize() | |
| 424 | ||
| 425 | if (byte === COLON) { | |
| 426 | this._headerName = this._buildString() | |
| 427 | this._state = HEADER_VALUE_WS | |
| 428 | } else if (byte !== COLON && isTokenByte(byte)) { | |
| 429 | this._accumulator.push(byte >= UPPER_A && byte <= UPPER_Z ? byte + 0x20 : byte) | |
| 430 | } else { | |
| 431 | throw errors.INVALID_HEADER() | |
| 432 | } | |
| 433 | ||
| 434 | break | |
| 435 | } | |
| 436 | ||
| 437 | case HEADER_VALUE_WS: { | |
| 438 | this._checkHeaderSize() | |
| 439 | ||
| 440 | if (byte === SP || byte === TAB) break | |
| 441 | ||
| 442 | if (byte === CR) { | |
| 443 | this._storeHeader(this._headerName, '') | |
| 444 | this._headerName = '' | |
| 445 | this._state = HEADER_LINE_LF | |
| 446 | ||
| 447 | break | |
| 448 | } | |
| 449 | ||
| 450 | if (!isFieldByte(byte)) throw errors.INVALID_HEADER() | |
| 451 | ||
| 452 | this._accumulator.push(byte) | |
| 453 | this._state = HEADER_VALUE | |
| 454 | ||
| 455 | break | |
| 456 | } | |
| 457 | ||
| 458 | case HEADER_VALUE: { | |
| 459 | this._checkHeaderSize() | |
| 460 | ||
| 461 | if (byte === CR) { | |
| 462 | this._storeHeader(this._headerName, this._buildString()) | |
| 463 | this._headerName = '' | |
| 464 | this._state = HEADER_LINE_LF | |
| 465 | } else if (isFieldByte(byte)) { | |
| 466 | this._accumulator.push(byte) | |
| 467 | } else { | |
| 468 | throw errors.INVALID_HEADER() | |
| 469 | } | |
| 470 | ||
| 471 | break | |
| 472 | } | |
| 473 | ||
| 474 | case HEADER_LINE_LF: { | |
| 475 | if (byte !== LF) throw errors.INVALID_HEADER() | |
| 476 | ||
| 477 | this._state = HEADER_START | |
| 478 | ||
| 479 | break | |
| 480 | } | |
| 481 | ||
| 482 | case HEADER_END_LF: { | |
| 483 | if (byte !== LF) throw errors.INVALID_MESSAGE() | |
| 484 | ||
| 485 | const headers = this._headers | |
| 486 | ||
| 487 | if (this._isResponse) { | |
| 488 | yield { | |
| 489 | type: constants.RESPONSE, | |
| 490 | version: this._version, | |
| 491 | code: this._code, | |
| 492 | reason: this._reason, | |
| 493 | headers | |
| 494 | } | |
| 495 | } else { | |
| 496 | if (this._version === 'HTTP/1.1' && !('host' in headers)) { | |
| 497 | throw errors.INVALID_HEADER("Header 'Host' is missing") | |
| 498 | } | |
| 499 | ||
| 500 | yield { | |
| 501 | type: constants.REQUEST, | |
| 502 | version: this._version, | |
| 503 | method: this._method, | |
| 504 | url: this._url, | |
| 505 | headers | |
| 506 | } | |
| 507 | } | |
| 508 | ||
| 509 | const transferEncoding = headers['transfer-encoding'] | |
| 510 | const contentLength = headers['content-length'] | |
| 511 | ||
| 512 | const encodings = transferEncoding ? transferEncoding.split(',') : null | |
| 513 | ||
| 514 | const lastEncoding = encodings | |
| 515 | ? encodings[encodings.length - 1].trim().toLowerCase() | |
| 516 | : null | |
| 517 | ||
| 518 | if (lastEncoding === 'chunked') { | |
| 519 | if (contentLength) { | |
| 520 | throw errors.INVALID_MESSAGE( | |
| 521 | "Conflicting 'Content-Length' and 'Transfer-Encoding' headers" | |
| 522 | ) | |
| 523 | } | |
| 524 | ||
| 525 | this._state = CHUNK_SIZE | |
| 526 | this._headerSize = 0 | |
| 527 | ||
| 528 | continue | |
| 529 | } | |
| 530 | ||
| 531 | if (contentLength) { | |
| 532 | if (contentLength.length === 0) throw errors.INVALID_CONTENT_LENGTH() | |
| 533 | ||
| 534 | let length = 0 | |
| 535 | ||
| 536 | for (let i = 0, n = contentLength.length; i < n; i++) { | |
| 537 | const c = contentLength.charCodeAt(i) | |
| 538 | ||
| 539 | if (c < ZERO || c > NINE) throw errors.INVALID_CONTENT_LENGTH() | |
| 540 | ||
| 541 | length = length * 10 + (c - ZERO) | |
| 542 | } | |
| 543 | ||
| 544 | if (!Number.isSafeInteger(length) || length < 0) { | |
| 545 | throw errors.INVALID_CONTENT_LENGTH() | |
| 546 | } | |
| 547 | ||
| 548 | if (length === 0) { | |
| 549 | this._state = FIRST_TOKEN | |
| 550 | this._headerSize = 0 | |
| 551 | ||
| 552 | yield { type: constants.END } | |
| 553 | } else { | |
| 554 | this._state = BODY | |
| 555 | this._remaining = length | |
| 556 | this._headerSize = 0 | |
| 557 | } | |
| 558 | } else { | |
| 559 | this._state = FIRST_TOKEN | |
| 560 | this._headerSize = 0 | |
| 561 | ||
| 562 | yield { type: constants.END } | |
| 563 | } | |
| 564 | ||
| 565 | break | |
| 566 | } | |
| 567 | ||
| 568 | case CHUNK_SIZE: { | |
| 569 | if (byte === CR || byte === 0x3b) { | |
| 570 | if (this._accumulator.length === 0) throw errors.INVALID_CHUNK_LENGTH() | |
| 571 | ||
| 572 | let length = 0 | |
| 573 | ||
| 574 | for (let i = 0, n = this._accumulator.length; i < n; i++) { | |
| 575 | length = length * 16 + this._accumulator[i] | |
| 576 | } | |
| 577 | ||
| 578 | this._accumulator = [] | |
| 579 | ||
| 580 | if (!Number.isSafeInteger(length)) throw errors.INVALID_CHUNK_LENGTH() | |
| 581 | ||
| 582 | if (byte === 0x3b) { | |
| 583 | this._remaining = length | |
| 584 | this._state = CHUNK_EXTENSION | |
| 585 | } else if (length === 0) { | |
| 586 | this._state = LAST_CHUNK_LF | |
| 587 | } else { | |
| 588 | this._remaining = length + 2 | |
| 589 | this._state = CHUNK_SIZE_LF | |
| 590 | } | |
| 591 | } else if (isHex(byte)) { | |
| 592 | if (this._accumulator.length >= MAX_CHUNK_SIZE_LENGTH) { | |
| 593 | throw errors.INVALID_CHUNK_LENGTH() | |
| 594 | } | |
| 595 | ||
| 596 | this._accumulator.push(hexValue(byte)) | |
| 597 | } else { | |
| 598 | throw errors.INVALID_CHUNK_LENGTH() | |
| 599 | } | |
| 600 | ||
| 601 | break | |
| 602 | } | |
| 603 | ||
| 604 | case CHUNK_EXTENSION: { | |
| 605 | this._checkHeaderSize() | |
| 606 | ||
| 607 | if (byte === CR) { | |
| 608 | if (this._remaining === 0) { | |
| 609 | this._state = LAST_CHUNK_LF | |
| 610 | } else { | |
| 611 | this._remaining += 2 | |
| 612 | this._state = CHUNK_SIZE_LF | |
| 613 | } | |
| 614 | } else if (!isFieldByte(byte)) { | |
| 615 | throw errors.INVALID_CHUNK_LENGTH() | |
| 616 | } | |
| 617 | ||
| 618 | break | |
| 619 | } | |
| 620 | ||
| 621 | case CHUNK_SIZE_LF: { | |
| 622 | if (byte !== LF) throw errors.INVALID_CHUNK_LENGTH() | |
| 623 | ||
| 624 | this._state = CHUNK_DATA | |
| 625 | ||
| 626 | break | |
| 627 | } | |
| 628 | ||
| 629 | case LAST_CHUNK_LF: { | |
| 630 | if (byte !== LF) throw errors.INVALID_CHUNK_LENGTH() | |
| 631 | ||
| 632 | this._state = TRAILER_CR | |
| 633 | ||
| 634 | break | |
| 635 | } | |
| 636 | ||
| 637 | case TRAILER_CR: { | |
| 638 | if (byte !== CR) throw errors.INVALID_MESSAGE() | |
| 639 | ||
| 640 | this._state = TRAILER_LF | |
| 641 | ||
| 642 | break | |
| 643 | } | |
| 644 | ||
| 645 | case TRAILER_LF: { | |
| 646 | if (byte !== LF) throw errors.INVALID_MESSAGE() | |
| 647 | ||
| 648 | this._state = FIRST_TOKEN | |
| 649 | this._headerSize = 0 | |
| 650 | ||
| 651 | yield { type: constants.END } | |
| 652 | ||
| 653 | break | |
| 654 | } | |
| 655 | ||
| 656 | default: | |
| 657 | throw errors.INVALID_MESSAGE() | |
| 658 | } | |
| 659 | } | |
| 660 | } | |
| 661 | } | |
| 662 | ||
| 663 | exports.constants = constants | |
| 664 | ||
| 665 | const TOKEN_BYTES = Buffer.from([ | |
| 666 | // 0x00-0x1f (control characters) + 0x20 (space) | |
| 667 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, | |
| 668 | // 0x21-0x41: ! " # $ % & ' ( ) * + , - . / 0-9 : ; < = > ? @ A | |
| 669 | 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, | |
| 670 | // 0x42-0x62: B-Z [ \ ] ^ _ ` a b | |
| 671 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, | |
| 672 | // 0x63-0x7f: c-z { | } ~ DEL | |
| 673 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0 | |
| 674 | ]) | |
| 675 | ||
| 676 | function isTokenByte(b) { | |
| 677 | return TOKEN_BYTES[b] === 1 | |
| 678 | } | |
| 679 | ||
| 680 | function isFieldByte(b) { | |
| 681 | return b === TAB || (b >= 0x20 && b <= 0x7e) | |
| 682 | } | |
| 683 | ||
| 684 | function isHex(b) { | |
| 685 | return ( | |
| 686 | (b >= ZERO && b <= NINE) || (b >= UPPER_A && b <= UPPER_F) || (b >= LOWER_A && b <= LOWER_F) | |
| 687 | ) | |
| 688 | } | |
| 689 | ||
| 690 | function hexValue(b) { | |
| 691 | if (b >= ZERO && b <= NINE) return b - ZERO | |
| 692 | if (b >= UPPER_A && b <= UPPER_F) return b - UPPER_A + 10 | |
| 693 | return b - LOWER_A + 10 | |
| 694 | } |
| … | … | @@ -0,0 +1,29 @@ |
| 1 | module.exports = class HTTPParserError extends Error { | |
| 2 | constructor(msg, fn = HTTPParserError, code = fn.name) { | |
| 3 | super(`${code}: ${msg}`) | |
| 4 | ||
| 5 | this.code = code | |
| 6 | ||
| 7 | if (Error.captureStackTrace) Error.captureStackTrace(this, fn) | |
| 8 | } | |
| 9 | ||
| 10 | get name() { | |
| 11 | return 'HTTPParserError' | |
| 12 | } | |
| 13 | ||
| 14 | static INVALID_MESSAGE(msg = 'Invalid HTTP message') { | |
| 15 | return new HTTPParserError(msg, HTTPParserError.INVALID_MESSAGE) | |
| 16 | } | |
| 17 | ||
| 18 | static INVALID_HEADER(msg = 'Invalid HTTP header') { | |
| 19 | return new HTTPParserError(msg, HTTPParserError.INVALID_HEADER) | |
| 20 | } | |
| 21 | ||
| 22 | static INVALID_CONTENT_LENGTH(msg = 'Invalid HTTP Content-Length') { | |
| 23 | return new HTTPParserError(msg, HTTPParserError.INVALID_CONTENT_LENGTH) | |
| 24 | } | |
| 25 | ||
| 26 | static INVALID_CHUNK_LENGTH(msg = 'Invalid HTTP chunk length') { | |
| 27 | return new HTTPParserError(msg, HTTPParserError.INVALID_CHUNK_LENGTH) | |
| 28 | } | |
| 29 | } |
| … | … | @@ -0,0 +1,32 @@ |
| 1 | { | |
| 2 | "name": "bare-http-parser", | |
| 3 | "version": "1.1.3", | |
| 4 | "description": "Streaming HTTP request and response parser for Bare", | |
| 5 | "exports": { | |
| 6 | "./package": "./package.json", | |
| 7 | ".": "./index.js", | |
| 8 | "./errors": "./lib/errors.js" | |
| 9 | }, | |
| 10 | "files": [ | |
| 11 | "index.js", | |
| 12 | "lib" | |
| 13 | ], | |
| 14 | "scripts": { | |
| 15 | "test": "prettier . --check && bare test.js" | |
| 16 | }, | |
| 17 | "repository": { | |
| 18 | "type": "git", | |
| 19 | "url": "git+https://github.com/holepunchto/bare-http-parser.git" | |
| 20 | }, | |
| 21 | "author": "Holepunch", | |
| 22 | "license": "Apache-2.0", | |
| 23 | "bugs": { | |
| 24 | "url": "https://github.com/holepunchto/bare-http-parser/issues" | |
| 25 | }, | |
| 26 | "homepage": "https://github.com/holepunchto/bare-http-parser#readme", | |
| 27 | "devDependencies": { | |
| 28 | "brittle": "^3.3.0", | |
| 29 | "prettier": "^3.4.1", | |
| 30 | "prettier-config-holepunch": "^2.0.0" | |
| 31 | } | |
| 32 | } |
| … | … | @@ -0,0 +1,201 @@ |
| 1 | Apache License | |
| 2 | Version 2.0, January 2004 | |
| 3 | http://www.apache.org/licenses/ | |
| 4 | ||
| 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | |
| 6 | ||
| 7 | 1. Definitions. | |
| 8 | ||
| 9 | "License" shall mean the terms and conditions for use, reproduction, | |
| 10 | and distribution as defined by Sections 1 through 9 of this document. | |
| 11 | ||
| 12 | "Licensor" shall mean the copyright owner or entity authorized by | |
| 13 | the copyright owner that is granting the License. | |
| 14 | ||
| 15 | "Legal Entity" shall mean the union of the acting entity and all | |
| 16 | other entities that control, are controlled by, or are under common | |
| 17 | control with that entity. For the purposes of this definition, | |
| 18 | "control" means (i) the power, direct or indirect, to cause the | |
| 19 | direction or management of such entity, whether by contract or | |
| 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the | |
| 21 | outstanding shares, or (iii) beneficial ownership of such entity. | |
| 22 | ||
| 23 | "You" (or "Your") shall mean an individual or Legal Entity | |
| 24 | exercising permissions granted by this License. | |
| 25 | ||
| 26 | "Source" form shall mean the preferred form for making modifications, | |
| 27 | including but not limited to software source code, documentation | |
| 28 | source, and configuration files. | |
| 29 | ||
| 30 | "Object" form shall mean any form resulting from mechanical | |
| 31 | transformation or translation of a Source form, including but | |
| 32 | not limited to compiled object code, generated documentation, | |
| 33 | and conversions to other media types. | |
| 34 | ||
| 35 | "Work" shall mean the work of authorship, whether in Source or | |
| 36 | Object form, made available under the License, as indicated by a | |
| 37 | copyright notice that is included in or attached to the work | |
| 38 | (an example is provided in the Appendix below). | |
| 39 | ||
| 40 | "Derivative Works" shall mean any work, whether in Source or Object | |
| 41 | form, that is based on (or derived from) the Work and for which the | |
| 42 | editorial revisions, annotations, elaborations, or other modifications | |
| 43 | represent, as a whole, an original work of authorship. For the purposes | |
| 44 | of this License, Derivative Works shall not include works that remain | |
| 45 | separable from, or merely link (or bind by name) to the interfaces of, | |
| 46 | the Work and Derivative Works thereof. | |
| 47 | ||
| 48 | "Contribution" shall mean any work of authorship, including | |
| 49 | the original version of the Work and any modifications or additions | |
| 50 | to that Work or Derivative Works thereof, that is intentionally | |
| 51 | submitted to Licensor for inclusion in the Work by the copyright owner | |
| 52 | or by an individual or Legal Entity authorized to submit on behalf of | |
| 53 | the copyright owner. For the purposes of this definition, "submitted" | |
| 54 | means any form of electronic, verbal, or written communication sent | |
| 55 | to the Licensor or its representatives, including but not limited to | |
| 56 | communication on electronic mailing lists, source code control systems, | |
| 57 | and issue tracking systems that are managed by, or on behalf of, the | |
| 58 | Licensor for the purpose of discussing and improving the Work, but | |
| 59 | excluding communication that is conspicuously marked or otherwise | |
| 60 | designated in writing by the copyright owner as "Not a Contribution." | |
| 61 | ||
| 62 | "Contributor" shall mean Licensor and any individual or Legal Entity | |
| 63 | on behalf of whom a Contribution has been received by Licensor and | |
| 64 | subsequently incorporated within the Work. | |
| 65 | ||
| 66 | 2. Grant of Copyright License. Subject to the terms and conditions of | |
| 67 | this License, each Contributor hereby grants to You a perpetual, | |
| 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 69 | copyright license to reproduce, prepare Derivative Works of, | |
| 70 | publicly display, publicly perform, sublicense, and distribute the | |
| 71 | Work and such Derivative Works in Source or Object form. | |
| 72 | ||
| 73 | 3. Grant of Patent License. Subject to the terms and conditions of | |
| 74 | this License, each Contributor hereby grants to You a perpetual, | |
| 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 76 | (except as stated in this section) patent license to make, have made, | |
| 77 | use, offer to sell, sell, import, and otherwise transfer the Work, | |
| 78 | where such license applies only to those patent claims licensable | |
| 79 | by such Contributor that are necessarily infringed by their | |
| 80 | Contribution(s) alone or by combination of their Contribution(s) | |
| 81 | with the Work to which such Contribution(s) was submitted. If You | |
| 82 | institute patent litigation against any entity (including a | |
| 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work | |
| 84 | or a Contribution incorporated within the Work constitutes direct | |
| 85 | or contributory patent infringement, then any patent licenses | |
| 86 | granted to You under this License for that Work shall terminate | |
| 87 | as of the date such litigation is filed. | |
| 88 | ||
| 89 | 4. Redistribution. You may reproduce and distribute copies of the | |
| 90 | Work or Derivative Works thereof in any medium, with or without | |
| 91 | modifications, and in Source or Object form, provided that You | |
| 92 | meet the following conditions: | |
| 93 | ||
| 94 | (a) You must give any other recipients of the Work or | |
| 95 | Derivative Works a copy of this License; and | |
| 96 | ||
| 97 | (b) You must cause any modified files to carry prominent notices | |
| 98 | stating that You changed the files; and | |
| 99 | ||
| 100 | (c) You must retain, in the Source form of any Derivative Works | |
| 101 | that You distribute, all copyright, patent, trademark, and | |
| 102 | attribution notices from the Source form of the Work, | |
| 103 | excluding those notices that do not pertain to any part of | |
| 104 | the Derivative Works; and | |
| 105 | ||
| 106 | (d) If the Work includes a "NOTICE" text file as part of its | |
| 107 | distribution, then any Derivative Works that You distribute must | |
| 108 | include a readable copy of the attribution notices contained | |
| 109 | within such NOTICE file, excluding those notices that do not | |
| 110 | pertain to any part of the Derivative Works, in at least one | |
| 111 | of the following places: within a NOTICE text file distributed | |
| 112 | as part of the Derivative Works; within the Source form or | |
| 113 | documentation, if provided along with the Derivative Works; or, | |
| 114 | within a display generated by the Derivative Works, if and | |
| 115 | wherever such third-party notices normally appear. The contents | |
| 116 | of the NOTICE file are for informational purposes only and | |
| 117 | do not modify the License. You may add Your own attribution | |
| 118 | notices within Derivative Works that You distribute, alongside | |
| 119 | or as an addendum to the NOTICE text from the Work, provided | |
| 120 | that such additional attribution notices cannot be construed | |
| 121 | as modifying the License. | |
| 122 | ||
| 123 | You may add Your own copyright statement to Your modifications and | |
| 124 | may provide additional or different license terms and conditions | |
| 125 | for use, reproduction, or distribution of Your modifications, or | |
| 126 | for any such Derivative Works as a whole, provided Your use, | |
| 127 | reproduction, and distribution of the Work otherwise complies with | |
| 128 | the conditions stated in this License. | |
| 129 | ||
| 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, | |
| 131 | any Contribution intentionally submitted for inclusion in the Work | |
| 132 | by You to the Licensor shall be under the terms and conditions of | |
| 133 | this License, without any additional terms or conditions. | |
| 134 | Notwithstanding the above, nothing herein shall supersede or modify | |
| 135 | the terms of any separate license agreement you may have executed | |
| 136 | with Licensor regarding such Contributions. | |
| 137 | ||
| 138 | 6. Trademarks. This License does not grant permission to use the trade | |
| 139 | names, trademarks, service marks, or product names of the Licensor, | |
| 140 | except as required for reasonable and customary use in describing the | |
| 141 | origin of the Work and reproducing the content of the NOTICE file. | |
| 142 | ||
| 143 | 7. Disclaimer of Warranty. Unless required by applicable law or | |
| 144 | agreed to in writing, Licensor provides the Work (and each | |
| 145 | Contributor provides its Contributions) on an "AS IS" BASIS, | |
| 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |
| 147 | implied, including, without limitation, any warranties or conditions | |
| 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | |
| 149 | PARTICULAR PURPOSE. You are solely responsible for determining the | |
| 150 | appropriateness of using or redistributing the Work and assume any | |
| 151 | risks associated with Your exercise of permissions under this License. | |
| 152 | ||
| 153 | 8. Limitation of Liability. In no event and under no legal theory, | |
| 154 | whether in tort (including negligence), contract, or otherwise, | |
| 155 | unless required by applicable law (such as deliberate and grossly | |
| 156 | negligent acts) or agreed to in writing, shall any Contributor be | |
| 157 | liable to You for damages, including any direct, indirect, special, | |
| 158 | incidental, or consequential damages of any character arising as a | |
| 159 | result of this License or out of the use or inability to use the | |
| 160 | Work (including but not limited to damages for loss of goodwill, | |
| 161 | work stoppage, computer failure or malfunction, or any and all | |
| 162 | other commercial damages or losses), even if such Contributor | |
| 163 | has been advised of the possibility of such damages. | |
| 164 | ||
| 165 | 9. Accepting Warranty or Additional Liability. While redistributing | |
| 166 | the Work or Derivative Works thereof, You may choose to offer, | |
| 167 | and charge a fee for, acceptance of support, warranty, indemnity, | |
| 168 | or other liability obligations and/or rights consistent with this | |
| 169 | License. However, in accepting such obligations, You may act only | |
| 170 | on Your own behalf and on Your sole responsibility, not on behalf | |
| 171 | of any other Contributor, and only if You agree to indemnify, | |
| 172 | defend, and hold each Contributor harmless for any liability | |
| 173 | incurred by, or claims asserted against, such Contributor by reason | |
| 174 | of your accepting any such warranty or additional liability. | |
| 175 | ||
| 176 | END OF TERMS AND CONDITIONS | |
| 177 | ||
| 178 | APPENDIX: How to apply the Apache License to your work. | |
| 179 | ||
| 180 | To apply the Apache License to your work, attach the following | |
| 181 | boilerplate notice, with the fields enclosed by brackets "[]" | |
| 182 | replaced with your own identifying information. (Don't include | |
| 183 | the brackets!) The text should be enclosed in the appropriate | |
| 184 | comment syntax for the file format. We also recommend that a | |
| 185 | file or class name and description of purpose be included on the | |
| 186 | same "printed page" as the copyright notice for easier | |
| 187 | identification within third-party archives. | |
| 188 | ||
| 189 | Copyright [yyyy] [name of copyright owner] | |
| 190 | ||
| 191 | Licensed under the Apache License, Version 2.0 (the "License"); | |
| 192 | you may not use this file except in compliance with the License. | |
| 193 | You may obtain a copy of the License at | |
| 194 | ||
| 195 | http://www.apache.org/licenses/LICENSE-2.0 | |
| 196 | ||
| 197 | Unless required by applicable law or agreed to in writing, software | |
| 198 | distributed under the License is distributed on an "AS IS" BASIS, | |
| 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 200 | See the License for the specific language governing permissions and | |
| 201 | limitations under the License. |
| … | … | @@ -0,0 +1,34 @@ |
| 1 | # bare-http1 | |
| 2 | ||
| 3 | HTTP/1 library for JavaScript. | |
| 4 | ||
| 5 | ``` | |
| 6 | npm i bare-http1 | |
| 7 | ``` | |
| 8 | ||
| 9 | ## Usage | |
| 10 | ||
| 11 | ```js | |
| 12 | const http = require('bare-http1') | |
| 13 | ||
| 14 | const server = http.createServer((req, res) => { | |
| 15 | res.statusCode = 200 | |
| 16 | res.setHeader('Content-Length', 10) | |
| 17 | res.write('hello world!') | |
| 18 | res.end() | |
| 19 | }) | |
| 20 | ||
| 21 | server.listen(0, () => { | |
| 22 | const { port } = server.address() | |
| 23 | console.log('server is bound on', port) | |
| 24 | ||
| 25 | const client = http.request({ port }, (res) => { | |
| 26 | res.on('data', (data) => console.log(data.toString())) | |
| 27 | }) | |
| 28 | client.end() | |
| 29 | }) | |
| 30 | ``` | |
| 31 | ||
| 32 | ## License | |
| 33 | ||
| 34 | Apache-2.0 |
| … | … | @@ -0,0 +1,272 @@ |
| 1 | import { Readable, ReadableEvents, Writable, WritableEvents } from 'bare-stream' | |
| 2 | import { | |
| 3 | TCPSocket, | |
| 4 | TCPSocketOptions, | |
| 5 | TCPSocketConnectOptions, | |
| 6 | TCPServer, | |
| 7 | TCPServerEvents | |
| 8 | } from 'bare-tcp' | |
| 9 | import Buffer from 'bare-buffer' | |
| 10 | import URL from 'bare-url' | |
| 11 | import constants, { HTTPMethod, HTTPStatusCode, HTTPStatusMessage } from './lib/constants' | |
| 12 | import HTTPError from './lib/errors' | |
| 13 | ||
| 14 | export { | |
| 15 | constants, | |
| 16 | type HTTPMethod, | |
| 17 | type HTTPStatusCode, | |
| 18 | type HTTPStatusMessage, | |
| 19 | type HTTPError, | |
| 20 | HTTPError as errors | |
| 21 | } | |
| 22 | ||
| 23 | export const METHODS: HTTPMethod[] | |
| 24 | export const STATUS_CODES: typeof constants.status | |
| 25 | ||
| 26 | export interface HTTPIncomingMessageEvents extends ReadableEvents { | |
| 27 | timeout: [] | |
| 28 | } | |
| 29 | ||
| 30 | export interface HTTPIncomingMessageOptions { | |
| 31 | headers?: Record<string, string | number> | |
| 32 | method?: HTTPMethod | |
| 33 | url?: string | |
| 34 | statusCode?: HTTPStatusCode | |
| 35 | statusMessage?: HTTPStatusMessage | |
| 36 | } | |
| 37 | ||
| 38 | interface HTTPIncomingMessage< | |
| 39 | M extends HTTPIncomingMessageEvents = HTTPIncomingMessageEvents | |
| 40 | > extends Readable<M> { | |
| 41 | readonly socket: TCPSocket | |
| 42 | readonly upgrade: boolean | |
| 43 | headers: Record<string, string | number> | |
| 44 | method: HTTPMethod | |
| 45 | url: string | |
| 46 | statusCode: HTTPStatusCode | |
| 47 | statusMessage: HTTPStatusMessage | |
| 48 | readonly httpVersion: '1.1' | |
| 49 | ||
| 50 | getHeader(name: string): string | number | undefined | |
| 51 | getHeaders(): Record<string, string | number> | |
| 52 | hasHeader(name: string): boolean | |
| 53 | ||
| 54 | setTimeout(ms: number, ontimeout?: () => void): this | |
| 55 | } | |
| 56 | ||
| 57 | declare class HTTPIncomingMessage< | |
| 58 | M extends HTTPIncomingMessageEvents = HTTPIncomingMessageEvents | |
| 59 | > extends Readable<M> { | |
| 60 | constructor(socket?: TCPSocket, opts?: HTTPIncomingMessageOptions) | |
| 61 | } | |
| 62 | ||
| 63 | export { type HTTPIncomingMessage, HTTPIncomingMessage as IncomingMessage } | |
| 64 | ||
| 65 | export interface HTTPOutgoingMessageEvents extends WritableEvents { | |
| 66 | timeout: [] | |
| 67 | } | |
| 68 | ||
| 69 | interface HTTPOutgoingMessage< | |
| 70 | M extends HTTPOutgoingMessageEvents = HTTPOutgoingMessageEvents | |
| 71 | > extends Writable<M> { | |
| 72 | readonly socket: TCPSocket | |
| 73 | readonly upgrade: boolean | |
| 74 | readonly headersSent: boolean | |
| 75 | headers: Record<string, string | number> | |
| 76 | ||
| 77 | getHeader(name: string): string | number | undefined | |
| 78 | getHeaders(): Record<string, string | number> | |
| 79 | hasHeader(name: string): boolean | |
| 80 | setHeader(name: string, value: string | number): void | |
| 81 | flushHeaders(): void | |
| 82 | ||
| 83 | setTimeout(ms: number, ontimeout?: () => void): this | |
| 84 | } | |
| 85 | ||
| 86 | declare class HTTPOutgoingMessage< | |
| 87 | M extends HTTPOutgoingMessageEvents = HTTPOutgoingMessageEvents | |
| 88 | > extends Writable<M> { | |
| 89 | constructor(socket?: TCPSocket) | |
| 90 | } | |
| 91 | ||
| 92 | export { type HTTPOutgoingMessage, HTTPOutgoingMessage as OutgoingMessage } | |
| 93 | ||
| 94 | export interface HTTPAgentOptions { | |
| 95 | keepAlive?: boolean | |
| 96 | keepAliveMsecs?: number | |
| 97 | } | |
| 98 | ||
| 99 | interface HTTPAgent { | |
| 100 | readonly suspended: boolean | |
| 101 | readonly resumed: Promise<void> | null | |
| 102 | readonly sockets: IterableIterator<TCPSocket> | |
| 103 | readonly freeSockets: IterableIterator<TCPSocket> | |
| 104 | ||
| 105 | createConnection(opts?: TCPSocketOptions & TCPSocketConnectOptions): TCPSocket | |
| 106 | reuseSocket(socket: TCPSocket, req?: HTTPClientRequest): void | |
| 107 | keepSocketAlive(socket: TCPSocket): boolean | |
| 108 | getName(opts: { host: string; port: number }): string | |
| 109 | addRequest(req: HTTPClientRequest, opts: TCPSocketOptions & TCPSocketConnectOptions): void | |
| 110 | ||
| 111 | suspend(): void | |
| 112 | resume(): void | |
| 113 | destroy(): void | |
| 114 | } | |
| 115 | ||
| 116 | declare class HTTPAgent { | |
| 117 | constructor(opts?: HTTPAgentOptions & TCPSocketOptions & TCPSocketConnectOptions) | |
| 118 | } | |
| 119 | ||
| 120 | declare namespace HTTPAgent { | |
| 121 | export const global: HTTPAgent | |
| 122 | } | |
| 123 | ||
| 124 | export const globalAgent: HTTPAgent | |
| 125 | ||
| 126 | export { type HTTPAgent, HTTPAgent as Agent } | |
| 127 | ||
| 128 | export interface HTTPServerEvents extends TCPServerEvents { | |
| 129 | request: [req: HTTPIncomingMessage, res: HTTPServerResponse] | |
| 130 | upgrade: [req: HTTPIncomingMessage, socket: TCPSocket, head: Buffer] | |
| 131 | timeout: [socket: TCPSocket] | |
| 132 | } | |
| 133 | ||
| 134 | interface HTTPServer<M extends HTTPServerEvents = HTTPServerEvents> extends TCPServer<M> { | |
| 135 | readonly timeout: number | undefined | |
| 136 | ||
| 137 | setTimeout(ms: number, ontimeout?: () => void): this | |
| 138 | } | |
| 139 | ||
| 140 | declare class HTTPServer<M extends HTTPServerEvents = HTTPServerEvents> extends TCPServer<M> { | |
| 141 | constructor( | |
| 142 | opts?: HTTPServerConnectionOptions, | |
| 143 | onrequest?: (req: HTTPIncomingMessage, res: HTTPServerResponse) => void | |
| 144 | ) | |
| 145 | ||
| 146 | constructor(onrequest: (req: HTTPIncomingMessage, res: HTTPServerResponse) => void) | |
| 147 | } | |
| 148 | ||
| 149 | export { type HTTPServer, HTTPServer as Server } | |
| 150 | ||
| 151 | interface HTTPServerResponse extends HTTPOutgoingMessage { | |
| 152 | readonly req: HTTPIncomingMessage | |
| 153 | statusCode: HTTPStatusCode | |
| 154 | statusMessage: HTTPStatusMessage | null | |
| 155 | ||
| 156 | writeHead( | |
| 157 | statusCode: HTTPStatusCode, | |
| 158 | statusMessage?: HTTPStatusMessage, | |
| 159 | headers?: Record<string, string | number> | |
| 160 | ): void | |
| 161 | ||
| 162 | writeHead(statusCode: HTTPStatusCode, headers?: Record<string, string | number>): void | |
| 163 | } | |
| 164 | ||
| 165 | declare class HTTPServerResponse extends HTTPOutgoingMessage { | |
| 166 | constructor(socket: TCPSocket, req: HTTPIncomingMessage) | |
| 167 | } | |
| 168 | ||
| 169 | export { type HTTPServerResponse, HTTPServerResponse as ServerResponse } | |
| 170 | ||
| 171 | export interface HTTPServerConnectionOptions { | |
| 172 | IncomingMessage?: typeof HTTPIncomingMessage | |
| 173 | ServerResponse?: typeof HTTPServerResponse | |
| 174 | } | |
| 175 | ||
| 176 | interface HTTPServerConnection { | |
| 177 | readonly server: HTTPServer | |
| 178 | readonly socket: TCPSocket | null | |
| 179 | readonly req: HTTPIncomingMessage | null | |
| 180 | readonly res: HTTPServerResponse | null | |
| 181 | readonly idle: boolean | |
| 182 | } | |
| 183 | ||
| 184 | declare class HTTPServerConnection { | |
| 185 | constructor(server: HTTPServer, socket: TCPSocket, opts?: HTTPServerConnectionOptions) | |
| 186 | ||
| 187 | static for(socket: TCPSocket): HTTPServerConnection | |
| 188 | } | |
| 189 | ||
| 190 | export { type HTTPServerConnection, HTTPServerConnection as ServerConnection } | |
| 191 | ||
| 192 | export interface HTTPClientRequestEvents extends HTTPOutgoingMessageEvents { | |
| 193 | response: [res: HTTPIncomingMessage] | |
| 194 | upgrade: [res: HTTPIncomingMessage, socket: TCPSocket, head: Buffer] | |
| 195 | } | |
| 196 | ||
| 197 | export interface HTTPClientRequestOptions extends TCPSocketConnectOptions { | |
| 198 | agent?: HTTPAgent | false | |
| 199 | headers?: Record<string, string | number> | |
| 200 | method?: HTTPMethod | |
| 201 | path?: string | |
| 202 | } | |
| 203 | ||
| 204 | interface HTTPClientRequest< | |
| 205 | M extends HTTPClientRequestEvents = HTTPClientRequestEvents | |
| 206 | > extends HTTPOutgoingMessage<M> { | |
| 207 | readonly method: HTTPMethod | |
| 208 | readonly path: string | |
| 209 | readonly headers: Record<string, string | number> | |
| 210 | } | |
| 211 | ||
| 212 | declare class HTTPClientRequest< | |
| 213 | M extends HTTPClientRequestEvents = HTTPClientRequestEvents | |
| 214 | > extends HTTPOutgoingMessage<M> { | |
| 215 | constructor(opts?: HTTPClientRequestOptions, onresponse?: () => void) | |
| 216 | ||
| 217 | constructor(onresponse: () => void) | |
| 218 | } | |
| 219 | ||
| 220 | export { type HTTPClientRequest, HTTPClientRequest as ClientRequest } | |
| 221 | ||
| 222 | export interface HTTPClientConnectionOptions { | |
| 223 | IncomingMessage?: typeof HTTPIncomingMessage | |
| 224 | } | |
| 225 | ||
| 226 | interface HTTPClientConnection { | |
| 227 | readonly socket: TCPSocket | null | |
| 228 | readonly req: HTTPClientRequest | null | |
| 229 | readonly res: HTTPIncomingMessage | null | |
| 230 | readonly idle: boolean | |
| 231 | } | |
| 232 | ||
| 233 | declare class HTTPClientConnection { | |
| 234 | constructor(socket: TCPSocket, opts?: HTTPClientConnectionOptions) | |
| 235 | ||
| 236 | static for(socket: TCPSocket): HTTPClientConnection | null | |
| 237 | ||
| 238 | static from(socket: TCPSocket, opts?: HTTPClientConnectionOptions): HTTPClientConnection | |
| 239 | } | |
| 240 | ||
| 241 | export { type HTTPClientConnection, HTTPClientConnection as ClientConnection } | |
| 242 | ||
| 243 | export function createServer( | |
| 244 | opts?: HTTPServerConnectionOptions, | |
| 245 | onrequest?: (req: HTTPIncomingMessage, res: HTTPServerResponse) => void | |
| 246 | ): HTTPServer | |
| 247 | ||
| 248 | export function createServer( | |
| 249 | onrequest: (req: HTTPIncomingMessage, res: HTTPServerResponse) => void | |
| 250 | ): HTTPServer | |
| 251 | ||
| 252 | export function request( | |
| 253 | url: URL | string, | |
| 254 | opts?: HTTPClientRequestOptions, | |
| 255 | onresponse?: (res: HTTPIncomingMessage) => void | |
| 256 | ): HTTPClientRequest | |
| 257 | ||
| 258 | export function get( | |
| 259 | url: URL | string, | |
| 260 | opts?: HTTPClientRequestOptions, | |
| 261 | onresponse?: (res: HTTPIncomingMessage) => void | |
| 262 | ): HTTPClientRequest | |
| 263 | ||
| 264 | export function request( | |
| 265 | url: URL | string, | |
| 266 | onresponse: (res: HTTPIncomingMessage) => void | |
| 267 | ): HTTPClientRequest | |
| 268 | ||
| 269 | export function request( | |
| 270 | opts: HTTPClientRequestOptions, | |
| 271 | onresponse?: (res: HTTPIncomingMessage) => void | |
| 272 | ): HTTPClientRequest |
| … | … | @@ -0,0 +1,81 @@ |
| 1 | exports.IncomingMessage = require('./lib/incoming-message') | |
| 2 | exports.OutgoingMessage = require('./lib/outgoing-message') | |
| 3 | ||
| 4 | exports.Agent = require('./lib/agent') | |
| 5 | exports.globalAgent = exports.Agent.global | |
| 6 | ||
| 7 | exports.Server = require('./lib/server') | |
| 8 | exports.ServerResponse = require('./lib/server-response') | |
| 9 | exports.ServerConnection = require('./lib/server-connection') | |
| 10 | ||
| 11 | exports.ClientRequest = require('./lib/client-request') | |
| 12 | exports.ClientConnection = require('./lib/client-connection') | |
| 13 | ||
| 14 | exports.constants = require('./lib/constants') | |
| 15 | exports.errors = require('./lib/errors') | |
| 16 | ||
| 17 | exports.METHODS = Object.values(exports.constants.method) // For Node.js compatibility | |
| 18 | exports.STATUS_CODES = exports.constants.status // For Node.js compatibility | |
| 19 | ||
| 20 | exports.createServer = function createServer(opts, onrequest) { | |
| 21 | return new exports.Server(opts, onrequest) | |
| 22 | } | |
| 23 | ||
| 24 | exports.request = function request(url, opts, onresponse) { | |
| 25 | if (typeof opts === 'function') { | |
| 26 | onresponse = opts | |
| 27 | opts = {} | |
| 28 | } | |
| 29 | ||
| 30 | if (typeof url === 'string') url = new URL(url) | |
| 31 | ||
| 32 | if (isURL(url)) { | |
| 33 | opts = opts ? { ...url, ...opts } : { ...url } | |
| 34 | ||
| 35 | opts.host = url.hostname | |
| 36 | opts.path = url.pathname + url.search | |
| 37 | opts.port = url.port ? parseInt(url.port, 10) : defaultPort(url) | |
| 38 | } else { | |
| 39 | opts = url ? { ...url } : {} | |
| 40 | ||
| 41 | // For Node.js compatibility | |
| 42 | opts.host = opts.hostname || opts.host | |
| 43 | opts.port = typeof opts.port === 'string' ? parseInt(opts.port, 10) : opts.port | |
| 44 | } | |
| 45 | ||
| 46 | return new exports.ClientRequest(opts, onresponse) | |
| 47 | } | |
| 48 | ||
| 49 | exports.get = function get(url, opts, onresponse) { | |
| 50 | const req = exports.request(url, opts, onresponse) | |
| 51 | req.end() | |
| 52 | return req | |
| 53 | } | |
| 54 | ||
| 55 | // https://url.spec.whatwg.org/#default-port | |
| 56 | function defaultPort(url) { | |
| 57 | switch (url.protocol) { | |
| 58 | case 'ftp:': | |
| 59 | return 21 | |
| 60 | case 'http:': | |
| 61 | case 'ws:': | |
| 62 | return 80 | |
| 63 | case 'https:': | |
| 64 | case 'wss:': | |
| 65 | return 443 | |
| 66 | } | |
| 67 | ||
| 68 | return null | |
| 69 | } | |
| 70 | ||
| 71 | // https://url.spec.whatwg.org/#api | |
| 72 | function isURL(url) { | |
| 73 | return ( | |
| 74 | url !== null && | |
| 75 | typeof url === 'object' && | |
| 76 | typeof url.protocol === 'string' && | |
| 77 | typeof url.hostname === 'string' && | |
| 78 | typeof url.pathname === 'string' && | |
| 79 | typeof url.search === 'string' | |
| 80 | ) | |
| 81 | } |
| … | … | @@ -0,0 +1,220 @@ |
| 1 | const EventEmitter = require('bare-events') | |
| 2 | const tcp = require('bare-tcp') | |
| 3 | const HTTPClientConnection = require('./client-connection') | |
| 4 | const errors = require('./errors') | |
| 5 | ||
| 6 | class HTTPSocketSet { | |
| 7 | constructor() { | |
| 8 | this._sockets = new Map() | |
| 9 | this._size = 0 | |
| 10 | } | |
| 11 | ||
| 12 | get size() { | |
| 13 | return this._size | |
| 14 | } | |
| 15 | ||
| 16 | add(name, socket) { | |
| 17 | const sockets = this._sockets.get(name) | |
| 18 | ||
| 19 | this._size++ | |
| 20 | ||
| 21 | if (sockets === undefined) this._sockets.set(name, [socket]) | |
| 22 | else sockets.push(socket) | |
| 23 | } | |
| 24 | ||
| 25 | pop(name) { | |
| 26 | const sockets = this._sockets.get(name) | |
| 27 | if (sockets === undefined || sockets.length === 0) return null | |
| 28 | ||
| 29 | this._size-- | |
| 30 | ||
| 31 | const last = sockets.pop() | |
| 32 | ||
| 33 | if (sockets.length === 0) this._sockets.delete(name) | |
| 34 | ||
| 35 | return last | |
| 36 | } | |
| 37 | ||
| 38 | delete(name, socket) { | |
| 39 | const sockets = this._sockets.get(name) | |
| 40 | if (sockets === undefined) return | |
| 41 | ||
| 42 | const i = sockets.indexOf(socket) | |
| 43 | if (i === -1) return | |
| 44 | ||
| 45 | this._size-- | |
| 46 | ||
| 47 | const last = sockets.pop() | |
| 48 | if (last !== socket) sockets[i] = last | |
| 49 | ||
| 50 | if (sockets.length === 0) this._sockets.delete(name) | |
| 51 | } | |
| 52 | ||
| 53 | *sockets() { | |
| 54 | for (const sockets of this._sockets.values()) { | |
| 55 | yield* sockets | |
| 56 | } | |
| 57 | } | |
| 58 | ||
| 59 | *[Symbol.iterator]() { | |
| 60 | for (const [name, sockets] of this._sockets) { | |
| 61 | for (const socket of sockets) yield [name, socket] | |
| 62 | } | |
| 63 | } | |
| 64 | } | |
| 65 | ||
| 66 | class HTTPAgent extends EventEmitter { | |
| 67 | constructor(opts = {}) { | |
| 68 | super() | |
| 69 | ||
| 70 | const { keepAlive = false, keepAliveMsecs = 1000, defaultPort = 80 } = opts | |
| 71 | ||
| 72 | this._suspended = false | |
| 73 | this._resuming = null | |
| 74 | ||
| 75 | this._sockets = new HTTPSocketSet() | |
| 76 | this._freeSockets = new HTTPSocketSet() | |
| 77 | ||
| 78 | this._keepAlive = typeof keepAlive === 'number' ? keepAlive : keepAlive ? keepAliveMsecs : -1 | |
| 79 | this._defaultPort = defaultPort | |
| 80 | ||
| 81 | this._opts = { ...opts } | |
| 82 | } | |
| 83 | ||
| 84 | get suspended() { | |
| 85 | return this._suspended | |
| 86 | } | |
| 87 | ||
| 88 | get resumed() { | |
| 89 | return this._resuming ? this._resuming.promise : null | |
| 90 | } | |
| 91 | ||
| 92 | get sockets() { | |
| 93 | return this._sockets.sockets() | |
| 94 | } | |
| 95 | ||
| 96 | get freeSockets() { | |
| 97 | return this._freeSockets.sockets() | |
| 98 | } | |
| 99 | ||
| 100 | get defaultPort() { | |
| 101 | return this._defaultPort | |
| 102 | } | |
| 103 | ||
| 104 | createConnection(opts) { | |
| 105 | if (this._suspended) throw errors.AGENT_SUSPENDED() | |
| 106 | ||
| 107 | return tcp.createConnection(opts) | |
| 108 | } | |
| 109 | ||
| 110 | reuseSocket(socket, req) { | |
| 111 | socket.ref() | |
| 112 | } | |
| 113 | ||
| 114 | keepSocketAlive(socket) { | |
| 115 | if (this._keepAlive === -1) return false | |
| 116 | ||
| 117 | socket.setKeepAlive(true, this._keepAlive) | |
| 118 | socket.unref() | |
| 119 | ||
| 120 | return true | |
| 121 | } | |
| 122 | ||
| 123 | getName(opts) { | |
| 124 | return `${opts.host}:${opts.port}` | |
| 125 | } | |
| 126 | ||
| 127 | addRequest(req, opts) { | |
| 128 | opts = { ...opts, ...this._opts } | |
| 129 | ||
| 130 | const name = this.getName(opts) | |
| 131 | ||
| 132 | let socket = this._freeSockets.pop(name) | |
| 133 | ||
| 134 | if (socket) this.reuseSocket(socket, req) | |
| 135 | else { | |
| 136 | const agent = this | |
| 137 | ||
| 138 | socket = this.createConnection(opts) | |
| 139 | ||
| 140 | socket | |
| 141 | .on('free', onfree) | |
| 142 | .on('end', onremove) | |
| 143 | .on('finish', onremove) | |
| 144 | .on('close', onremove) | |
| 145 | .on('timeout', ontimeout) | |
| 146 | ||
| 147 | function onfree() { | |
| 148 | if (socket.destroyed) return | |
| 149 | ||
| 150 | if (agent.keepSocketAlive(socket)) { | |
| 151 | agent._freeSockets.add(name, socket) | |
| 152 | } else { | |
| 153 | socket.end() | |
| 154 | } | |
| 155 | ||
| 156 | agent.emit('free', socket) | |
| 157 | } | |
| 158 | ||
| 159 | function onremove() { | |
| 160 | socket.off('free', onfree) | |
| 161 | ||
| 162 | agent._sockets.delete(name, socket) | |
| 163 | agent._freeSockets.delete(name, socket) | |
| 164 | ||
| 165 | if (agent._sockets.size === 0) HTTPAgent._agents.delete(agent) | |
| 166 | } | |
| 167 | ||
| 168 | function ontimeout() { | |
| 169 | socket.destroy() | |
| 170 | ||
| 171 | agent._freeSockets.delete(name, socket) | |
| 172 | } | |
| 173 | } | |
| 174 | ||
| 175 | if (this._sockets.size === 0) HTTPAgent._agents.add(this) | |
| 176 | ||
| 177 | this._sockets.add(name, socket) | |
| 178 | ||
| 179 | req._socket = socket | |
| 180 | ||
| 181 | const connection = HTTPClientConnection.from(socket, opts) | |
| 182 | ||
| 183 | connection._req = req | |
| 184 | } | |
| 185 | ||
| 186 | suspend() { | |
| 187 | if (this._suspended) return | |
| 188 | ||
| 189 | this._resuming = Promise.withResolvers() | |
| 190 | this._suspended = true | |
| 191 | ||
| 192 | this.destroy() | |
| 193 | } | |
| 194 | ||
| 195 | resume() { | |
| 196 | if (this._resuming === null) return | |
| 197 | ||
| 198 | this._resuming.resolve() | |
| 199 | this._resuming = null | |
| 200 | this._suspended = false | |
| 201 | } | |
| 202 | ||
| 203 | destroy() { | |
| 204 | for (const socket of this._sockets.sockets()) socket.destroy() | |
| 205 | } | |
| 206 | ||
| 207 | static _agents = new Set() | |
| 208 | ||
| 209 | static _onidle() { | |
| 210 | for (const agent of this._agents) { | |
| 211 | agent.destroy() | |
| 212 | } | |
| 213 | } | |
| 214 | } | |
| 215 | ||
| 216 | HTTPAgent.global = new HTTPAgent({ keepAlive: 1000, timeout: 5000 }) | |
| 217 | ||
| 218 | module.exports = HTTPAgent | |
| 219 | ||
| 220 | Bare.on('idle', HTTPAgent._onidle.bind(HTTPAgent)) |
| … | … | @@ -0,0 +1,169 @@ |
| 1 | const HTTPParser = require('bare-http-parser') | |
| 2 | const HTTPIncomingMessage = require('./incoming-message') | |
| 3 | const errors = require('./errors') | |
| 4 | ||
| 5 | const { | |
| 6 | constants: { RESPONSE, DATA, END } | |
| 7 | } = HTTPParser | |
| 8 | ||
| 9 | const EMPTY = Buffer.alloc(0) | |
| 10 | ||
| 11 | module.exports = class HTTPClientConnection { | |
| 12 | static _connections = new WeakMap() | |
| 13 | ||
| 14 | static for(socket) { | |
| 15 | return this._connections.get(socket) || null | |
| 16 | } | |
| 17 | ||
| 18 | static from(socket, opts) { | |
| 19 | return this.for(socket) || new this(socket, opts) | |
| 20 | } | |
| 21 | ||
| 22 | constructor(socket, opts = {}) { | |
| 23 | const { IncomingMessage = HTTPIncomingMessage } = opts | |
| 24 | ||
| 25 | this._socket = socket | |
| 26 | ||
| 27 | this._req = null | |
| 28 | this._res = null | |
| 29 | ||
| 30 | this._IncomingMessage = IncomingMessage | |
| 31 | ||
| 32 | this._parser = new HTTPParser() | |
| 33 | this._idle = true | |
| 34 | ||
| 35 | this._onerror = this._onerror.bind(this) | |
| 36 | this._onclose = this._onclose.bind(this) | |
| 37 | this._onend = this._onend.bind(this) | |
| 38 | this._ondata = this._ondata.bind(this) | |
| 39 | this._ondrain = this._ondrain.bind(this) | |
| 40 | this._ontimeout = this._ontimeout.bind(this) | |
| 41 | ||
| 42 | socket | |
| 43 | .on('error', this._onerror) | |
| 44 | .on('close', this._onclose) | |
| 45 | .on('end', this._onend) | |
| 46 | .on('data', this._ondata) | |
| 47 | .on('drain', this._ondrain) | |
| 48 | .on('timeout', this._ontimeout) | |
| 49 | ||
| 50 | HTTPClientConnection._connections.set(socket, this) | |
| 51 | } | |
| 52 | ||
| 53 | get socket() { | |
| 54 | return this._socket | |
| 55 | } | |
| 56 | ||
| 57 | get req() { | |
| 58 | return this._req | |
| 59 | } | |
| 60 | ||
| 61 | get res() { | |
| 62 | return this._res | |
| 63 | } | |
| 64 | ||
| 65 | get idle() { | |
| 66 | return this._idle | |
| 67 | } | |
| 68 | ||
| 69 | _onerror(err) { | |
| 70 | if (this._req) this._req.destroy(err) | |
| 71 | } | |
| 72 | ||
| 73 | _onclose() { | |
| 74 | if (this._req) this._req.destroy() | |
| 75 | } | |
| 76 | ||
| 77 | _onend() { | |
| 78 | if (this._req) this._req.destroy(errors.CONNECTION_LOST()) | |
| 79 | } | |
| 80 | ||
| 81 | _ondata(data) { | |
| 82 | this._idle = false | |
| 83 | ||
| 84 | try { | |
| 85 | for (const op of this._parser.push(data)) { | |
| 86 | switch (op.type) { | |
| 87 | case RESPONSE: | |
| 88 | this._req.on('close', () => { | |
| 89 | this._req = null | |
| 90 | }) | |
| 91 | ||
| 92 | this._res = new this._IncomingMessage(this._socket, { | |
| 93 | headers: op.headers, | |
| 94 | statusCode: op.code, | |
| 95 | statusMessage: op.reason | |
| 96 | }) | |
| 97 | ||
| 98 | this._res.on('close', () => { | |
| 99 | this._res = null | |
| 100 | this._idle = true | |
| 101 | ||
| 102 | this._socket.emit('free') | |
| 103 | }) | |
| 104 | ||
| 105 | if (op.headers.connection && op.headers.connection.toLowerCase() === 'upgrade') { | |
| 106 | return this._onupgrade(this._parser.end()) | |
| 107 | } | |
| 108 | ||
| 109 | this._req.emit('response', this._res) | |
| 110 | break | |
| 111 | ||
| 112 | case DATA: | |
| 113 | this._res.push(op.data) | |
| 114 | break | |
| 115 | ||
| 116 | case END: | |
| 117 | if (this._res) { | |
| 118 | this._res._socket = null | |
| 119 | this._res.push(null) | |
| 120 | } | |
| 121 | ||
| 122 | if (this._req) { | |
| 123 | this._req._socket = null | |
| 124 | this._req.destroy() | |
| 125 | } | |
| 126 | break | |
| 127 | } | |
| 128 | } | |
| 129 | } catch (err) { | |
| 130 | this._socket.destroy(err) | |
| 131 | } | |
| 132 | } | |
| 133 | ||
| 134 | _onupgrade(data) { | |
| 135 | this._detach() | |
| 136 | ||
| 137 | const res = this._res | |
| 138 | const req = this._req | |
| 139 | ||
| 140 | res._upgrade = req._upgrade = true | |
| 141 | ||
| 142 | const upgraded = req.emit('upgrade', res, this._socket, data || EMPTY) | |
| 143 | ||
| 144 | res.push(null) | |
| 145 | req.destroy() | |
| 146 | ||
| 147 | if (!upgraded) this._socket.destroy() | |
| 148 | } | |
| 149 | ||
| 150 | _ontimeout() { | |
| 151 | if (this._req) this._req.emit('timeout') | |
| 152 | } | |
| 153 | ||
| 154 | _ondrain() { | |
| 155 | if (this._req) this._req._continueWrite() | |
| 156 | } | |
| 157 | ||
| 158 | _detach() { | |
| 159 | this._socket | |
| 160 | .off('error', this._onerror) | |
| 161 | .off('close', this._onclose) | |
| 162 | .off('end', this._onend) | |
| 163 | .off('data', this._ondata) | |
| 164 | .off('drain', this._ondrain) | |
| 165 | .off('timeout', this._ontimeout) | |
| 166 | ||
| 167 | HTTPClientConnection._connections.delete(this._socket) | |
| 168 | } | |
| 169 | } |
| … | … | @@ -0,0 +1,143 @@ |
| 1 | const HTTPAgent = require('./agent') | |
| 2 | const HTTPOutgoingMessage = require('./outgoing-message') | |
| 3 | ||
| 4 | const CHUNK_DELIMITER = Buffer.from('\r\n') | |
| 5 | const CHUNK_TERMINATOR = Buffer.from('0\r\n\r\n') | |
| 6 | ||
| 7 | module.exports = class HTTPClientRequest extends HTTPOutgoingMessage { | |
| 8 | constructor(opts = {}, onresponse = null) { | |
| 9 | if (typeof opts === 'function') { | |
| 10 | onresponse = opts | |
| 11 | opts = {} | |
| 12 | } | |
| 13 | ||
| 14 | opts = opts ? { ...opts } : {} | |
| 15 | ||
| 16 | const agent = opts.agent === false ? new HTTPAgent() : opts.agent || HTTPAgent.global | |
| 17 | const method = opts.method || 'GET' | |
| 18 | const path = opts.path || '/' | |
| 19 | const defaultPort = opts.defaultPort || (agent && agent.defaultPort) || 80 | |
| 20 | const host = (opts.host = opts.host || 'localhost') | |
| 21 | const port = (opts.port = opts.port || defaultPort) | |
| 22 | const headers = { host: hostHeader(host, port, defaultPort), ...opts.headers } | |
| 23 | ||
| 24 | super() | |
| 25 | ||
| 26 | agent.addRequest(this, opts) | |
| 27 | ||
| 28 | this._headers = headers | |
| 29 | this._method = method | |
| 30 | this._path = path | |
| 31 | ||
| 32 | this._chunked = method !== 'GET' && method !== 'HEAD' | |
| 33 | ||
| 34 | this._pendingWrite = null | |
| 35 | this._pendingFinal = null | |
| 36 | ||
| 37 | if (onresponse) this.once('response', onresponse) | |
| 38 | } | |
| 39 | ||
| 40 | get method() { | |
| 41 | return this._method | |
| 42 | } | |
| 43 | ||
| 44 | get path() { | |
| 45 | return this._path | |
| 46 | } | |
| 47 | ||
| 48 | // For Node.js compatibility | |
| 49 | abort() { | |
| 50 | return this.destroy() | |
| 51 | } | |
| 52 | ||
| 53 | _header() { | |
| 54 | let h = `${this._method} ${this._path} HTTP/1.1\r\n` | |
| 55 | ||
| 56 | let upgrade = false | |
| 57 | ||
| 58 | for (const name of Object.keys(this._headers)) { | |
| 59 | const n = name.toLowerCase() | |
| 60 | const v = this._headers[name] | |
| 61 | ||
| 62 | if (n === 'content-length') this._chunked = false | |
| 63 | if (n === 'connection' && v && v.toLowerCase() === 'upgrade') upgrade = true | |
| 64 | ||
| 65 | h += `${httpCase(n)}: ${v}\r\n` | |
| 66 | } | |
| 67 | ||
| 68 | if (upgrade) this._chunked = false | |
| 69 | ||
| 70 | if (this._chunked) h += 'Transfer-Encoding: chunked\r\n' | |
| 71 | ||
| 72 | h += '\r\n' | |
| 73 | ||
| 74 | return h | |
| 75 | } | |
| 76 | ||
| 77 | _write(data, encoding, cb) { | |
| 78 | if (this._headersSent === false) this.flushHeaders() | |
| 79 | ||
| 80 | if (this._chunked) { | |
| 81 | this._socket.write(Buffer.from(data.byteLength.toString(16))) | |
| 82 | this._socket.write(CHUNK_DELIMITER) | |
| 83 | } | |
| 84 | ||
| 85 | let flushed = this._socket.write(data) | |
| 86 | ||
| 87 | if (this._chunked) flushed = this._socket.write(CHUNK_DELIMITER) | |
| 88 | ||
| 89 | if (flushed) cb(null) | |
| 90 | else this._pendingWrite = cb | |
| 91 | } | |
| 92 | ||
| 93 | _final(cb) { | |
| 94 | if (this._headersSent === false) this.flushHeaders() | |
| 95 | ||
| 96 | if (this._chunked) this._socket.write(CHUNK_TERMINATOR) | |
| 97 | ||
| 98 | this._pendingFinal = cb | |
| 99 | } | |
| 100 | ||
| 101 | _predestroy() { | |
| 102 | super._predestroy() | |
| 103 | ||
| 104 | this._continueWrite() | |
| 105 | this._continueFinal() | |
| 106 | } | |
| 107 | ||
| 108 | _continueWrite() { | |
| 109 | if (this._pendingWrite === null) return | |
| 110 | const cb = this._pendingWrite | |
| 111 | this._pendingWrite = null | |
| 112 | cb(null) | |
| 113 | } | |
| 114 | ||
| 115 | _continueFinal() { | |
| 116 | if (this._pendingFinal === null) return | |
| 117 | const cb = this._pendingFinal | |
| 118 | this._pendingFinal = null | |
| 119 | cb(null) | |
| 120 | } | |
| 121 | } | |
| 122 | ||
| 123 | function httpCase(n) { | |
| 124 | let s = '' | |
| 125 | for (const part of n.split('-')) { | |
| 126 | s += (s ? '-' : '') + part.slice(0, 1).toUpperCase() + part.slice(1) | |
| 127 | } | |
| 128 | return s | |
| 129 | } | |
| 130 | ||
| 131 | function hostHeader(host, port, defaultPort) { | |
| 132 | const i = host.indexOf(':') | |
| 133 | ||
| 134 | if (i !== -1 && host.includes(':', i + 1) && host.charCodeAt(0) !== 91 /* [ */) { | |
| 135 | host = `[${host}]` | |
| 136 | } | |
| 137 | ||
| 138 | if (port && +port !== defaultPort) { | |
| 139 | host += ':' + port | |
| 140 | } | |
| 141 | ||
| 142 | return host | |
| 143 | } |
| … | … | @@ -0,0 +1,86 @@ |
| 1 | declare const constants: { | |
| 2 | method: { | |
| 3 | GET: 'GET' | |
| 4 | HEAD: 'HEAD' | |
| 5 | POST: 'POST' | |
| 6 | PUT: 'PUT' | |
| 7 | DELETE: 'DELETE' | |
| 8 | CONNECT: 'CONNECT' | |
| 9 | OPTIONS: 'OPTIONS' | |
| 10 | TRACE: 'TRACE' | |
| 11 | PATCH: 'PATCH' | |
| 12 | } | |
| 13 | status: { | |
| 14 | 100: 'Continue' | |
| 15 | 101: 'Switching Protocols' | |
| 16 | 102: 'Processing' | |
| 17 | 103: 'Early Hints' | |
| 18 | 200: 'OK' | |
| 19 | 201: 'Created' | |
| 20 | 202: 'Accepted' | |
| 21 | 203: 'Non-Authoritative Information' | |
| 22 | 204: 'No Content' | |
| 23 | 205: 'Reset Content' | |
| 24 | 206: 'Partial Content' | |
| 25 | 207: 'Multi-Status' | |
| 26 | 208: 'Already Reported' | |
| 27 | 226: 'IM Used' | |
| 28 | 300: 'Multiple Choices' | |
| 29 | 301: 'Moved Permanently' | |
| 30 | 302: 'Found' | |
| 31 | 303: 'See Other' | |
| 32 | 304: 'Not Modified' | |
| 33 | 305: 'Use Proxy' | |
| 34 | 307: 'Temporary Redirect' | |
| 35 | 308: 'Permanent Redirect' | |
| 36 | 400: 'Bad Request' | |
| 37 | 401: 'Unauthorized' | |
| 38 | 402: 'Payment Required' | |
| 39 | 403: 'Forbidden' | |
| 40 | 404: 'Not Found' | |
| 41 | 405: 'Method Not Allowed' | |
| 42 | 406: 'Not Acceptable' | |
| 43 | 407: 'Proxy Authentication Required' | |
| 44 | 408: 'Request Timeout' | |
| 45 | 409: 'Conflict' | |
| 46 | 410: 'Gone' | |
| 47 | 411: 'Length Required' | |
| 48 | 412: 'Precondition Failed' | |
| 49 | 413: 'Payload Too Large' | |
| 50 | 414: 'URI Too Long' | |
| 51 | 415: 'Unsupported Media Type' | |
| 52 | 416: 'Range Not Satisfiable' | |
| 53 | 417: 'Expectation Failed' | |
| 54 | 418: "I'm a Teapot" | |
| 55 | 421: 'Misdirected Request' | |
| 56 | 422: 'Unprocessable Entity' | |
| 57 | 423: 'Locked' | |
| 58 | 424: 'Failed Dependency' | |
| 59 | 425: 'Too Early' | |
| 60 | 426: 'Upgrade Required' | |
| 61 | 428: 'Precondition Required' | |
| 62 | 429: 'Too Many Requests' | |
| 63 | 431: 'Request Header Fields Too Large' | |
| 64 | 451: 'Unavailable For Legal Reasons' | |
| 65 | 500: 'Internal Server Error' | |
| 66 | 501: 'Not Implemented' | |
| 67 | 502: 'Bad Gateway' | |
| 68 | 503: 'Service Unavailable' | |
| 69 | 504: 'Gateway Timeout' | |
| 70 | 505: 'HTTP Version Not Supported' | |
| 71 | 506: 'Variant Also Negotiates' | |
| 72 | 507: 'Insufficient Storage' | |
| 73 | 508: 'Loop Detected' | |
| 74 | 509: 'Bandwidth Limit Exceeded' | |
| 75 | 510: 'Not Extended' | |
| 76 | 511: 'Network Authentication Required' | |
| 77 | } | |
| 78 | } | |
| 79 | ||
| 80 | declare namespace constants { | |
| 81 | export type HTTPMethod = keyof typeof constants.method | |
| 82 | export type HTTPStatusCode = keyof typeof constants.status | |
| 83 | export type HTTPStatusMessage = (typeof constants.status)[HTTPStatusCode] | |
| 84 | } | |
| 85 | ||
| 86 | export = constants |
| … | … | @@ -0,0 +1,78 @@ |
| 1 | module.exports = { | |
| 2 | method: { | |
| 3 | GET: 'GET', | |
| 4 | HEAD: 'HEAD', | |
| 5 | POST: 'POST', | |
| 6 | PUT: 'PUT', | |
| 7 | DELETE: 'DELETE', | |
| 8 | CONNECT: 'CONNECT', | |
| 9 | OPTIONS: 'OPTIONS', | |
| 10 | TRACE: 'TRACE', | |
| 11 | PATCH: 'PATCH' | |
| 12 | }, | |
| 13 | status: { | |
| 14 | 100: 'Continue', | |
| 15 | 101: 'Switching Protocols', | |
| 16 | 102: 'Processing', | |
| 17 | 103: 'Early Hints', | |
| 18 | 200: 'OK', | |
| 19 | 201: 'Created', | |
| 20 | 202: 'Accepted', | |
| 21 | 203: 'Non-Authoritative Information', | |
| 22 | 204: 'No Content', | |
| 23 | 205: 'Reset Content', | |
| 24 | 206: 'Partial Content', | |
| 25 | 207: 'Multi-Status', | |
| 26 | 208: 'Already Reported', | |
| 27 | 226: 'IM Used', | |
| 28 | 300: 'Multiple Choices', | |
| 29 | 301: 'Moved Permanently', | |
| 30 | 302: 'Found', | |
| 31 | 303: 'See Other', | |
| 32 | 304: 'Not Modified', | |
| 33 | 305: 'Use Proxy', | |
| 34 | 307: 'Temporary Redirect', | |
| 35 | 308: 'Permanent Redirect', | |
| 36 | 400: 'Bad Request', | |
| 37 | 401: 'Unauthorized', | |
| 38 | 402: 'Payment Required', | |
| 39 | 403: 'Forbidden', | |
| 40 | 404: 'Not Found', | |
| 41 | 405: 'Method Not Allowed', | |
| 42 | 406: 'Not Acceptable', | |
| 43 | 407: 'Proxy Authentication Required', | |
| 44 | 408: 'Request Timeout', | |
| 45 | 409: 'Conflict', | |
| 46 | 410: 'Gone', | |
| 47 | 411: 'Length Required', | |
| 48 | 412: 'Precondition Failed', | |
| 49 | 413: 'Payload Too Large', | |
| 50 | 414: 'URI Too Long', | |
| 51 | 415: 'Unsupported Media Type', | |
| 52 | 416: 'Range Not Satisfiable', | |
| 53 | 417: 'Expectation Failed', | |
| 54 | 418: "I'm a Teapot", | |
| 55 | 421: 'Misdirected Request', | |
| 56 | 422: 'Unprocessable Entity', | |
| 57 | 423: 'Locked', | |
| 58 | 424: 'Failed Dependency', | |
| 59 | 425: 'Too Early', | |
| 60 | 426: 'Upgrade Required', | |
| 61 | 428: 'Precondition Required', | |
| 62 | 429: 'Too Many Requests', | |
| 63 | 431: 'Request Header Fields Too Large', | |
| 64 | 451: 'Unavailable For Legal Reasons', | |
| 65 | 500: 'Internal Server Error', | |
| 66 | 501: 'Not Implemented', | |
| 67 | 502: 'Bad Gateway', | |
| 68 | 503: 'Service Unavailable', | |
| 69 | 504: 'Gateway Timeout', | |
| 70 | 505: 'HTTP Version Not Supported', | |
| 71 | 506: 'Variant Also Negotiates', | |
| 72 | 507: 'Insufficient Storage', | |
| 73 | 508: 'Loop Detected', | |
| 74 | 509: 'Bandwidth Limit Exceeded', | |
| 75 | 510: 'Not Extended', | |
| 76 | 511: 'Network Authentication Required' | |
| 77 | } | |
| 78 | } |
| … | … | @@ -0,0 +1,5 @@ |
| 1 | declare class HTTPError extends Error { | |
| 2 | readonly code: string | |
| 3 | } | |
| 4 | ||
| 5 | export = HTTPError |
| … | … | @@ -0,0 +1,26 @@ |
| 1 | module.exports = class HTTPError extends Error { | |
| 2 | constructor(msg, fn = HTTPError, code = fn.name) { | |
| 3 | super(`${code}: ${msg}`) | |
| 4 | this.code = code | |
| 5 | ||
| 6 | if (Error.captureStackTrace) { | |
| 7 | Error.captureStackTrace(this, fn) | |
| 8 | } | |
| 9 | } | |
| 10 | ||
| 11 | get name() { | |
| 12 | return 'HTTPError' | |
| 13 | } | |
| 14 | ||
| 15 | static NOT_IMPLEMENTED(msg = 'Method not implemented') { | |
| 16 | return new HTTPError(msg, HTTPError.NOT_IMPLEMENTED) | |
| 17 | } | |
| 18 | ||
| 19 | static CONNECTION_LOST(msg = 'Socket hung up') { | |
| 20 | return new HTTPError(msg, HTTPError.CONNECTION_LOST) | |
| 21 | } | |
| 22 | ||
| 23 | static AGENT_SUSPENDED(msg = 'Agent is suspended') { | |
| 24 | return new HTTPError(msg, HTTPError.AGENT_SUSPENDED) | |
| 25 | } | |
| 26 | } |
| … | … | @@ -0,0 +1,95 @@ |
| 1 | const { Readable } = require('bare-stream') | |
| 2 | ||
| 3 | module.exports = class HTTPIncomingMessage extends Readable { | |
| 4 | constructor(socket = null, opts = {}) { | |
| 5 | super() | |
| 6 | ||
| 7 | this._socket = socket | |
| 8 | this._upgrade = false | |
| 9 | this._headers = opts.headers || {} | |
| 10 | ||
| 11 | // Server options | |
| 12 | this._method = opts.method || '' | |
| 13 | this._url = opts.url || '' | |
| 14 | ||
| 15 | // Client options | |
| 16 | this._statusCode = opts.statusCode || 0 | |
| 17 | this._statusMessage = opts.statusMessage || '' | |
| 18 | } | |
| 19 | ||
| 20 | get socket() { | |
| 21 | return this._socket | |
| 22 | } | |
| 23 | ||
| 24 | get upgrade() { | |
| 25 | return this._upgrade | |
| 26 | } | |
| 27 | ||
| 28 | get headers() { | |
| 29 | return this._headers | |
| 30 | } | |
| 31 | ||
| 32 | set headers(value) { | |
| 33 | this._headers = value | |
| 34 | } | |
| 35 | ||
| 36 | get method() { | |
| 37 | return this._method | |
| 38 | } | |
| 39 | ||
| 40 | set method(value) { | |
| 41 | this._method = value | |
| 42 | } | |
| 43 | ||
| 44 | get url() { | |
| 45 | return this._url | |
| 46 | } | |
| 47 | ||
| 48 | set url(value) { | |
| 49 | this._url = value | |
| 50 | } | |
| 51 | ||
| 52 | get statusCode() { | |
| 53 | return this._statusCode | |
| 54 | } | |
| 55 | ||
| 56 | set statusCode(value) { | |
| 57 | this._statusCode = value | |
| 58 | } | |
| 59 | ||
| 60 | get statusMessage() { | |
| 61 | return this._statusMessage | |
| 62 | } | |
| 63 | ||
| 64 | set statusMessage(value) { | |
| 65 | this._statusMessage = value | |
| 66 | } | |
| 67 | ||
| 68 | get httpVersion() { | |
| 69 | return '1.1' | |
| 70 | } | |
| 71 | ||
| 72 | getHeader(name) { | |
| 73 | return this._headers[name.toLowerCase()] | |
| 74 | } | |
| 75 | ||
| 76 | getHeaders() { | |
| 77 | return { ...this._headers } | |
| 78 | } | |
| 79 | ||
| 80 | hasHeader(name) { | |
| 81 | return name.toLowerCase() in this._headers | |
| 82 | } | |
| 83 | ||
| 84 | setTimeout(ms, ontimeout) { | |
| 85 | if (ontimeout) this.once('timeout', ontimeout) | |
| 86 | ||
| 87 | this._socket.setTimeout(ms) | |
| 88 | ||
| 89 | return this | |
| 90 | } | |
| 91 | ||
| 92 | _predestroy() { | |
| 93 | if (this._upgrade === false && this._socket !== null) this._socket.destroy() | |
| 94 | } | |
| 95 | } |
| … | … | @@ -0,0 +1,72 @@ |
| 1 | const { Writable } = require('bare-stream') | |
| 2 | const errors = require('./errors') | |
| 3 | ||
| 4 | module.exports = class HTTPOutgoingMessage extends Writable { | |
| 5 | constructor(socket = null) { | |
| 6 | super() | |
| 7 | ||
| 8 | this._socket = socket | |
| 9 | this._upgrade = false | |
| 10 | this._headersSent = false | |
| 11 | this._headers = {} | |
| 12 | } | |
| 13 | ||
| 14 | get socket() { | |
| 15 | return this._socket | |
| 16 | } | |
| 17 | ||
| 18 | get upgrade() { | |
| 19 | return this._upgrade | |
| 20 | } | |
| 21 | ||
| 22 | get headersSent() { | |
| 23 | return this._headersSent | |
| 24 | } | |
| 25 | ||
| 26 | get headers() { | |
| 27 | return this._headers | |
| 28 | } | |
| 29 | ||
| 30 | set headers(value) { | |
| 31 | this._headers = value | |
| 32 | } | |
| 33 | ||
| 34 | getHeader(name) { | |
| 35 | return this._headers[name.toLowerCase()] | |
| 36 | } | |
| 37 | ||
| 38 | getHeaders() { | |
| 39 | return { ...this._headers } | |
| 40 | } | |
| 41 | ||
| 42 | hasHeader(name) { | |
| 43 | return name.toLowerCase() in this._headers | |
| 44 | } | |
| 45 | ||
| 46 | setHeader(name, value) { | |
| 47 | this._headers[name.toLowerCase()] = value | |
| 48 | } | |
| 49 | ||
| 50 | flushHeaders() { | |
| 51 | if (this._headersSent === true || this._socket === null) return | |
| 52 | ||
| 53 | this._socket.write(Buffer.from(this._header())) | |
| 54 | this._headersSent = true | |
| 55 | } | |
| 56 | ||
| 57 | setTimeout(ms, ontimeout) { | |
| 58 | if (ontimeout) this.once('timeout', ontimeout) | |
| 59 | ||
| 60 | this._socket.setTimeout(ms) | |
| 61 | ||
| 62 | return this | |
| 63 | } | |
| 64 | ||
| 65 | _header() { | |
| 66 | throw errors.NOT_IMPLEMENTED() | |
| 67 | } | |
| 68 | ||
| 69 | _predestroy() { | |
| 70 | if (this._upgrade === false && this._socket !== null) this._socket.destroy() | |
| 71 | } | |
| 72 | } |
| … | … | @@ -0,0 +1,172 @@ |
| 1 | const { isEnded, isFinished, getStreamError } = require('bare-stream') | |
| 2 | const HTTPParser = require('bare-http-parser') | |
| 3 | const HTTPIncomingMessage = require('./incoming-message') | |
| 4 | const HTTPServerResponse = require('./server-response') | |
| 5 | ||
| 6 | const { | |
| 7 | constants: { REQUEST, DATA, END } | |
| 8 | } = HTTPParser | |
| 9 | ||
| 10 | const EMPTY = Buffer.alloc(0) | |
| 11 | ||
| 12 | module.exports = class HTTPServerConnection { | |
| 13 | static _connections = new WeakMap() | |
| 14 | ||
| 15 | static for(socket) { | |
| 16 | return this._connections.get(socket) || null | |
| 17 | } | |
| 18 | ||
| 19 | constructor(server, socket, opts = {}) { | |
| 20 | const { IncomingMessage = HTTPIncomingMessage, ServerResponse = HTTPServerResponse } = opts | |
| 21 | ||
| 22 | this._server = server | |
| 23 | this._socket = socket | |
| 24 | ||
| 25 | this._req = null | |
| 26 | this._res = null | |
| 27 | ||
| 28 | this._IncomingMessage = IncomingMessage | |
| 29 | this._ServerResponse = ServerResponse | |
| 30 | ||
| 31 | this._parser = new HTTPParser() | |
| 32 | this._idle = true | |
| 33 | ||
| 34 | this._onclose = this._onclose.bind(this) | |
| 35 | this._ondata = this._ondata.bind(this) | |
| 36 | this._ondrain = this._ondrain.bind(this) | |
| 37 | this._ontimeout = this._ontimeout.bind(this) | |
| 38 | ||
| 39 | socket | |
| 40 | .on('error', noop) | |
| 41 | .on('close', this._onclose) | |
| 42 | .on('data', this._ondata) | |
| 43 | .on('drain', this._ondrain) | |
| 44 | .on('timeout', this._ontimeout) | |
| 45 | ||
| 46 | HTTPServerConnection._connections.set(socket, this) | |
| 47 | ||
| 48 | if (this._server.timeout) socket.setTimeout(this._server.timeout) | |
| 49 | } | |
| 50 | ||
| 51 | get server() { | |
| 52 | return this._server | |
| 53 | } | |
| 54 | ||
| 55 | get socket() { | |
| 56 | return this._socket | |
| 57 | } | |
| 58 | ||
| 59 | get req() { | |
| 60 | return this._req | |
| 61 | } | |
| 62 | ||
| 63 | get res() { | |
| 64 | return this._res | |
| 65 | } | |
| 66 | ||
| 67 | get idle() { | |
| 68 | return this._idle | |
| 69 | } | |
| 70 | ||
| 71 | _onclose() { | |
| 72 | if (this._req && !isEnded(this._req)) this._req.destroy() | |
| 73 | if (this._res && !isFinished(this._res)) this._res.destroy() | |
| 74 | ||
| 75 | const err = getStreamError(this._socket) | |
| 76 | if (err) this._socket.destroy(err) | |
| 77 | } | |
| 78 | ||
| 79 | _ondata(data) { | |
| 80 | this._idle = false | |
| 81 | ||
| 82 | try { | |
| 83 | for (const op of this._parser.push(data)) { | |
| 84 | switch (op.type) { | |
| 85 | case REQUEST: | |
| 86 | this._req = new this._IncomingMessage(this._socket, { | |
| 87 | headers: op.headers, | |
| 88 | method: op.method, | |
| 89 | url: op.url | |
| 90 | }) | |
| 91 | ||
| 92 | this._req.on('close', () => { | |
| 93 | this._req = null | |
| 94 | ||
| 95 | this._idle = true | |
| 96 | ||
| 97 | if (this._server.closing) this._socket.destroy() | |
| 98 | }) | |
| 99 | ||
| 100 | // Eagerly open the request stream | |
| 101 | this._req.resume() | |
| 102 | this._req.pause() | |
| 103 | ||
| 104 | if (op.headers.connection && op.headers.connection.toLowerCase() === 'upgrade') { | |
| 105 | return this._onupgrade(this._parser.end()) | |
| 106 | } | |
| 107 | ||
| 108 | this._res = new this._ServerResponse(this._socket, this._req) | |
| 109 | ||
| 110 | this._res.on('close', () => { | |
| 111 | this._res = null | |
| 112 | }) | |
| 113 | ||
| 114 | this._server.emit('request', this._req, this._res) | |
| 115 | break | |
| 116 | ||
| 117 | case DATA: | |
| 118 | this._req.push(op.data) | |
| 119 | break | |
| 120 | ||
| 121 | case END: | |
| 122 | if (this._req) { | |
| 123 | this._req._socket = null | |
| 124 | this._req.push(null) | |
| 125 | } | |
| 126 | break | |
| 127 | } | |
| 128 | } | |
| 129 | } catch (err) { | |
| 130 | this._socket.destroy(err) | |
| 131 | } | |
| 132 | } | |
| 133 | ||
| 134 | _onupgrade(data) { | |
| 135 | this._detach() | |
| 136 | ||
| 137 | const req = this._req | |
| 138 | ||
| 139 | req._upgrade = true | |
| 140 | ||
| 141 | const upgraded = this._server.emit('upgrade', req, this._socket, data || EMPTY) | |
| 142 | ||
| 143 | req.push(null) | |
| 144 | ||
| 145 | if (!upgraded) this._socket.destroy() | |
| 146 | } | |
| 147 | ||
| 148 | _ontimeout() { | |
| 149 | const reqTimeout = this._req && this._req.emit('timeout') | |
| 150 | const resTimeout = this._res && this._res.emit('timeout') | |
| 151 | const serverTimeout = this._server.emit('timeout', this._socket) | |
| 152 | ||
| 153 | if (!reqTimeout && !resTimeout && !serverTimeout) this._socket.destroy() | |
| 154 | } | |
| 155 | ||
| 156 | _ondrain() { | |
| 157 | if (this._res) this._res._continueWrite() | |
| 158 | } | |
| 159 | ||
| 160 | _detach() { | |
| 161 | this._socket | |
| 162 | .off('error', noop) | |
| 163 | .off('close', this._onclose) | |
| 164 | .off('data', this._ondata) | |
| 165 | .off('drain', this._ondrain) | |
| 166 | .off('timeout', this._ontimeout) | |
| 167 | ||
| 168 | HTTPServerConnection._connections.delete(this._socket) | |
| 169 | } | |
| 170 | } | |
| 171 | ||
| 172 | function noop() {} |
| … | … | @@ -0,0 +1,143 @@ |
| 1 | const { isFinishing } = require('bare-stream') | |
| 2 | const HTTPOutgoingMessage = require('./outgoing-message') | |
| 3 | const constants = require('./constants') | |
| 4 | ||
| 5 | const CHUNK_DELIMITER = Buffer.from('\r\n') | |
| 6 | const CHUNK_TERMINATOR = Buffer.from('0\r\n\r\n') | |
| 7 | ||
| 8 | module.exports = class HTTPServerResponse extends HTTPOutgoingMessage { | |
| 9 | constructor(socket, req) { | |
| 10 | super(socket) | |
| 11 | ||
| 12 | this._req = req | |
| 13 | ||
| 14 | this._statusCode = 200 | |
| 15 | this._statusMessage = null | |
| 16 | ||
| 17 | this._chunked = true | |
| 18 | this._close = req.headers.connection === 'close' | |
| 19 | this._onlyHeaders = req.method === 'HEAD' | |
| 20 | ||
| 21 | this._pendingWrite = null | |
| 22 | } | |
| 23 | ||
| 24 | get req() { | |
| 25 | return this._req | |
| 26 | } | |
| 27 | ||
| 28 | get statusCode() { | |
| 29 | return this._statusCode | |
| 30 | } | |
| 31 | ||
| 32 | set statusCode(value) { | |
| 33 | this._statusCode = value | |
| 34 | } | |
| 35 | ||
| 36 | get statusMessage() { | |
| 37 | return this._statusMessage | |
| 38 | } | |
| 39 | ||
| 40 | set statusMessage(value) { | |
| 41 | this._statusMessage = value | |
| 42 | } | |
| 43 | ||
| 44 | writeHead(statusCode, statusMessage = null, headers = {}) { | |
| 45 | if (typeof statusMessage === 'object' && statusMessage !== null) { | |
| 46 | headers = statusMessage | |
| 47 | statusMessage = null | |
| 48 | } | |
| 49 | ||
| 50 | this._statusCode = statusCode | |
| 51 | this._statusMessage = statusMessage || null | |
| 52 | ||
| 53 | if (headers) this._headers = { ...this._headers, ...headers } | |
| 54 | } | |
| 55 | ||
| 56 | _header() { | |
| 57 | let h = | |
| 58 | 'HTTP/1.1 ' + | |
| 59 | this._statusCode + | |
| 60 | ' ' + | |
| 61 | (this._statusMessage === null ? constants.status[this._statusCode] : this._statusMessage) + | |
| 62 | '\r\n' | |
| 63 | ||
| 64 | for (const name of Object.keys(this._headers)) { | |
| 65 | const n = name.toLowerCase() | |
| 66 | const v = this._headers[name] | |
| 67 | ||
| 68 | if (n === 'content-length') this._chunked = false | |
| 69 | if (n === 'connection' && v && v.toLowerCase() === 'close') this._close = true | |
| 70 | ||
| 71 | h += httpCase(n) + ': ' + v + '\r\n' | |
| 72 | } | |
| 73 | ||
| 74 | if (this._chunked) h += 'Transfer-Encoding: chunked\r\n' | |
| 75 | ||
| 76 | h += '\r\n' | |
| 77 | ||
| 78 | return h | |
| 79 | } | |
| 80 | ||
| 81 | _write(data, encoding, cb) { | |
| 82 | if (this._headersSent === false) { | |
| 83 | if (isFinishing(this)) { | |
| 84 | this.setHeader( | |
| 85 | 'Content-Length', | |
| 86 | (data.byteLength + this._writableState.buffered).toString() | |
| 87 | ) | |
| 88 | } | |
| 89 | ||
| 90 | this.flushHeaders() | |
| 91 | } | |
| 92 | ||
| 93 | if (this._onlyHeaders === true) return cb(null) | |
| 94 | ||
| 95 | if (this._chunked) { | |
| 96 | this._socket.write(Buffer.from(data.byteLength.toString(16))) | |
| 97 | this._socket.write(CHUNK_DELIMITER) | |
| 98 | } | |
| 99 | ||
| 100 | let flushed = this._socket.write(data) | |
| 101 | ||
| 102 | if (this._chunked) flushed = this._socket.write(CHUNK_DELIMITER) | |
| 103 | ||
| 104 | if (flushed) cb(null) | |
| 105 | else this._pendingWrite = cb | |
| 106 | } | |
| 107 | ||
| 108 | _final(cb) { | |
| 109 | if (this._headersSent === false) { | |
| 110 | this.setHeader('Content-Length', '0') | |
| 111 | this.flushHeaders() | |
| 112 | } | |
| 113 | ||
| 114 | if (this._chunked && this._onlyHeaders === false) this._socket.write(CHUNK_TERMINATOR) | |
| 115 | ||
| 116 | if (this._close) this._socket.end() | |
| 117 | ||
| 118 | cb(null) | |
| 119 | } | |
| 120 | ||
| 121 | _predestroy() { | |
| 122 | super._predestroy() | |
| 123 | ||
| 124 | this._req.destroy() | |
| 125 | ||
| 126 | this._continueWrite() | |
| 127 | } | |
| 128 | ||
| 129 | _continueWrite() { | |
| 130 | if (this._pendingWrite === null) return | |
| 131 | const cb = this._pendingWrite | |
| 132 | this._pendingWrite = null | |
| 133 | cb(null) | |
| 134 | } | |
| 135 | } | |
| 136 | ||
| 137 | function httpCase(n) { | |
| 138 | let s = '' | |
| 139 | for (const part of n.split('-')) { | |
| 140 | s += (s ? '-' : '') + part.slice(0, 1).toUpperCase() + part.slice(1) | |
| 141 | } | |
| 142 | return s | |
| 143 | } |
| … | … | @@ -0,0 +1,47 @@ |
| 1 | const TCPServer = require('bare-tcp').Server | |
| 2 | const HTTPServerConnection = require('./server-connection') | |
| 3 | ||
| 4 | module.exports = class HTTPServer extends TCPServer { | |
| 5 | constructor(opts = {}, onrequest) { | |
| 6 | if (typeof opts === 'function') { | |
| 7 | onrequest = opts | |
| 8 | opts = {} | |
| 9 | } | |
| 10 | ||
| 11 | super({ allowHalfOpen: false }) | |
| 12 | ||
| 13 | this._timeout = 0 | |
| 14 | ||
| 15 | this.on('connection', (socket) => { | |
| 16 | new HTTPServerConnection(this, socket, opts) | |
| 17 | }) | |
| 18 | ||
| 19 | if (onrequest) this.on('request', onrequest) | |
| 20 | } | |
| 21 | ||
| 22 | get timeout() { | |
| 23 | return this._timeout || undefined // For Node.js compatibility | |
| 24 | } | |
| 25 | ||
| 26 | setTimeout(ms = 0, ontimeout) { | |
| 27 | if (ontimeout) this.on('timeout', ontimeout) | |
| 28 | ||
| 29 | this._timeout = ms | |
| 30 | ||
| 31 | return this | |
| 32 | } | |
| 33 | ||
| 34 | close(onclose) { | |
| 35 | super.close(onclose) | |
| 36 | ||
| 37 | for (const socket of this.connections) { | |
| 38 | const connection = HTTPServerConnection.for(socket) | |
| 39 | ||
| 40 | if (connection === null || connection.idle) { | |
| 41 | socket.destroy() | |
| 42 | } | |
| 43 | } | |
| 44 | ||
| 45 | return this | |
| 46 | } | |
| 47 | } |
| … | … | @@ -0,0 +1,63 @@ |
| 1 | { | |
| 2 | "name": "bare-http1", | |
| 3 | "version": "4.5.5", | |
| 4 | "description": "Native HTTP/1 library for JavaScript", | |
| 5 | "exports": { | |
| 6 | "./package": "./package.json", | |
| 7 | ".": { | |
| 8 | "types": "./index.d.ts", | |
| 9 | "default": "./index.js" | |
| 10 | }, | |
| 11 | "./constants": { | |
| 12 | "types": "./lib/constants.d.ts", | |
| 13 | "default": "./lib/constants.js" | |
| 14 | }, | |
| 15 | "./errors": { | |
| 16 | "types": "./lib/errors.d.ts", | |
| 17 | "default": "./lib/errors.js" | |
| 18 | } | |
| 19 | }, | |
| 20 | "files": [ | |
| 21 | "index.js", | |
| 22 | "index.d.ts", | |
| 23 | "lib" | |
| 24 | ], | |
| 25 | "scripts": { | |
| 26 | "test": "prettier . --check && bare test.js" | |
| 27 | }, | |
| 28 | "repository": { | |
| 29 | "type": "git", | |
| 30 | "url": "git+https://github.com/holepunchto/bare-http1.git" | |
| 31 | }, | |
| 32 | "author": "Holepunch", | |
| 33 | "license": "Apache-2.0", | |
| 34 | "bugs": { | |
| 35 | "url": "https://github.com/holepunchto/bare-http1/issues" | |
| 36 | }, | |
| 37 | "homepage": "https://github.com/holepunchto/bare-http1#readme", | |
| 38 | "dependencies": { | |
| 39 | "bare-events": "^2.6.0", | |
| 40 | "bare-http-parser": "^1.1.1", | |
| 41 | "bare-stream": "^2.10.0", | |
| 42 | "bare-tcp": "^2.2.0" | |
| 43 | }, | |
| 44 | "devDependencies": { | |
| 45 | "bare-buffer": "^3.0.2", | |
| 46 | "bare-url": "^2.1.3", | |
| 47 | "brittle": "^3.3.0", | |
| 48 | "prettier": "^3.4.1", | |
| 49 | "prettier-config-holepunch": "^2.0.0" | |
| 50 | }, | |
| 51 | "peerDependencies": { | |
| 52 | "bare-buffer": "*", | |
| 53 | "bare-url": "*" | |
| 54 | }, | |
| 55 | "peerDependenciesMeta": { | |
| 56 | "bare-buffer": { | |
| 57 | "optional": true | |
| 58 | }, | |
| 59 | "bare-url": { | |
| 60 | "optional": true | |
| 61 | } | |
| 62 | } | |
| 63 | } |
| … | … | @@ -0,0 +1,201 @@ |
| 1 | Apache License | |
| 2 | Version 2.0, January 2004 | |
| 3 | http://www.apache.org/licenses/ | |
| 4 | ||
| 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | |
| 6 | ||
| 7 | 1. Definitions. | |
| 8 | ||
| 9 | "License" shall mean the terms and conditions for use, reproduction, | |
| 10 | and distribution as defined by Sections 1 through 9 of this document. | |
| 11 | ||
| 12 | "Licensor" shall mean the copyright owner or entity authorized by | |
| 13 | the copyright owner that is granting the License. | |
| 14 | ||
| 15 | "Legal Entity" shall mean the union of the acting entity and all | |
| 16 | other entities that control, are controlled by, or are under common | |
| 17 | control with that entity. For the purposes of this definition, | |
| 18 | "control" means (i) the power, direct or indirect, to cause the | |
| 19 | direction or management of such entity, whether by contract or | |
| 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the | |
| 21 | outstanding shares, or (iii) beneficial ownership of such entity. | |
| 22 | ||
| 23 | "You" (or "Your") shall mean an individual or Legal Entity | |
| 24 | exercising permissions granted by this License. | |
| 25 | ||
| 26 | "Source" form shall mean the preferred form for making modifications, | |
| 27 | including but not limited to software source code, documentation | |
| 28 | source, and configuration files. | |
| 29 | ||
| 30 | "Object" form shall mean any form resulting from mechanical | |
| 31 | transformation or translation of a Source form, including but | |
| 32 | not limited to compiled object code, generated documentation, | |
| 33 | and conversions to other media types. | |
| 34 | ||
| 35 | "Work" shall mean the work of authorship, whether in Source or | |
| 36 | Object form, made available under the License, as indicated by a | |
| 37 | copyright notice that is included in or attached to the work | |
| 38 | (an example is provided in the Appendix below). | |
| 39 | ||
| 40 | "Derivative Works" shall mean any work, whether in Source or Object | |
| 41 | form, that is based on (or derived from) the Work and for which the | |
| 42 | editorial revisions, annotations, elaborations, or other modifications | |
| 43 | represent, as a whole, an original work of authorship. For the purposes | |
| 44 | of this License, Derivative Works shall not include works that remain | |
| 45 | separable from, or merely link (or bind by name) to the interfaces of, | |
| 46 | the Work and Derivative Works thereof. | |
| 47 | ||
| 48 | "Contribution" shall mean any work of authorship, including | |
| 49 | the original version of the Work and any modifications or additions | |
| 50 | to that Work or Derivative Works thereof, that is intentionally | |
| 51 | submitted to Licensor for inclusion in the Work by the copyright owner | |
| 52 | or by an individual or Legal Entity authorized to submit on behalf of | |
| 53 | the copyright owner. For the purposes of this definition, "submitted" | |
| 54 | means any form of electronic, verbal, or written communication sent | |
| 55 | to the Licensor or its representatives, including but not limited to | |
| 56 | communication on electronic mailing lists, source code control systems, | |
| 57 | and issue tracking systems that are managed by, or on behalf of, the | |
| 58 | Licensor for the purpose of discussing and improving the Work, but | |
| 59 | excluding communication that is conspicuously marked or otherwise | |
| 60 | designated in writing by the copyright owner as "Not a Contribution." | |
| 61 | ||
| 62 | "Contributor" shall mean Licensor and any individual or Legal Entity | |
| 63 | on behalf of whom a Contribution has been received by Licensor and | |
| 64 | subsequently incorporated within the Work. | |
| 65 | ||
| 66 | 2. Grant of Copyright License. Subject to the terms and conditions of | |
| 67 | this License, each Contributor hereby grants to You a perpetual, | |
| 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 69 | copyright license to reproduce, prepare Derivative Works of, | |
| 70 | publicly display, publicly perform, sublicense, and distribute the | |
| 71 | Work and such Derivative Works in Source or Object form. | |
| 72 | ||
| 73 | 3. Grant of Patent License. Subject to the terms and conditions of | |
| 74 | this License, each Contributor hereby grants to You a perpetual, | |
| 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 76 | (except as stated in this section) patent license to make, have made, | |
| 77 | use, offer to sell, sell, import, and otherwise transfer the Work, | |
| 78 | where such license applies only to those patent claims licensable | |
| 79 | by such Contributor that are necessarily infringed by their | |
| 80 | Contribution(s) alone or by combination of their Contribution(s) | |
| 81 | with the Work to which such Contribution(s) was submitted. If You | |
| 82 | institute patent litigation against any entity (including a | |
| 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work | |
| 84 | or a Contribution incorporated within the Work constitutes direct | |
| 85 | or contributory patent infringement, then any patent licenses | |
| 86 | granted to You under this License for that Work shall terminate | |
| 87 | as of the date such litigation is filed. | |
| 88 | ||
| 89 | 4. Redistribution. You may reproduce and distribute copies of the | |
| 90 | Work or Derivative Works thereof in any medium, with or without | |
| 91 | modifications, and in Source or Object form, provided that You | |
| 92 | meet the following conditions: | |
| 93 | ||
| 94 | (a) You must give any other recipients of the Work or | |
| 95 | Derivative Works a copy of this License; and | |
| 96 | ||
| 97 | (b) You must cause any modified files to carry prominent notices | |
| 98 | stating that You changed the files; and | |
| 99 | ||
| 100 | (c) You must retain, in the Source form of any Derivative Works | |
| 101 | that You distribute, all copyright, patent, trademark, and | |
| 102 | attribution notices from the Source form of the Work, | |
| 103 | excluding those notices that do not pertain to any part of | |
| 104 | the Derivative Works; and | |
| 105 | ||
| 106 | (d) If the Work includes a "NOTICE" text file as part of its | |
| 107 | distribution, then any Derivative Works that You distribute must | |
| 108 | include a readable copy of the attribution notices contained | |
| 109 | within such NOTICE file, excluding those notices that do not | |
| 110 | pertain to any part of the Derivative Works, in at least one | |
| 111 | of the following places: within a NOTICE text file distributed | |
| 112 | as part of the Derivative Works; within the Source form or | |
| 113 | documentation, if provided along with the Derivative Works; or, | |
| 114 | within a display generated by the Derivative Works, if and | |
| 115 | wherever such third-party notices normally appear. The contents | |
| 116 | of the NOTICE file are for informational purposes only and | |
| 117 | do not modify the License. You may add Your own attribution | |
| 118 | notices within Derivative Works that You distribute, alongside | |
| 119 | or as an addendum to the NOTICE text from the Work, provided | |
| 120 | that such additional attribution notices cannot be construed | |
| 121 | as modifying the License. | |
| 122 | ||
| 123 | You may add Your own copyright statement to Your modifications and | |
| 124 | may provide additional or different license terms and conditions | |
| 125 | for use, reproduction, or distribution of Your modifications, or | |
| 126 | for any such Derivative Works as a whole, provided Your use, | |
| 127 | reproduction, and distribution of the Work otherwise complies with | |
| 128 | the conditions stated in this License. | |
| 129 | ||
| 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, | |
| 131 | any Contribution intentionally submitted for inclusion in the Work | |
| 132 | by You to the Licensor shall be under the terms and conditions of | |
| 133 | this License, without any additional terms or conditions. | |
| 134 | Notwithstanding the above, nothing herein shall supersede or modify | |
| 135 | the terms of any separate license agreement you may have executed | |
| 136 | with Licensor regarding such Contributions. | |
| 137 | ||
| 138 | 6. Trademarks. This License does not grant permission to use the trade | |
| 139 | names, trademarks, service marks, or product names of the Licensor, | |
| 140 | except as required for reasonable and customary use in describing the | |
| 141 | origin of the Work and reproducing the content of the NOTICE file. | |
| 142 | ||
| 143 | 7. Disclaimer of Warranty. Unless required by applicable law or | |
| 144 | agreed to in writing, Licensor provides the Work (and each | |
| 145 | Contributor provides its Contributions) on an "AS IS" BASIS, | |
| 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |
| 147 | implied, including, without limitation, any warranties or conditions | |
| 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | |
| 149 | PARTICULAR PURPOSE. You are solely responsible for determining the | |
| 150 | appropriateness of using or redistributing the Work and assume any | |
| 151 | risks associated with Your exercise of permissions under this License. | |
| 152 | ||
| 153 | 8. Limitation of Liability. In no event and under no legal theory, | |
| 154 | whether in tort (including negligence), contract, or otherwise, | |
| 155 | unless required by applicable law (such as deliberate and grossly | |
| 156 | negligent acts) or agreed to in writing, shall any Contributor be | |
| 157 | liable to You for damages, including any direct, indirect, special, | |
| 158 | incidental, or consequential damages of any character arising as a | |
| 159 | result of this License or out of the use or inability to use the | |
| 160 | Work (including but not limited to damages for loss of goodwill, | |
| 161 | work stoppage, computer failure or malfunction, or any and all | |
| 162 | other commercial damages or losses), even if such Contributor | |
| 163 | has been advised of the possibility of such damages. | |
| 164 | ||
| 165 | 9. Accepting Warranty or Additional Liability. While redistributing | |
| 166 | the Work or Derivative Works thereof, You may choose to offer, | |
| 167 | and charge a fee for, acceptance of support, warranty, indemnity, | |
| 168 | or other liability obligations and/or rights consistent with this | |
| 169 | License. However, in accepting such obligations, You may act only | |
| 170 | on Your own behalf and on Your sole responsibility, not on behalf | |
| 171 | of any other Contributor, and only if You agree to indemnify, | |
| 172 | defend, and hold each Contributor harmless for any liability | |
| 173 | incurred by, or claims asserted against, such Contributor by reason | |
| 174 | of your accepting any such warranty or additional liability. | |
| 175 | ||
| 176 | END OF TERMS AND CONDITIONS | |
| 177 | ||
| 178 | APPENDIX: How to apply the Apache License to your work. | |
| 179 | ||
| 180 | To apply the Apache License to your work, attach the following | |
| 181 | boilerplate notice, with the fields enclosed by brackets "[]" | |
| 182 | replaced with your own identifying information. (Don't include | |
| 183 | the brackets!) The text should be enclosed in the appropriate | |
| 184 | comment syntax for the file format. We also recommend that a | |
| 185 | file or class name and description of purpose be included on the | |
| 186 | same "printed page" as the copyright notice for easier | |
| 187 | identification within third-party archives. | |
| 188 | ||
| 189 | Copyright [yyyy] [name of copyright owner] | |
| 190 | ||
| 191 | Licensed under the Apache License, Version 2.0 (the "License"); | |
| 192 | you may not use this file except in compliance with the License. | |
| 193 | You may obtain a copy of the License at | |
| 194 | ||
| 195 | http://www.apache.org/licenses/LICENSE-2.0 | |
| 196 | ||
| 197 | Unless required by applicable law or agreed to in writing, software | |
| 198 | distributed under the License is distributed on an "AS IS" BASIS, | |
| 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 200 | See the License for the specific language governing permissions and | |
| 201 | limitations under the License. |
| … | … | @@ -0,0 +1,39 @@ |
| 1 | # bare-https | |
| 2 | ||
| 3 | HTTPS library for JavaScript. | |
| 4 | ||
| 5 | ``` | |
| 6 | npm i bare-https | |
| 7 | ``` | |
| 8 | ||
| 9 | ## Usage | |
| 10 | ||
| 11 | ```js | |
| 12 | const https = require('bare-https') | |
| 13 | ||
| 14 | const options = { | |
| 15 | cert: fs.readFileSync('test/fixtures/cert.crt'), | |
| 16 | key: fs.readFileSync('test/fixtures/cert.key') | |
| 17 | } | |
| 18 | ||
| 19 | const server = https.createServer(options, (req, res) => { | |
| 20 | res.statusCode = 200 | |
| 21 | res.setHeader('Content-Length', 10) | |
| 22 | res.write('hello world!') | |
| 23 | res.end() | |
| 24 | }) | |
| 25 | ||
| 26 | server.listen(0, () => { | |
| 27 | const { port } = server.address() | |
| 28 | console.log('server is bound on', port) | |
| 29 | ||
| 30 | const client = https.request({ port }, (res) => { | |
| 31 | res.on('data', (data) => console.log(data.toString())) | |
| 32 | }) | |
| 33 | client.end() | |
| 34 | }) | |
| 35 | ``` | |
| 36 | ||
| 37 | ## License | |
| 38 | ||
| 39 | Apache-2.0 |
| … | … | @@ -0,0 +1,82 @@ |
| 1 | import { TLSSocket, TLSSocketOptions } from 'bare-tls' | |
| 2 | import { | |
| 3 | HTTPAgent, | |
| 4 | HTTPClientRequest, | |
| 5 | HTTPClientRequestOptions, | |
| 6 | HTTPIncomingMessage, | |
| 7 | HTTPServer, | |
| 8 | HTTPServerConnectionOptions, | |
| 9 | HTTPServerResponse | |
| 10 | } from 'bare-http1' | |
| 11 | import { TCPSocket, TCPSocketOptions, TCPSocketConnectOptions } from 'bare-tcp' | |
| 12 | ||
| 13 | export interface HTTPSSocketOptions | |
| 14 | extends TLSSocketOptions, TCPSocketOptions, TCPSocketConnectOptions {} | |
| 15 | ||
| 16 | export interface HTTPSSocket extends TLSSocket, TCPSocket {} | |
| 17 | ||
| 18 | class HTTPSSocket extends TLSSocket {} | |
| 19 | ||
| 20 | export interface HTTPSAgent extends HTTPAgent { | |
| 21 | createConnection(opts?: HTTPSSocketOptions): HTTPSSocket | |
| 22 | } | |
| 23 | ||
| 24 | class HTTPSAgent extends HTTPAgent { | |
| 25 | static global: HTTPSAgent | |
| 26 | } | |
| 27 | ||
| 28 | export const globalAgent: HTTPSAgent | |
| 29 | ||
| 30 | export { HTTPSAgent as Agent } | |
| 31 | ||
| 32 | export interface HTTPSServerOptions extends HTTPSSocketOptions, HTTPServerConnectionOptions {} | |
| 33 | ||
| 34 | class HTTPSServer extends HTTPServer { | |
| 35 | constructor( | |
| 36 | opts?: HTTPSServerOptions, | |
| 37 | onrequest?: (req: HTTPIncomingMessage, res: HTTPServerResponse) => void | |
| 38 | ) | |
| 39 | ||
| 40 | constructor(onrequest: (req: HTTPIncomingMessage, res: HTTPServerResponse) => void) | |
| 41 | } | |
| 42 | ||
| 43 | export { HTTPSServer as Server } | |
| 44 | ||
| 45 | export interface HTTPSClientRequestOptions extends HTTPClientRequestOptions { | |
| 46 | agent?: HTTPSAgent | false | |
| 47 | } | |
| 48 | ||
| 49 | export interface HTTPSClientRequest extends HTTPClientRequest {} | |
| 50 | ||
| 51 | class HTTPSClientRequest extends HTTPClientRequest { | |
| 52 | constructor(opts?: HTTPSClientRequestOptions, onresponse?: () => void) | |
| 53 | ||
| 54 | constructor(onresponse: () => void) | |
| 55 | } | |
| 56 | ||
| 57 | export { HTTPClientRequest as ClientRequest } | |
| 58 | ||
| 59 | export function createServer( | |
| 60 | opts?: HTTPSServerOptions, | |
| 61 | onrequest?: (req: HTTPIncomingMessage, res: HTTPServerResponse) => void | |
| 62 | ): HTTPSServer | |
| 63 | ||
| 64 | export function createServer( | |
| 65 | onrequest: (req: HTTPIncomingMessage, res: HTTPServerResponse) => void | |
| 66 | ): HTTPSServer | |
| 67 | ||
| 68 | export function request( | |
| 69 | url: URL | string, | |
| 70 | opts?: HTTPSClientRequestOptions, | |
| 71 | onresponse?: (res: HTTPIncomingMessage) => void | |
| 72 | ): HTTPSClientRequest | |
| 73 | ||
| 74 | export function request( | |
| 75 | url: URL | string, | |
| 76 | onresponse: (res: HTTPIncomingMessage) => void | |
| 77 | ): HTTPSClientRequest | |
| 78 | ||
| 79 | export function request( | |
| 80 | opts: HTTPSClientRequestOptions, | |
| 81 | onresponse?: (res: HTTPIncomingMessage) => void | |
| 82 | ): HTTPSClientRequest |
| … | … | @@ -0,0 +1,63 @@ |
| 1 | exports.Agent = require('./lib/agent') | |
| 2 | exports.globalAgent = exports.Agent.global | |
| 3 | ||
| 4 | exports.Server = require('./lib/server') | |
| 5 | ||
| 6 | exports.ClientRequest = require('./lib/client-request') | |
| 7 | ||
| 8 | exports.createServer = function createServer(opts, onrequest) { | |
| 9 | return new exports.Server(opts, onrequest) | |
| 10 | } | |
| 11 | ||
| 12 | exports.request = function request(url, opts, onresponse) { | |
| 13 | if (typeof opts === 'function') { | |
| 14 | onresponse = opts | |
| 15 | opts = {} | |
| 16 | } | |
| 17 | ||
| 18 | if (typeof url === 'string') url = new URL(url) | |
| 19 | ||
| 20 | if (isURL(url)) { | |
| 21 | opts = opts ? { ...url, ...opts } : { ...url } | |
| 22 | ||
| 23 | opts.host = url.hostname | |
| 24 | opts.path = url.pathname + url.search | |
| 25 | opts.port = url.port ? parseInt(url.port, 10) : defaultPort(url) | |
| 26 | } else { | |
| 27 | opts = url ? { ...url } : {} | |
| 28 | ||
| 29 | // For Node.js compatibility | |
| 30 | opts.host = opts.hostname || opts.host | |
| 31 | opts.port = typeof opts.port === 'string' ? parseInt(opts.port, 10) : opts.port | |
| 32 | } | |
| 33 | ||
| 34 | return new exports.ClientRequest(opts, onresponse) | |
| 35 | } | |
| 36 | ||
| 37 | // https://url.spec.whatwg.org/#default-port | |
| 38 | function defaultPort(url) { | |
| 39 | switch (url.protocol) { | |
| 40 | case 'ftp:': | |
| 41 | return 21 | |
| 42 | case 'http:': | |
| 43 | case 'ws:': | |
| 44 | return 80 | |
| 45 | case 'https:': | |
| 46 | case 'wss:': | |
| 47 | return 443 | |
| 48 | } | |
| 49 | ||
| 50 | return null | |
| 51 | } | |
| 52 | ||
| 53 | // https://url.spec.whatwg.org/#api | |
| 54 | function isURL(url) { | |
| 55 | return ( | |
| 56 | url !== null && | |
| 57 | typeof url === 'object' && | |
| 58 | typeof url.protocol === 'string' && | |
| 59 | typeof url.hostname === 'string' && | |
| 60 | typeof url.pathname === 'string' && | |
| 61 | typeof url.search === 'string' | |
| 62 | ) | |
| 63 | } |
| … | … | @@ -0,0 +1,16 @@ |
| 1 | const HTTPAgent = require('bare-http1').Agent | |
| 2 | const HTTPSSocket = require('./socket') | |
| 3 | ||
| 4 | class HTTPSAgent extends HTTPAgent { | |
| 5 | constructor(opts = {}) { | |
| 6 | super({ defaultPort: 443, ...opts }) | |
| 7 | } | |
| 8 | ||
| 9 | createConnection(opts) { | |
| 10 | return new HTTPSSocket(super.createConnection(opts), opts) | |
| 11 | } | |
| 12 | } | |
| 13 | ||
| 14 | HTTPSAgent.global = new HTTPSAgent({ keepAlive: 1000, timeout: 5000 }) | |
| 15 | ||
| 16 | module.exports = HTTPSAgent |
| … | … | @@ -0,0 +1,17 @@ |
| 1 | const HTTPClientRequest = require('bare-http1').ClientRequest | |
| 2 | const HTTPSAgent = require('./agent') | |
| 3 | ||
| 4 | module.exports = class HTTPSClientRequest extends HTTPClientRequest { | |
| 5 | constructor(opts = {}, onresponse = null) { | |
| 6 | if (typeof opts === 'function') { | |
| 7 | onresponse = opts | |
| 8 | opts = {} | |
| 9 | } | |
| 10 | ||
| 11 | opts = opts ? { ...opts } : {} | |
| 12 | ||
| 13 | opts.agent = opts.agent === false ? new HTTPSAgent() : opts.agent || HTTPSAgent.global | |
| 14 | ||
| 15 | super(opts, onresponse) | |
| 16 | } | |
| 17 | } |
| … | … | @@ -0,0 +1,48 @@ |
| 1 | const TCPServer = require('bare-tcp').Server | |
| 2 | const HTTPServerConnection = require('bare-http1').ServerConnection | |
| 3 | const HTTPSSocket = require('./socket') | |
| 4 | ||
| 5 | module.exports = class HTTPSServer extends TCPServer { | |
| 6 | constructor(opts = {}, onrequest) { | |
| 7 | if (typeof opts === 'function') { | |
| 8 | onrequest = opts | |
| 9 | opts = {} | |
| 10 | } | |
| 11 | ||
| 12 | super({ allowHalfOpen: false }) | |
| 13 | ||
| 14 | this._timeout = 0 | |
| 15 | ||
| 16 | this.on('connection', (socket) => { | |
| 17 | new HTTPServerConnection(this, new HTTPSSocket(socket, { ...opts, isServer: true }), opts) | |
| 18 | }) | |
| 19 | ||
| 20 | if (onrequest) this.on('request', onrequest) | |
| 21 | } | |
| 22 | ||
| 23 | get timeout() { | |
| 24 | return this._timeout || undefined // For Node.js compatibility | |
| 25 | } | |
| 26 | ||
| 27 | setTimeout(ms = 0, ontimeout) { | |
| 28 | if (ontimeout) this.on('timeout', ontimeout) | |
| 29 | ||
| 30 | this._timeout = ms | |
| 31 | ||
| 32 | return this | |
| 33 | } | |
| 34 | ||
| 35 | close(onclose) { | |
| 36 | super.close(onclose) | |
| 37 | ||
| 38 | for (const socket of this.connections) { | |
| 39 | const connection = HTTPServerConnection.for(socket) | |
| 40 | ||
| 41 | if (connection === null || connection.idle) { | |
| 42 | socket.destroy() | |
| 43 | } | |
| 44 | } | |
| 45 | ||
| 46 | return this | |
| 47 | } | |
| 48 | } |
| … | … | @@ -0,0 +1,33 @@ |
| 1 | const TLSSocket = require('bare-tls').Socket | |
| 2 | ||
| 3 | module.exports = class HTTPSSocket extends TLSSocket { | |
| 4 | setKeepAlive(...args) { | |
| 5 | this.socket.setKeepAlive(...args) | |
| 6 | ||
| 7 | return this | |
| 8 | } | |
| 9 | ||
| 10 | setNoDelay(...args) { | |
| 11 | this.socket.setNoDelay(...args) | |
| 12 | ||
| 13 | return this | |
| 14 | } | |
| 15 | ||
| 16 | setTimeout(...args) { | |
| 17 | this.socket.setTimeout(...args) | |
| 18 | ||
| 19 | return this | |
| 20 | } | |
| 21 | ||
| 22 | ref() { | |
| 23 | this.socket.ref() | |
| 24 | ||
| 25 | return this | |
| 26 | } | |
| 27 | ||
| 28 | unref() { | |
| 29 | this.socket.unref() | |
| 30 | ||
| 31 | return this | |
| 32 | } | |
| 33 | } |
| … | … | @@ -0,0 +1,41 @@ |
| 1 | { | |
| 2 | "name": "bare-https", | |
| 3 | "version": "2.1.3", | |
| 4 | "description": "HTTPS library for JavaScript", | |
| 5 | "exports": { | |
| 6 | ".": { | |
| 7 | "types": "./index.d.ts", | |
| 8 | "default": "./index.js" | |
| 9 | }, | |
| 10 | "./package": "./package.json" | |
| 11 | }, | |
| 12 | "files": [ | |
| 13 | "index.js", | |
| 14 | "index.d.ts", | |
| 15 | "lib" | |
| 16 | ], | |
| 17 | "scripts": { | |
| 18 | "test": "prettier . --check && bare test.js" | |
| 19 | }, | |
| 20 | "repository": { | |
| 21 | "type": "git", | |
| 22 | "url": "git+https://github.com/holepunchto/bare-https.git" | |
| 23 | }, | |
| 24 | "author": "Holepunch", | |
| 25 | "license": "Apache-2.0", | |
| 26 | "bugs": { | |
| 27 | "url": "https://github.com/holepunchto/bare-https/issues" | |
| 28 | }, | |
| 29 | "homepage": "https://github.com/holepunchto/bare-https#readme", | |
| 30 | "dependencies": { | |
| 31 | "bare-http1": "^4.4.0", | |
| 32 | "bare-tcp": "^2.2.0", | |
| 33 | "bare-tls": "^2.0.0" | |
| 34 | }, | |
| 35 | "devDependencies": { | |
| 36 | "bare-fs": "^4.0.0", | |
| 37 | "brittle": "^3.3.0", | |
| 38 | "prettier": "^3.4.1", | |
| 39 | "prettier-config-holepunch": "^2.0.0" | |
| 40 | } | |
| 41 | } |
| … | … | @@ -0,0 +1,13 @@ |
| 1 | cmake_minimum_required(VERSION 3.25) | |
| 2 | ||
| 3 | find_package(cmake-bare REQUIRED PATHS node_modules/cmake-bare) | |
| 4 | ||
| 5 | project(bare_inspector C) | |
| 6 | ||
| 7 | add_bare_module(bare_inspector) | |
| 8 | ||
| 9 | target_sources( | |
| 10 | ${bare_inspector} | |
| 11 | PRIVATE | |
| 12 | binding.c | |
| 13 | ) |
| … | … | @@ -0,0 +1,201 @@ |
| 1 | Apache License | |
| 2 | Version 2.0, January 2004 | |
| 3 | http://www.apache.org/licenses/ | |
| 4 | ||
| 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | |
| 6 | ||
| 7 | 1. Definitions. | |
| 8 | ||
| 9 | "License" shall mean the terms and conditions for use, reproduction, | |
| 10 | and distribution as defined by Sections 1 through 9 of this document. | |
| 11 | ||
| 12 | "Licensor" shall mean the copyright owner or entity authorized by | |
| 13 | the copyright owner that is granting the License. | |
| 14 | ||
| 15 | "Legal Entity" shall mean the union of the acting entity and all | |
| 16 | other entities that control, are controlled by, or are under common | |
| 17 | control with that entity. For the purposes of this definition, | |
| 18 | "control" means (i) the power, direct or indirect, to cause the | |
| 19 | direction or management of such entity, whether by contract or | |
| 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the | |
| 21 | outstanding shares, or (iii) beneficial ownership of such entity. | |
| 22 | ||
| 23 | "You" (or "Your") shall mean an individual or Legal Entity | |
| 24 | exercising permissions granted by this License. | |
| 25 | ||
| 26 | "Source" form shall mean the preferred form for making modifications, | |
| 27 | including but not limited to software source code, documentation | |
| 28 | source, and configuration files. | |
| 29 | ||
| 30 | "Object" form shall mean any form resulting from mechanical | |
| 31 | transformation or translation of a Source form, including but | |
| 32 | not limited to compiled object code, generated documentation, | |
| 33 | and conversions to other media types. | |
| 34 | ||
| 35 | "Work" shall mean the work of authorship, whether in Source or | |
| 36 | Object form, made available under the License, as indicated by a | |
| 37 | copyright notice that is included in or attached to the work | |
| 38 | (an example is provided in the Appendix below). | |
| 39 | ||
| 40 | "Derivative Works" shall mean any work, whether in Source or Object | |
| 41 | form, that is based on (or derived from) the Work and for which the | |
| 42 | editorial revisions, annotations, elaborations, or other modifications | |
| 43 | represent, as a whole, an original work of authorship. For the purposes | |
| 44 | of this License, Derivative Works shall not include works that remain | |
| 45 | separable from, or merely link (or bind by name) to the interfaces of, | |
| 46 | the Work and Derivative Works thereof. | |
| 47 | ||
| 48 | "Contribution" shall mean any work of authorship, including | |
| 49 | the original version of the Work and any modifications or additions | |
| 50 | to that Work or Derivative Works thereof, that is intentionally | |
| 51 | submitted to Licensor for inclusion in the Work by the copyright owner | |
| 52 | or by an individual or Legal Entity authorized to submit on behalf of | |
| 53 | the copyright owner. For the purposes of this definition, "submitted" | |
| 54 | means any form of electronic, verbal, or written communication sent | |
| 55 | to the Licensor or its representatives, including but not limited to | |
| 56 | communication on electronic mailing lists, source code control systems, | |
| 57 | and issue tracking systems that are managed by, or on behalf of, the | |
| 58 | Licensor for the purpose of discussing and improving the Work, but | |
| 59 | excluding communication that is conspicuously marked or otherwise | |
| 60 | designated in writing by the copyright owner as "Not a Contribution." | |
| 61 | ||
| 62 | "Contributor" shall mean Licensor and any individual or Legal Entity | |
| 63 | on behalf of whom a Contribution has been received by Licensor and | |
| 64 | subsequently incorporated within the Work. | |
| 65 | ||
| 66 | 2. Grant of Copyright License. Subject to the terms and conditions of | |
| 67 | this License, each Contributor hereby grants to You a perpetual, | |
| 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 69 | copyright license to reproduce, prepare Derivative Works of, | |
| 70 | publicly display, publicly perform, sublicense, and distribute the | |
| 71 | Work and such Derivative Works in Source or Object form. | |
| 72 | ||
| 73 | 3. Grant of Patent License. Subject to the terms and conditions of | |
| 74 | this License, each Contributor hereby grants to You a perpetual, | |
| 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 76 | (except as stated in this section) patent license to make, have made, | |
| 77 | use, offer to sell, sell, import, and otherwise transfer the Work, | |
| 78 | where such license applies only to those patent claims licensable | |
| 79 | by such Contributor that are necessarily infringed by their | |
| 80 | Contribution(s) alone or by combination of their Contribution(s) | |
| 81 | with the Work to which such Contribution(s) was submitted. If You | |
| 82 | institute patent litigation against any entity (including a | |
| 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work | |
| 84 | or a Contribution incorporated within the Work constitutes direct | |
| 85 | or contributory patent infringement, then any patent licenses | |
| 86 | granted to You under this License for that Work shall terminate | |
| 87 | as of the date such litigation is filed. | |
| 88 | ||
| 89 | 4. Redistribution. You may reproduce and distribute copies of the | |
| 90 | Work or Derivative Works thereof in any medium, with or without | |
| 91 | modifications, and in Source or Object form, provided that You | |
| 92 | meet the following conditions: | |
| 93 | ||
| 94 | (a) You must give any other recipients of the Work or | |
| 95 | Derivative Works a copy of this License; and | |
| 96 | ||
| 97 | (b) You must cause any modified files to carry prominent notices | |
| 98 | stating that You changed the files; and | |
| 99 | ||
| 100 | (c) You must retain, in the Source form of any Derivative Works | |
| 101 | that You distribute, all copyright, patent, trademark, and | |
| 102 | attribution notices from the Source form of the Work, | |
| 103 | excluding those notices that do not pertain to any part of | |
| 104 | the Derivative Works; and | |
| 105 | ||
| 106 | (d) If the Work includes a "NOTICE" text file as part of its | |
| 107 | distribution, then any Derivative Works that You distribute must | |
| 108 | include a readable copy of the attribution notices contained | |
| 109 | within such NOTICE file, excluding those notices that do not | |
| 110 | pertain to any part of the Derivative Works, in at least one | |
| 111 | of the following places: within a NOTICE text file distributed | |
| 112 | as part of the Derivative Works; within the Source form or | |
| 113 | documentation, if provided along with the Derivative Works; or, | |
| 114 | within a display generated by the Derivative Works, if and | |
| 115 | wherever such third-party notices normally appear. The contents | |
| 116 | of the NOTICE file are for informational purposes only and | |
| 117 | do not modify the License. You may add Your own attribution | |
| 118 | notices within Derivative Works that You distribute, alongside | |
| 119 | or as an addendum to the NOTICE text from the Work, provided | |
| 120 | that such additional attribution notices cannot be construed | |
| 121 | as modifying the License. | |
| 122 | ||
| 123 | You may add Your own copyright statement to Your modifications and | |
| 124 | may provide additional or different license terms and conditions | |
| 125 | for use, reproduction, or distribution of Your modifications, or | |
| 126 | for any such Derivative Works as a whole, provided Your use, | |
| 127 | reproduction, and distribution of the Work otherwise complies with | |
| 128 | the conditions stated in this License. | |
| 129 | ||
| 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, | |
| 131 | any Contribution intentionally submitted for inclusion in the Work | |
| 132 | by You to the Licensor shall be under the terms and conditions of | |
| 133 | this License, without any additional terms or conditions. | |
| 134 | Notwithstanding the above, nothing herein shall supersede or modify | |
| 135 | the terms of any separate license agreement you may have executed | |
| 136 | with Licensor regarding such Contributions. | |
| 137 | ||
| 138 | 6. Trademarks. This License does not grant permission to use the trade | |
| 139 | names, trademarks, service marks, or product names of the Licensor, | |
| 140 | except as required for reasonable and customary use in describing the | |
| 141 | origin of the Work and reproducing the content of the NOTICE file. | |
| 142 | ||
| 143 | 7. Disclaimer of Warranty. Unless required by applicable law or | |
| 144 | agreed to in writing, Licensor provides the Work (and each | |
| 145 | Contributor provides its Contributions) on an "AS IS" BASIS, | |
| 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |
| 147 | implied, including, without limitation, any warranties or conditions | |
| 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | |
| 149 | PARTICULAR PURPOSE. You are solely responsible for determining the | |
| 150 | appropriateness of using or redistributing the Work and assume any | |
| 151 | risks associated with Your exercise of permissions under this License. | |
| 152 | ||
| 153 | 8. Limitation of Liability. In no event and under no legal theory, | |
| 154 | whether in tort (including negligence), contract, or otherwise, | |
| 155 | unless required by applicable law (such as deliberate and grossly | |
| 156 | negligent acts) or agreed to in writing, shall any Contributor be | |
| 157 | liable to You for damages, including any direct, indirect, special, | |
| 158 | incidental, or consequential damages of any character arising as a | |
| 159 | result of this License or out of the use or inability to use the | |
| 160 | Work (including but not limited to damages for loss of goodwill, | |
| 161 | work stoppage, computer failure or malfunction, or any and all | |
| 162 | other commercial damages or losses), even if such Contributor | |
| 163 | has been advised of the possibility of such damages. | |
| 164 | ||
| 165 | 9. Accepting Warranty or Additional Liability. While redistributing | |
| 166 | the Work or Derivative Works thereof, You may choose to offer, | |
| 167 | and charge a fee for, acceptance of support, warranty, indemnity, | |
| 168 | or other liability obligations and/or rights consistent with this | |
| 169 | License. However, in accepting such obligations, You may act only | |
| 170 | on Your own behalf and on Your sole responsibility, not on behalf | |
| 171 | of any other Contributor, and only if You agree to indemnify, | |
| 172 | defend, and hold each Contributor harmless for any liability | |
| 173 | incurred by, or claims asserted against, such Contributor by reason | |
| 174 | of your accepting any such warranty or additional liability. | |
| 175 | ||
| 176 | END OF TERMS AND CONDITIONS | |
| 177 | ||
| 178 | APPENDIX: How to apply the Apache License to your work. | |
| 179 | ||
| 180 | To apply the Apache License to your work, attach the following | |
| 181 | boilerplate notice, with the fields enclosed by brackets "[]" | |
| 182 | replaced with your own identifying information. (Don't include | |
| 183 | the brackets!) The text should be enclosed in the appropriate | |
| 184 | comment syntax for the file format. We also recommend that a | |
| 185 | file or class name and description of purpose be included on the | |
| 186 | same "printed page" as the copyright notice for easier | |
| 187 | identification within third-party archives. | |
| 188 | ||
| 189 | Copyright [yyyy] [name of copyright owner] | |
| 190 | ||
| 191 | Licensed under the Apache License, Version 2.0 (the "License"); | |
| 192 | you may not use this file except in compliance with the License. | |
| 193 | You may obtain a copy of the License at | |
| 194 | ||
| 195 | http://www.apache.org/licenses/LICENSE-2.0 | |
| 196 | ||
| 197 | Unless required by applicable law or agreed to in writing, software | |
| 198 | distributed under the License is distributed on an "AS IS" BASIS, | |
| 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 200 | See the License for the specific language governing permissions and | |
| 201 | limitations under the License. |
| … | … | @@ -0,0 +1,62 @@ |
| 1 | # bare-inspector | |
| 2 | ||
| 3 | V8 inspector support for Bare. | |
| 4 | ||
| 5 | ``` | |
| 6 | npm i bare-inspector | |
| 7 | ``` | |
| 8 | ||
| 9 | ## Usage | |
| 10 | ||
| 11 | ```js | |
| 12 | const { Session } = require('bare-inspector') | |
| 13 | ||
| 14 | const session = new Session() | |
| 15 | session.connect() | |
| 16 | ||
| 17 | try { | |
| 18 | const { result } = await session.post('Runtime.evaluate', { | |
| 19 | expression: '1 + 2' | |
| 20 | }) | |
| 21 | ||
| 22 | console.log(result) | |
| 23 | } catch (err) { | |
| 24 | console.error(err) | |
| 25 | } | |
| 26 | ``` | |
| 27 | ||
| 28 | ### Heap snapshots | |
| 29 | ||
| 30 | ```js | |
| 31 | const { Session, HeapSnapshot } = require('bare-inspector') | |
| 32 | const fs = require('bare-fs') | |
| 33 | ||
| 34 | const session = new Session() | |
| 35 | session.connect() | |
| 36 | ||
| 37 | const snapshot = new HeapSnapshot(session) | |
| 38 | ||
| 39 | snapshot.pipe(fs.createWriteStream('profile.heapsnapshot')) | |
| 40 | ``` | |
| 41 | ||
| 42 | ## API | |
| 43 | ||
| 44 | #### `const session = new Session()` | |
| 45 | ||
| 46 | #### `session.connect()` | |
| 47 | ||
| 48 | #### `session.post()` | |
| 49 | ||
| 50 | #### `session.destroy()` | |
| 51 | ||
| 52 | #### `session.on('<inspector-protocol-method>', message)` | |
| 53 | ||
| 54 | #### `session.on('inspectorNotification', message)` | |
| 55 | ||
| 56 | #### `const snapshot = new HeapSnapshot(session)` | |
| 57 | ||
| 58 | #### `for await (const chunk of snapshot)` | |
| 59 | ||
| 60 | ## License | |
| 61 | ||
| 62 | Apache-2.0 |
| … | … | @@ -0,0 +1,222 @@ |
| 1 | #include <assert.h> | |
| 2 | #include <bare.h> | |
| 3 | #include <js.h> | |
| 4 | ||
| 5 | typedef struct { | |
| 6 | js_inspector_t *handle; | |
| 7 | ||
| 8 | js_env_t *env; | |
| 9 | js_ref_t *ctx; | |
| 10 | js_ref_t *on_response; | |
| 11 | js_ref_t *on_paused; | |
| 12 | } bare_inspector_t; | |
| 13 | ||
| 14 | static void | |
| 15 | bare_inspector__on_response(js_env_t *env, js_inspector_t *handle, js_value_t *message, void *data) { | |
| 16 | int err; | |
| 17 | ||
| 18 | bare_inspector_t *inspector = (bare_inspector_t *) data; | |
| 19 | ||
| 20 | js_handle_scope_t *scope; | |
| 21 | err = js_open_handle_scope(env, &scope); | |
| 22 | assert(err == 0); | |
| 23 | ||
| 24 | js_value_t *ctx; | |
| 25 | err = js_get_reference_value(env, inspector->ctx, &ctx); | |
| 26 | assert(err == 0); | |
| 27 | ||
| 28 | js_value_t *callback; | |
| 29 | err = js_get_reference_value(env, inspector->on_response, &callback); | |
| 30 | assert(err == 0); | |
| 31 | ||
| 32 | js_call_function(env, ctx, callback, 1, &message, NULL); | |
| 33 | ||
| 34 | err = js_close_handle_scope(env, scope); | |
| 35 | assert(err == 0); | |
| 36 | } | |
| 37 | ||
| 38 | static bool | |
| 39 | bare_inspector__on_paused(js_env_t *env, js_inspector_t *handle, void *data) { | |
| 40 | int err; | |
| 41 | ||
| 42 | bare_inspector_t *inspector = (bare_inspector_t *) data; | |
| 43 | ||
| 44 | js_handle_scope_t *scope; | |
| 45 | err = js_open_handle_scope(env, &scope); | |
| 46 | assert(err == 0); | |
| 47 | ||
| 48 | js_value_t *ctx; | |
| 49 | err = js_get_reference_value(env, inspector->ctx, &ctx); | |
| 50 | assert(err == 0); | |
| 51 | ||
| 52 | js_value_t *callback; | |
| 53 | err = js_get_reference_value(env, inspector->on_paused, &callback); | |
| 54 | assert(err == 0); | |
| 55 | ||
| 56 | js_value_t *result; | |
| 57 | err = js_call_function(env, ctx, callback, 0, NULL, &result); | |
| 58 | ||
| 59 | bool value; | |
| 60 | ||
| 61 | if (err < 0) value = false; | |
| 62 | else { | |
| 63 | err = js_get_value_bool(env, result, &value); | |
| 64 | assert(err == 0); | |
| 65 | } | |
| 66 | ||
| 67 | err = js_close_handle_scope(env, scope); | |
| 68 | assert(err == 0); | |
| 69 | ||
| 70 | return value; | |
| 71 | } | |
| 72 | ||
| 73 | static js_value_t * | |
| 74 | bare_inspector_create(js_env_t *env, js_callback_info_t *info) { | |
| 75 | int err; | |
| 76 | ||
| 77 | size_t argc = 3; | |
| 78 | js_value_t *argv[3]; | |
| 79 | ||
| 80 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 81 | assert(err == 0); | |
| 82 | ||
| 83 | assert(argc == 3); | |
| 84 | ||
| 85 | js_value_t *handle; | |
| 86 | ||
| 87 | bare_inspector_t *inspector; | |
| 88 | err = js_create_arraybuffer(env, sizeof(bare_inspector_t), (void **) &inspector, &handle); | |
| 89 | assert(err == 0); | |
| 90 | ||
| 91 | err = js_create_inspector(env, &inspector->handle); | |
| 92 | assert(err == 0); | |
| 93 | ||
| 94 | err = js_on_inspector_response(env, inspector->handle, bare_inspector__on_response, (void *) inspector); | |
| 95 | assert(err == 0); | |
| 96 | ||
| 97 | err = js_on_inspector_paused(env, inspector->handle, bare_inspector__on_paused, (void *) inspector); | |
| 98 | assert(err == 0); | |
| 99 | ||
| 100 | inspector->env = env; | |
| 101 | ||
| 102 | err = js_create_reference(env, argv[0], 1, &inspector->ctx); | |
| 103 | assert(err == 0); | |
| 104 | ||
| 105 | err = js_create_reference(env, argv[1], 1, &inspector->on_response); | |
| 106 | assert(err == 0); | |
| 107 | ||
| 108 | err = js_create_reference(env, argv[2], 1, &inspector->on_paused); | |
| 109 | assert(err == 0); | |
| 110 | ||
| 111 | return handle; | |
| 112 | } | |
| 113 | ||
| 114 | static js_value_t * | |
| 115 | bare_inspector_destroy(js_env_t *env, js_callback_info_t *info) { | |
| 116 | int err; | |
| 117 | ||
| 118 | size_t argc = 1; | |
| 119 | js_value_t *argv[1]; | |
| 120 | ||
| 121 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 122 | assert(err == 0); | |
| 123 | ||
| 124 | assert(argc == 1); | |
| 125 | ||
| 126 | bare_inspector_t *inspector; | |
| 127 | err = js_get_arraybuffer_info(env, argv[0], (void **) &inspector, NULL); | |
| 128 | assert(err == 0); | |
| 129 | ||
| 130 | err = js_destroy_inspector(env, inspector->handle); | |
| 131 | assert(err == 0); | |
| 132 | ||
| 133 | err = js_delete_reference(env, inspector->on_response); | |
| 134 | assert(err == 0); | |
| 135 | ||
| 136 | err = js_delete_reference(env, inspector->on_paused); | |
| 137 | assert(err == 0); | |
| 138 | ||
| 139 | err = js_delete_reference(env, inspector->ctx); | |
| 140 | assert(err == 0); | |
| 141 | ||
| 142 | return NULL; | |
| 143 | } | |
| 144 | ||
| 145 | static js_value_t * | |
| 146 | bare_inspector_connect(js_env_t *env, js_callback_info_t *info) { | |
| 147 | int err; | |
| 148 | ||
| 149 | size_t argc = 1; | |
| 150 | js_value_t *argv[1]; | |
| 151 | ||
| 152 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 153 | assert(err == 0); | |
| 154 | ||
| 155 | assert(argc == 1); | |
| 156 | ||
| 157 | bare_inspector_t *inspector; | |
| 158 | err = js_get_arraybuffer_info(env, argv[0], (void **) &inspector, NULL); | |
| 159 | assert(err == 0); | |
| 160 | ||
| 161 | err = js_connect_inspector(env, inspector->handle); | |
| 162 | assert(err == 0); | |
| 163 | ||
| 164 | return NULL; | |
| 165 | } | |
| 166 | ||
| 167 | static js_value_t * | |
| 168 | bare_inspector_post(js_env_t *env, js_callback_info_t *info) { | |
| 169 | int err; | |
| 170 | ||
| 171 | size_t argc = 2; | |
| 172 | js_value_t *argv[2]; | |
| 173 | ||
| 174 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 175 | assert(err == 0); | |
| 176 | ||
| 177 | assert(argc == 2); | |
| 178 | ||
| 179 | bare_inspector_t *inspector; | |
| 180 | err = js_get_arraybuffer_info(env, argv[0], (void **) &inspector, NULL); | |
| 181 | assert(err == 0); | |
| 182 | ||
| 183 | err = js_send_inspector_request(env, inspector->handle, argv[1]); | |
| 184 | assert(err == 0); | |
| 185 | ||
| 186 | return NULL; | |
| 187 | } | |
| 188 | ||
| 189 | static js_value_t * | |
| 190 | bare_inspector_exports(js_env_t *env, js_value_t *exports) { | |
| 191 | int err; | |
| 192 | ||
| 193 | #define V(name, fn) \ | |
| 194 | { \ | |
| 195 | js_value_t *val; \ | |
| 196 | err = js_create_function(env, name, -1, fn, NULL, &val); \ | |
| 197 | assert(err == 0); \ | |
| 198 | err = js_set_named_property(env, exports, name, val); \ | |
| 199 | assert(err == 0); \ | |
| 200 | } | |
| 201 | ||
| 202 | V("create", bare_inspector_create) | |
| 203 | V("destroy", bare_inspector_destroy) | |
| 204 | V("connect", bare_inspector_connect) | |
| 205 | V("post", bare_inspector_post) | |
| 206 | #undef V | |
| 207 | ||
| 208 | js_value_t *bindings; | |
| 209 | err = js_get_bindings(env, &bindings); | |
| 210 | assert(err == 0); | |
| 211 | ||
| 212 | js_value_t *console; | |
| 213 | err = js_get_named_property(env, bindings, "console", &console); | |
| 214 | assert(err == 0); | |
| 215 | ||
| 216 | err = js_set_named_property(env, exports, "console", console); | |
| 217 | assert(err == 0); | |
| 218 | ||
| 219 | return exports; | |
| 220 | } | |
| 221 | ||
| 222 | BARE_MODULE(bare_inspector, bare_inspector_exports) |
| … | … | @@ -0,0 +1 @@ |
| 1 | module.exports = require.addon() |
| … | … | @@ -0,0 +1,6 @@ |
| 1 | import Console from './lib/console' | |
| 2 | import Session from './lib/session' | |
| 3 | import Server from './lib/server' | |
| 4 | import HeapSnapshot from './lib/heap-snapshot' | |
| 5 | ||
| 6 | export { Console, Session, Server, HeapSnapshot } |
| … | … | @@ -0,0 +1,4 @@ |
| 1 | exports.Console = require('./lib/console') | |
| 2 | exports.Session = require('./lib/session') | |
| 3 | exports.Server = require('./lib/server') | |
| 4 | exports.HeapSnapshot = require('./lib/heap-snapshot') |
| … | … | @@ -0,0 +1,38 @@ |
| 1 | interface DirOptions { | |
| 2 | colors?: number | |
| 3 | depth?: number | |
| 4 | showHidden?: boolean | |
| 5 | } | |
| 6 | ||
| 7 | declare class InspectorConsole { | |
| 8 | debug(...data: unknown[]): void | |
| 9 | error(...data: unknown[]): void | |
| 10 | info(...data: unknown[]): void | |
| 11 | log(...data: unknown[]): void | |
| 12 | warn(...data: unknown[]): void | |
| 13 | ||
| 14 | assert(condition: unknown, ...data: unknown[]): void | |
| 15 | clear(): void | |
| 16 | count(label?: string): void | |
| 17 | countReset(label?: string): void | |
| 18 | dir(object: unknown, opts?: DirOptions): void | |
| 19 | dirxml(...data: unknown[]): void | |
| 20 | group(...data: unknown[]): void | |
| 21 | groupCollapsed(...data: unknown[]): void | |
| 22 | groupEnd(): void | |
| 23 | table(data: unknown, props?: string[]): void | |
| 24 | time(label?: string): void | |
| 25 | timeEnd(label?: string): void | |
| 26 | timeLog(label?: string, ...data: unknown[]): void | |
| 27 | trace(...data: unknown[]): void | |
| 28 | ||
| 29 | profile(label?: string): void | |
| 30 | profileEnd(label?: string): void | |
| 31 | timeStamp(label?: string): void | |
| 32 | } | |
| 33 | ||
| 34 | declare namespace InspectorConsole { | |
| 35 | export { type DirOptions } | |
| 36 | } | |
| 37 | ||
| 38 | export = InspectorConsole |
| … | … | @@ -0,0 +1,9 @@ |
| 1 | const binding = require('../binding') | |
| 2 | ||
| 3 | module.exports = class InspectorConsole { | |
| 4 | constructor() { | |
| 5 | for (const method of Object.keys(binding.console)) { | |
| 6 | this[method] = binding.console[method] | |
| 7 | } | |
| 8 | } | |
| 9 | } |
| … | … | @@ -0,0 +1,6 @@ |
| 1 | module.exports = { | |
| 2 | state: { | |
| 3 | CONNECTED: 0x1, | |
| 4 | DESTROYED: 0x2 | |
| 5 | } | |
| 6 | } |
| … | … | @@ -0,0 +1,10 @@ |
| 1 | import { Readable } from 'bare-stream' | |
| 2 | import Session from './session' | |
| 3 | ||
| 4 | interface InspectorHeapSnapshot extends Readable {} | |
| 5 | ||
| 6 | declare class InspectorHeapSnapshot { | |
| 7 | constructor(session: Session) | |
| 8 | } | |
| 9 | ||
| 10 | export = InspectorHeapSnapshot |
| … | … | @@ -0,0 +1,30 @@ |
| 1 | const { Readable } = require('bare-stream') | |
| 2 | ||
| 3 | module.exports = class InspectorHeapSnapshot extends Readable { | |
| 4 | constructor(session) { | |
| 5 | super() | |
| 6 | ||
| 7 | this._session = session | |
| 8 | this._request = null | |
| 9 | } | |
| 10 | ||
| 11 | _open(cb) { | |
| 12 | const onchunk = ({ params }) => { | |
| 13 | this.push(params.chunk) | |
| 14 | } | |
| 15 | ||
| 16 | const onclose = ({ error } = {}) => { | |
| 17 | this._session.off('HeapProfiler.addHeapSnapshotChunk', onchunk) | |
| 18 | ||
| 19 | if (error) this.destroy(error) | |
| 20 | else this.push(null) | |
| 21 | } | |
| 22 | ||
| 23 | this._session.on('HeapProfiler.addHeapSnapshotChunk', onchunk) | |
| 24 | ||
| 25 | this._request = this._session.post('HeapProfiler.takeHeapSnapshot') | |
| 26 | this._request.then(onclose, onclose) | |
| 27 | ||
| 28 | cb() | |
| 29 | } | |
| 30 | } |
| … | … | @@ -0,0 +1,35 @@ |
| 1 | import EventEmitter, { EventMap } from 'bare-events' | |
| 2 | import { type TCPSocketAddress } from 'bare-tcp' | |
| 3 | import URL from 'bare-url' | |
| 4 | ||
| 5 | interface InspectorServerEvents extends EventMap { | |
| 6 | listening: [] | |
| 7 | } | |
| 8 | ||
| 9 | interface InspectorServerOptions { | |
| 10 | path: URL | string | |
| 11 | } | |
| 12 | ||
| 13 | interface InspectorServer< | |
| 14 | M extends InspectorServerEvents = InspectorServerEvents | |
| 15 | > extends EventEmitter<M> { | |
| 16 | readonly listening: boolean | |
| 17 | ||
| 18 | address(): TCPSocketAddress | |
| 19 | close(onclose?: (err: Error) => void): void | |
| 20 | ||
| 21 | ref(): void | |
| 22 | unref(): void | |
| 23 | } | |
| 24 | ||
| 25 | declare class InspectorServer { | |
| 26 | constructor(opts: InspectorServerOptions) | |
| 27 | constructor(port: number, opts: InspectorServerOptions) | |
| 28 | constructor(port: number, host: string, opts?: InspectorServerOptions) | |
| 29 | } | |
| 30 | ||
| 31 | declare namespace InspectorServer { | |
| 32 | export { type InspectorServerEvents, type InspectorServerOptions } | |
| 33 | } | |
| 34 | ||
| 35 | export = InspectorServer |
| … | … | @@ -0,0 +1,120 @@ |
| 1 | /* global Bare */ | |
| 2 | const EventEmitter = require('bare-events') | |
| 3 | const url = require('bare-url') | |
| 4 | const ws = require('bare-ws') | |
| 5 | const http = require('bare-http1') | |
| 6 | const Session = require('./session') | |
| 7 | ||
| 8 | module.exports = class InspectorServer extends EventEmitter { | |
| 9 | constructor(port, host, opts = {}) { | |
| 10 | if (typeof port === 'object' && port !== null) { | |
| 11 | opts = port | |
| 12 | port = 0 | |
| 13 | host = 'localhost' | |
| 14 | } else if (typeof host === 'object' && host !== null) { | |
| 15 | opts = host | |
| 16 | host = 'localhost' | |
| 17 | } | |
| 18 | ||
| 19 | const { path = require.main.path } = opts | |
| 20 | ||
| 21 | super() | |
| 22 | ||
| 23 | this._path = typeof path === 'string' ? url.pathToFileURL(path) : path | |
| 24 | this._sessions = new Map() | |
| 25 | ||
| 26 | this._server = new ws.Server( | |
| 27 | { | |
| 28 | server: new http.Server(this._onrequest.bind(this)).listen( | |
| 29 | { port, host }, | |
| 30 | this._onlistening.bind(this) | |
| 31 | ) | |
| 32 | }, | |
| 33 | this._onconnection.bind(this) | |
| 34 | ) | |
| 35 | } | |
| 36 | ||
| 37 | get listening() { | |
| 38 | return this._server.listening | |
| 39 | } | |
| 40 | ||
| 41 | address() { | |
| 42 | return this._server.address() | |
| 43 | } | |
| 44 | ||
| 45 | close(cb) { | |
| 46 | for (const socket of this._sessions.keys()) socket.destroy() | |
| 47 | ||
| 48 | return this._server.close(cb) | |
| 49 | } | |
| 50 | ||
| 51 | ref() { | |
| 52 | this._server.ref() | |
| 53 | } | |
| 54 | ||
| 55 | unref() { | |
| 56 | this._server.unref() | |
| 57 | } | |
| 58 | ||
| 59 | _onlistening() { | |
| 60 | this.emit('listening') | |
| 61 | } | |
| 62 | ||
| 63 | _onrequest(req, res) { | |
| 64 | if (req.url === '/json/list') return this._onlist(req, res) | |
| 65 | ||
| 66 | res.writeHead(404) | |
| 67 | res.end() | |
| 68 | } | |
| 69 | ||
| 70 | _onconnection(socket) { | |
| 71 | const sessions = this._sessions | |
| 72 | ||
| 73 | const session = new Session() | |
| 74 | ||
| 75 | sessions.set(socket, session) | |
| 76 | ||
| 77 | session.on('inspectorNotification', onnotification).connect() | |
| 78 | ||
| 79 | socket.on('close', onclose).on('data', ondata) | |
| 80 | ||
| 81 | function onnotification(message) { | |
| 82 | socket.write(JSON.stringify(message)) | |
| 83 | } | |
| 84 | ||
| 85 | function onclose() { | |
| 86 | session.destroy() | |
| 87 | sessions.delete(socket) | |
| 88 | } | |
| 89 | ||
| 90 | function ondata(data) { | |
| 91 | const { id, method, params } = JSON.parse(data) | |
| 92 | ||
| 93 | session.post(method, params, (err, result) => { | |
| 94 | const response = err ? { id, error: err } : { id, result } | |
| 95 | ||
| 96 | socket.write(JSON.stringify(response)) | |
| 97 | }) | |
| 98 | } | |
| 99 | } | |
| 100 | ||
| 101 | _onlist(req, res) { | |
| 102 | res.writeHead(200, { 'Content-Type': 'application/json' }) | |
| 103 | ||
| 104 | const { address, port } = this.address() | |
| 105 | ||
| 106 | res.end( | |
| 107 | JSON.stringify([ | |
| 108 | { | |
| 109 | title: `bare[${Bare.pid}]`, | |
| 110 | id: `${Bare.pid}`, | |
| 111 | type: 'node', | |
| 112 | url: this._path, | |
| 113 | devtoolsFrontendUrl: `devtools://devtools/bundled/js_app.html?ws=${address}:${port}`, | |
| 114 | webSocketDebuggerUrl: `ws://${address}:${port}`, | |
| 115 | faviconUrl: 'https://holepunch.to/favicon.ico' | |
| 116 | } | |
| 117 | ]) | |
| 118 | ) | |
| 119 | } | |
| 120 | } |
| … | … | @@ -0,0 +1,43 @@ |
| 1 | import EventEmitter, { EventMap } from 'bare-events' | |
| 2 | ||
| 3 | interface SessionMessage { | |
| 4 | method: string | |
| 5 | params: Record<string, unknown> | |
| 6 | } | |
| 7 | ||
| 8 | interface InspectorSessionEvents extends EventMap { | |
| 9 | inspectorNotification: [message: SessionMessage] | |
| 10 | [method: string]: [message: SessionMessage] | |
| 11 | } | |
| 12 | ||
| 13 | interface InspectorSession< | |
| 14 | M extends InspectorSessionEvents = InspectorSessionEvents | |
| 15 | > extends EventEmitter<M> { | |
| 16 | readonly connected: boolean | |
| 17 | readonly destroyed: boolean | |
| 18 | ||
| 19 | connect(): void | |
| 20 | ||
| 21 | post<T extends unknown = unknown>( | |
| 22 | method: string, | |
| 23 | cb: (err: Error, result: T) => void | |
| 24 | ): Promise<T> | |
| 25 | ||
| 26 | post<T extends unknown = unknown>( | |
| 27 | method: string, | |
| 28 | params?: Record<string, unknown>, | |
| 29 | cb?: (err: Error, result: T) => void | |
| 30 | ): Promise<T> | |
| 31 | ||
| 32 | destroy(): void | |
| 33 | } | |
| 34 | ||
| 35 | declare class InspectorSession { | |
| 36 | constructor(onpaused?: () => boolean) | |
| 37 | } | |
| 38 | ||
| 39 | declare namespace InspectorSession { | |
| 40 | export { type SessionMessage, type InspectorSessionEvents } | |
| 41 | } | |
| 42 | ||
| 43 | export = InspectorSession |
| … | … | @@ -0,0 +1,112 @@ |
| 1 | const EventEmitter = require('bare-events') | |
| 2 | const binding = require('../binding') | |
| 3 | const constants = require('./constants') | |
| 4 | ||
| 5 | module.exports = class InspectorSession extends EventEmitter { | |
| 6 | constructor(onpaused) { | |
| 7 | super() | |
| 8 | ||
| 9 | this._state = 0 | |
| 10 | ||
| 11 | this._nextId = 1 | |
| 12 | this._requests = new Map() | |
| 13 | ||
| 14 | this._onpaused = onpaused || defaultPaused | |
| 15 | ||
| 16 | this._handle = binding.create(this, this._onresponse, this._onpaused) | |
| 17 | } | |
| 18 | ||
| 19 | get connected() { | |
| 20 | return ( | |
| 21 | (this._state & constants.state.CONNECTED) !== 0 && | |
| 22 | (this._state & constants.state.DESTROYED) === 0 | |
| 23 | ) | |
| 24 | } | |
| 25 | ||
| 26 | get destroyed() { | |
| 27 | return (this._state & constants.state.DESTROYED) !== 0 | |
| 28 | } | |
| 29 | ||
| 30 | connect() { | |
| 31 | if (this._state & (constants.state.CONNECTED | constants.state.DESTROYED)) | |
| 32 | return | |
| 33 | ||
| 34 | this._state |= constants.state.CONNECTED | |
| 35 | ||
| 36 | binding.connect(this._handle) | |
| 37 | } | |
| 38 | ||
| 39 | post(method, params, cb) { | |
| 40 | if (typeof params === 'function') { | |
| 41 | cb = params | |
| 42 | params = null | |
| 43 | } | |
| 44 | ||
| 45 | let result | |
| 46 | ||
| 47 | if (typeof cb !== 'function') { | |
| 48 | result = new Promise((resolve, reject) => { | |
| 49 | cb = (err, result) => { | |
| 50 | if (err) reject(err) | |
| 51 | else resolve(result) | |
| 52 | } | |
| 53 | }) | |
| 54 | } | |
| 55 | ||
| 56 | const id = this._nextId++ | |
| 57 | ||
| 58 | const req = { | |
| 59 | id, | |
| 60 | callback: cb | |
| 61 | } | |
| 62 | ||
| 63 | this._requests.set(id, req) | |
| 64 | ||
| 65 | binding.post( | |
| 66 | this._handle, | |
| 67 | JSON.stringify({ | |
| 68 | id, | |
| 69 | method, | |
| 70 | params | |
| 71 | }) | |
| 72 | ) | |
| 73 | ||
| 74 | return result | |
| 75 | } | |
| 76 | ||
| 77 | destroy() { | |
| 78 | if (this._state & constants.state.DESTROYED) return | |
| 79 | ||
| 80 | this._state |= constants.state.DESTROYED | |
| 81 | ||
| 82 | binding.destroy(this._handle) | |
| 83 | } | |
| 84 | ||
| 85 | _onresponse(string) { | |
| 86 | const message = JSON.parse(string) | |
| 87 | ||
| 88 | if (message.method) { | |
| 89 | this.emit(message.method, message) | |
| 90 | this.emit('inspectorNotification', message) | |
| 91 | } else { | |
| 92 | const req = this._requests.get(message.id) | |
| 93 | ||
| 94 | if (req) { | |
| 95 | this._requests.delete(message.id) | |
| 96 | ||
| 97 | let err = null | |
| 98 | ||
| 99 | if (message.error) { | |
| 100 | err = new Error(message.error.message) | |
| 101 | err.code = message.error.code | |
| 102 | } | |
| 103 | ||
| 104 | req.callback(err, message.result) | |
| 105 | } | |
| 106 | } | |
| 107 | } | |
| 108 | } | |
| 109 | ||
| 110 | function defaultPaused() { | |
| 111 | return false | |
| 112 | } |
| … | … | @@ -0,0 +1,76 @@ |
| 1 | { | |
| 2 | "name": "bare-inspector", | |
| 3 | "version": "4.0.3", | |
| 4 | "description": "V8 inspector support for Bare", | |
| 5 | "exports": { | |
| 6 | ".": { | |
| 7 | "types": "./index.d.ts", | |
| 8 | "default": "./index.js" | |
| 9 | }, | |
| 10 | "./console": { | |
| 11 | "types": "./lib/console.d.ts", | |
| 12 | "default": "./lib/console.js" | |
| 13 | }, | |
| 14 | "./session": { | |
| 15 | "types": "./lib/session.d.ts", | |
| 16 | "default": "./lib/session.js" | |
| 17 | }, | |
| 18 | "./server": { | |
| 19 | "types": "./lib/server.d.ts", | |
| 20 | "default": "./lib/server.js" | |
| 21 | }, | |
| 22 | "./heap-snapshot": { | |
| 23 | "types": "./lib/heap-snapshot.d.ts", | |
| 24 | "default": "./lib/heap-snapshot.js" | |
| 25 | }, | |
| 26 | "./package": "./package.json" | |
| 27 | }, | |
| 28 | "files": [ | |
| 29 | "index.js", | |
| 30 | "index.d.ts", | |
| 31 | "binding.c", | |
| 32 | "binding.js", | |
| 33 | "lib", | |
| 34 | "CMakeLists.txt", | |
| 35 | "prebuilds" | |
| 36 | ], | |
| 37 | "addon": true, | |
| 38 | "scripts": { | |
| 39 | "test": "prettier . --check && bare test.js" | |
| 40 | }, | |
| 41 | "repository": { | |
| 42 | "type": "git", | |
| 43 | "url": "git+https://github.com/holepunchto/bare-inspector.git" | |
| 44 | }, | |
| 45 | "author": "Holepunch", | |
| 46 | "license": "Apache-2.0", | |
| 47 | "bugs": { | |
| 48 | "url": "https://github.com/holepunchto/bare-inspector/issues" | |
| 49 | }, | |
| 50 | "homepage": "https://github.com/holepunchto/bare-inspector#readme", | |
| 51 | "engines": { | |
| 52 | "bare": ">=1.2.0" | |
| 53 | }, | |
| 54 | "dependencies": { | |
| 55 | "bare-events": "^2.1.0", | |
| 56 | "bare-http1": "^4.0.0", | |
| 57 | "bare-stream": "^2.0.0", | |
| 58 | "bare-url": "^2.0.0", | |
| 59 | "bare-ws": "^2.0.0" | |
| 60 | }, | |
| 61 | "devDependencies": { | |
| 62 | "bare-tcp": "^2.0.3", | |
| 63 | "brittle": "^3.3.2", | |
| 64 | "cmake-bare": "^1.1.6", | |
| 65 | "prettier": "^3.4.1", | |
| 66 | "prettier-config-standard": "^7.0.0" | |
| 67 | }, | |
| 68 | "peerDependencies": { | |
| 69 | "bare-tcp": "*" | |
| 70 | }, | |
| 71 | "peerDependenciesMeta": { | |
| 72 | "bare-tcp": { | |
| 73 | "optional": true | |
| 74 | } | |
| 75 | } | |
| 76 | } |
| … | … | @@ -0,0 +1,37 @@ |
| 1 | cmake_minimum_required(VERSION 3.25) | |
| 2 | ||
| 3 | find_package(cmake-bare REQUIRED PATHS node_modules/cmake-bare) | |
| 4 | find_package(cmake-napi REQUIRED PATHS node_modules/cmake-napi) | |
| 5 | find_package(cmake-npm REQUIRED PATHS node_modules/cmake-npm) | |
| 6 | ||
| 7 | project(bare_module_lexer C) | |
| 8 | ||
| 9 | add_bare_module(bare_module_lexer) | |
| 10 | ||
| 11 | target_sources( | |
| 12 | ${bare_module_lexer} | |
| 13 | PRIVATE | |
| 14 | binding.c | |
| 15 | ) | |
| 16 | ||
| 17 | add_napi_module(bare_module_lexer_napi) | |
| 18 | ||
| 19 | target_sources( | |
| 20 | ${bare_module_lexer_napi} | |
| 21 | PRIVATE | |
| 22 | binding.c | |
| 23 | ) | |
| 24 | ||
| 25 | target_compile_definitions( | |
| 26 | ${bare_module_lexer_napi} | |
| 27 | PRIVATE | |
| 28 | NAPI_VERSION=9 | |
| 29 | ) | |
| 30 | ||
| 31 | resolve_node_module(bare-compat-napi compat) | |
| 32 | ||
| 33 | target_include_directories( | |
| 34 | ${bare_module_lexer_napi} | |
| 35 | PRIVATE | |
| 36 | "${compat}/include" | |
| 37 | ) |
| … | … | @@ -0,0 +1,201 @@ |
| 1 | Apache License | |
| 2 | Version 2.0, January 2004 | |
| 3 | http://www.apache.org/licenses/ | |
| 4 | ||
| 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | |
| 6 | ||
| 7 | 1. Definitions. | |
| 8 | ||
| 9 | "License" shall mean the terms and conditions for use, reproduction, | |
| 10 | and distribution as defined by Sections 1 through 9 of this document. | |
| 11 | ||
| 12 | "Licensor" shall mean the copyright owner or entity authorized by | |
| 13 | the copyright owner that is granting the License. | |
| 14 | ||
| 15 | "Legal Entity" shall mean the union of the acting entity and all | |
| 16 | other entities that control, are controlled by, or are under common | |
| 17 | control with that entity. For the purposes of this definition, | |
| 18 | "control" means (i) the power, direct or indirect, to cause the | |
| 19 | direction or management of such entity, whether by contract or | |
| 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the | |
| 21 | outstanding shares, or (iii) beneficial ownership of such entity. | |
| 22 | ||
| 23 | "You" (or "Your") shall mean an individual or Legal Entity | |
| 24 | exercising permissions granted by this License. | |
| 25 | ||
| 26 | "Source" form shall mean the preferred form for making modifications, | |
| 27 | including but not limited to software source code, documentation | |
| 28 | source, and configuration files. | |
| 29 | ||
| 30 | "Object" form shall mean any form resulting from mechanical | |
| 31 | transformation or translation of a Source form, including but | |
| 32 | not limited to compiled object code, generated documentation, | |
| 33 | and conversions to other media types. | |
| 34 | ||
| 35 | "Work" shall mean the work of authorship, whether in Source or | |
| 36 | Object form, made available under the License, as indicated by a | |
| 37 | copyright notice that is included in or attached to the work | |
| 38 | (an example is provided in the Appendix below). | |
| 39 | ||
| 40 | "Derivative Works" shall mean any work, whether in Source or Object | |
| 41 | form, that is based on (or derived from) the Work and for which the | |
| 42 | editorial revisions, annotations, elaborations, or other modifications | |
| 43 | represent, as a whole, an original work of authorship. For the purposes | |
| 44 | of this License, Derivative Works shall not include works that remain | |
| 45 | separable from, or merely link (or bind by name) to the interfaces of, | |
| 46 | the Work and Derivative Works thereof. | |
| 47 | ||
| 48 | "Contribution" shall mean any work of authorship, including | |
| 49 | the original version of the Work and any modifications or additions | |
| 50 | to that Work or Derivative Works thereof, that is intentionally | |
| 51 | submitted to Licensor for inclusion in the Work by the copyright owner | |
| 52 | or by an individual or Legal Entity authorized to submit on behalf of | |
| 53 | the copyright owner. For the purposes of this definition, "submitted" | |
| 54 | means any form of electronic, verbal, or written communication sent | |
| 55 | to the Licensor or its representatives, including but not limited to | |
| 56 | communication on electronic mailing lists, source code control systems, | |
| 57 | and issue tracking systems that are managed by, or on behalf of, the | |
| 58 | Licensor for the purpose of discussing and improving the Work, but | |
| 59 | excluding communication that is conspicuously marked or otherwise | |
| 60 | designated in writing by the copyright owner as "Not a Contribution." | |
| 61 | ||
| 62 | "Contributor" shall mean Licensor and any individual or Legal Entity | |
| 63 | on behalf of whom a Contribution has been received by Licensor and | |
| 64 | subsequently incorporated within the Work. | |
| 65 | ||
| 66 | 2. Grant of Copyright License. Subject to the terms and conditions of | |
| 67 | this License, each Contributor hereby grants to You a perpetual, | |
| 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 69 | copyright license to reproduce, prepare Derivative Works of, | |
| 70 | publicly display, publicly perform, sublicense, and distribute the | |
| 71 | Work and such Derivative Works in Source or Object form. | |
| 72 | ||
| 73 | 3. Grant of Patent License. Subject to the terms and conditions of | |
| 74 | this License, each Contributor hereby grants to You a perpetual, | |
| 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 76 | (except as stated in this section) patent license to make, have made, | |
| 77 | use, offer to sell, sell, import, and otherwise transfer the Work, | |
| 78 | where such license applies only to those patent claims licensable | |
| 79 | by such Contributor that are necessarily infringed by their | |
| 80 | Contribution(s) alone or by combination of their Contribution(s) | |
| 81 | with the Work to which such Contribution(s) was submitted. If You | |
| 82 | institute patent litigation against any entity (including a | |
| 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work | |
| 84 | or a Contribution incorporated within the Work constitutes direct | |
| 85 | or contributory patent infringement, then any patent licenses | |
| 86 | granted to You under this License for that Work shall terminate | |
| 87 | as of the date such litigation is filed. | |
| 88 | ||
| 89 | 4. Redistribution. You may reproduce and distribute copies of the | |
| 90 | Work or Derivative Works thereof in any medium, with or without | |
| 91 | modifications, and in Source or Object form, provided that You | |
| 92 | meet the following conditions: | |
| 93 | ||
| 94 | (a) You must give any other recipients of the Work or | |
| 95 | Derivative Works a copy of this License; and | |
| 96 | ||
| 97 | (b) You must cause any modified files to carry prominent notices | |
| 98 | stating that You changed the files; and | |
| 99 | ||
| 100 | (c) You must retain, in the Source form of any Derivative Works | |
| 101 | that You distribute, all copyright, patent, trademark, and | |
| 102 | attribution notices from the Source form of the Work, | |
| 103 | excluding those notices that do not pertain to any part of | |
| 104 | the Derivative Works; and | |
| 105 | ||
| 106 | (d) If the Work includes a "NOTICE" text file as part of its | |
| 107 | distribution, then any Derivative Works that You distribute must | |
| 108 | include a readable copy of the attribution notices contained | |
| 109 | within such NOTICE file, excluding those notices that do not | |
| 110 | pertain to any part of the Derivative Works, in at least one | |
| 111 | of the following places: within a NOTICE text file distributed | |
| 112 | as part of the Derivative Works; within the Source form or | |
| 113 | documentation, if provided along with the Derivative Works; or, | |
| 114 | within a display generated by the Derivative Works, if and | |
| 115 | wherever such third-party notices normally appear. The contents | |
| 116 | of the NOTICE file are for informational purposes only and | |
| 117 | do not modify the License. You may add Your own attribution | |
| 118 | notices within Derivative Works that You distribute, alongside | |
| 119 | or as an addendum to the NOTICE text from the Work, provided | |
| 120 | that such additional attribution notices cannot be construed | |
| 121 | as modifying the License. | |
| 122 | ||
| 123 | You may add Your own copyright statement to Your modifications and | |
| 124 | may provide additional or different license terms and conditions | |
| 125 | for use, reproduction, or distribution of Your modifications, or | |
| 126 | for any such Derivative Works as a whole, provided Your use, | |
| 127 | reproduction, and distribution of the Work otherwise complies with | |
| 128 | the conditions stated in this License. | |
| 129 | ||
| 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, | |
| 131 | any Contribution intentionally submitted for inclusion in the Work | |
| 132 | by You to the Licensor shall be under the terms and conditions of | |
| 133 | this License, without any additional terms or conditions. | |
| 134 | Notwithstanding the above, nothing herein shall supersede or modify | |
| 135 | the terms of any separate license agreement you may have executed | |
| 136 | with Licensor regarding such Contributions. | |
| 137 | ||
| 138 | 6. Trademarks. This License does not grant permission to use the trade | |
| 139 | names, trademarks, service marks, or product names of the Licensor, | |
| 140 | except as required for reasonable and customary use in describing the | |
| 141 | origin of the Work and reproducing the content of the NOTICE file. | |
| 142 | ||
| 143 | 7. Disclaimer of Warranty. Unless required by applicable law or | |
| 144 | agreed to in writing, Licensor provides the Work (and each | |
| 145 | Contributor provides its Contributions) on an "AS IS" BASIS, | |
| 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |
| 147 | implied, including, without limitation, any warranties or conditions | |
| 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | |
| 149 | PARTICULAR PURPOSE. You are solely responsible for determining the | |
| 150 | appropriateness of using or redistributing the Work and assume any | |
| 151 | risks associated with Your exercise of permissions under this License. | |
| 152 | ||
| 153 | 8. Limitation of Liability. In no event and under no legal theory, | |
| 154 | whether in tort (including negligence), contract, or otherwise, | |
| 155 | unless required by applicable law (such as deliberate and grossly | |
| 156 | negligent acts) or agreed to in writing, shall any Contributor be | |
| 157 | liable to You for damages, including any direct, indirect, special, | |
| 158 | incidental, or consequential damages of any character arising as a | |
| 159 | result of this License or out of the use or inability to use the | |
| 160 | Work (including but not limited to damages for loss of goodwill, | |
| 161 | work stoppage, computer failure or malfunction, or any and all | |
| 162 | other commercial damages or losses), even if such Contributor | |
| 163 | has been advised of the possibility of such damages. | |
| 164 | ||
| 165 | 9. Accepting Warranty or Additional Liability. While redistributing | |
| 166 | the Work or Derivative Works thereof, You may choose to offer, | |
| 167 | and charge a fee for, acceptance of support, warranty, indemnity, | |
| 168 | or other liability obligations and/or rights consistent with this | |
| 169 | License. However, in accepting such obligations, You may act only | |
| 170 | on Your own behalf and on Your sole responsibility, not on behalf | |
| 171 | of any other Contributor, and only if You agree to indemnify, | |
| 172 | defend, and hold each Contributor harmless for any liability | |
| 173 | incurred by, or claims asserted against, such Contributor by reason | |
| 174 | of your accepting any such warranty or additional liability. | |
| 175 | ||
| 176 | END OF TERMS AND CONDITIONS | |
| 177 | ||
| 178 | APPENDIX: How to apply the Apache License to your work. | |
| 179 | ||
| 180 | To apply the Apache License to your work, attach the following | |
| 181 | boilerplate notice, with the fields enclosed by brackets "[]" | |
| 182 | replaced with your own identifying information. (Don't include | |
| 183 | the brackets!) The text should be enclosed in the appropriate | |
| 184 | comment syntax for the file format. We also recommend that a | |
| 185 | file or class name and description of purpose be included on the | |
| 186 | same "printed page" as the copyright notice for easier | |
| 187 | identification within third-party archives. | |
| 188 | ||
| 189 | Copyright [yyyy] [name of copyright owner] | |
| 190 | ||
| 191 | Licensed under the Apache License, Version 2.0 (the "License"); | |
| 192 | you may not use this file except in compliance with the License. | |
| 193 | You may obtain a copy of the License at | |
| 194 | ||
| 195 | http://www.apache.org/licenses/LICENSE-2.0 | |
| 196 | ||
| 197 | Unless required by applicable law or agreed to in writing, software | |
| 198 | distributed under the License is distributed on an "AS IS" BASIS, | |
| 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 200 | See the License for the specific language governing permissions and | |
| 201 | limitations under the License. |
| … | … | @@ -0,0 +1,73 @@ |
| 1 | # bare-module-lexer | |
| 2 | ||
| 3 | Heuristic lexer for detecting imports and exports in JavaScript modules. It trades off correctness for performance, aiming to reliably support the most common import and export patterns with as little overhead as possible. | |
| 4 | ||
| 5 | ## Usage | |
| 6 | ||
| 7 | ```js | |
| 8 | const lex = require('bare-module-lexer') | |
| 9 | ||
| 10 | lex(` | |
| 11 | const foo = require('./foo.js') | |
| 12 | exports.bar = 42 | |
| 13 | `) | |
| 14 | ||
| 15 | // { | |
| 16 | // imports: [ | |
| 17 | // { specifier: './foo.js', type: REQUIRE, names: [], position: [ 15, 24, 32 ] } | |
| 18 | // ], | |
| 19 | // exports: [ | |
| 20 | // { name: 'bar', position: [ 37, 45, 48 ] } | |
| 21 | // ] | |
| 22 | // } | |
| 23 | ``` | |
| 24 | ||
| 25 | ## API | |
| 26 | ||
| 27 | #### `const { imports, exports } = lex(source[, encoding][, options])` | |
| 28 | ||
| 29 | `imports` is an array of objects with the following shape: | |
| 30 | ||
| 31 | ```js | |
| 32 | imports = { | |
| 33 | specifier: 'string', | |
| 34 | type: number, | |
| 35 | names: ['string'], | |
| 36 | position: [ | |
| 37 | number, // Import start | |
| 38 | number, // Specifier start | |
| 39 | number // Specifier end | |
| 40 | ] | |
| 41 | } | |
| 42 | ``` | |
| 43 | ||
| 44 | `exports` is an array of objects with the following shape: | |
| 45 | ||
| 46 | ```js | |
| 47 | exports = { | |
| 48 | name: 'string', | |
| 49 | position: [ | |
| 50 | number, // Export start | |
| 51 | number, // Name start | |
| 52 | number // Name end | |
| 53 | ] | |
| 54 | } | |
| 55 | ``` | |
| 56 | ||
| 57 | Options are reserved. | |
| 58 | ||
| 59 | #### `lex.constants` | |
| 60 | ||
| 61 | | Constant | Description | | |
| 62 | | :--------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | |
| 63 | | `REQUIRE` | CommonJS `require()`. | | |
| 64 | | `IMPORT` | ES module `import`. | | |
| 65 | | `DYNAMIC` | ES module `import()` if `IMPORT` is set. | | |
| 66 | | `ADDON` | CommonJS `require.addon()` if `REQUIRE` is set, or ES module `import.meta.addon()` if `IMPORT` is set. | | |
| 67 | | `ASSET` | CommonJS `require.asset()` if `REQUIRE` is set, or ES module `import.meta.asset()` if `IMPORT` is set. | | |
| 68 | | `RESOLVE` | CommonJS `require.resolve()` or `require.addon.resolve()` if `REQUIRE` and optionally `ADDON` are set, or ES module `import.meta.resolve()` or `import.meta.addon.resolve()` if `IMPORT` and optionally `ADDON` are set. | | |
| 69 | | `REEXPORT` | Re-export of a CommonJS `require()` if `REQUIRE` is set, or ES module `export from` if `IMPORT` is set. | | |
| 70 | ||
| 71 | ## License | |
| 72 | ||
| 73 | Apache-2.0 |
| … | … | @@ -0,0 +1,86 @@ |
| 1 | #include <assert.h> | |
| 2 | #include <bare.h> | |
| 3 | #include <js.h> | |
| 4 | #include <uv.h> | |
| 5 | ||
| 6 | #include "lib/lex.h" | |
| 7 | ||
| 8 | static js_value_t * | |
| 9 | bare_module_lexer_lex(js_env_t *env, js_callback_info_t *info) { | |
| 10 | int err; | |
| 11 | ||
| 12 | size_t argc = 1; | |
| 13 | js_value_t *argv[1]; | |
| 14 | ||
| 15 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 16 | assert(err == 0); | |
| 17 | ||
| 18 | assert(argc == 1); | |
| 19 | ||
| 20 | utf8_t *input; | |
| 21 | size_t len; | |
| 22 | err = js_get_typedarray_info(env, argv[0], NULL, (void **) &input, (size_t *) &len, NULL, NULL); | |
| 23 | assert(err == 0); | |
| 24 | ||
| 25 | js_value_t *imports; | |
| 26 | err = js_create_array(env, &imports); | |
| 27 | assert(err == 0); | |
| 28 | ||
| 29 | js_value_t *exports; | |
| 30 | err = js_create_array(env, &exports); | |
| 31 | assert(err == 0); | |
| 32 | ||
| 33 | err = bare_module_lexer__lex(env, imports, exports, input, len); | |
| 34 | if (err < 0) return NULL; | |
| 35 | ||
| 36 | js_value_t *result; | |
| 37 | err = js_create_object(env, &result); | |
| 38 | assert(err == 0); | |
| 39 | ||
| 40 | err = js_set_named_property(env, result, "imports", imports); | |
| 41 | assert(err == 0); | |
| 42 | ||
| 43 | err = js_set_named_property(env, result, "exports", exports); | |
| 44 | assert(err == 0); | |
| 45 | ||
| 46 | return result; | |
| 47 | } | |
| 48 | ||
| 49 | static js_value_t * | |
| 50 | bare_module_lexer_exports(js_env_t *env, js_value_t *exports) { | |
| 51 | int err; | |
| 52 | ||
| 53 | #define V(name, fn) \ | |
| 54 | { \ | |
| 55 | js_value_t *val; \ | |
| 56 | err = js_create_function(env, name, -1, fn, NULL, &val); \ | |
| 57 | assert(err == 0); \ | |
| 58 | err = js_set_named_property(env, exports, name, val); \ | |
| 59 | assert(err == 0); \ | |
| 60 | } | |
| 61 | ||
| 62 | V("lex", bare_module_lexer_lex) | |
| 63 | #undef V | |
| 64 | ||
| 65 | #define V(name, n) \ | |
| 66 | { \ | |
| 67 | js_value_t *val; \ | |
| 68 | err = js_create_uint32(env, n, &val); \ | |
| 69 | assert(err == 0); \ | |
| 70 | err = js_set_named_property(env, exports, name, val); \ | |
| 71 | assert(err == 0); \ | |
| 72 | } | |
| 73 | ||
| 74 | V("REQUIRE", bare_module_lexer_require) | |
| 75 | V("IMPORT", bare_module_lexer_import) | |
| 76 | V("DYNAMIC", bare_module_lexer_dynamic) | |
| 77 | V("ADDON", bare_module_lexer_addon) | |
| 78 | V("ASSET", bare_module_lexer_asset) | |
| 79 | V("RESOLVE", bare_module_lexer_resolve) | |
| 80 | V("REEXPORT", bare_module_lexer_reexport) | |
| 81 | #undef V | |
| 82 | ||
| 83 | return exports; | |
| 84 | } | |
| 85 | ||
| 86 | BARE_MODULE(bare_module_lexer, bare_module_lexer_exports) |
| … | … | @@ -0,0 +1,38 @@ |
| 1 | import Buffer, { BufferEncoding } from 'bare-buffer' | |
| 2 | ||
| 3 | interface Import { | |
| 4 | specifier: string | |
| 5 | type: number | |
| 6 | names: string[] | |
| 7 | attributes: { [attribute: string]: string } | |
| 8 | position: [importStart: number, specifierStart: number, specifierEnd: number] | |
| 9 | } | |
| 10 | ||
| 11 | interface Export { | |
| 12 | name: string | |
| 13 | position: [exportStart: number, nameStart: number, nameEnd: number] | |
| 14 | } | |
| 15 | ||
| 16 | declare function lex( | |
| 17 | input: string | Buffer, | |
| 18 | encoding?: BufferEncoding | |
| 19 | ): { | |
| 20 | imports: Import[] | |
| 21 | exports: Export[] | |
| 22 | } | |
| 23 | ||
| 24 | declare namespace lex { | |
| 25 | export { type Import, type Export } | |
| 26 | ||
| 27 | export const constants: { | |
| 28 | REQUIRE: number | |
| 29 | IMPORT: number | |
| 30 | DYNAMIC: number | |
| 31 | ADDON: number | |
| 32 | ASSET: number | |
| 33 | RESOLVE: number | |
| 34 | REEXPORT: number | |
| 35 | } | |
| 36 | } | |
| 37 | ||
| 38 | export = lex |
| … | … | @@ -0,0 +1,56 @@ |
| 1 | const binding = require('#binding') | |
| 2 | ||
| 3 | module.exports = exports = function lex(input, encoding, opts = {}) { | |
| 4 | if (typeof encoding === 'object' && encoding !== null) { | |
| 5 | opts = encoding | |
| 6 | encoding = null | |
| 7 | } | |
| 8 | ||
| 9 | if (typeof input !== 'string' && !ArrayBuffer.isView(input)) { | |
| 10 | throw new TypeError( | |
| 11 | `Input must be a string or buffer. Received type ${typeof input}` | |
| 12 | ) | |
| 13 | } | |
| 14 | ||
| 15 | return binding.lex( | |
| 16 | typeof input === 'string' ? Buffer.from(input, encoding) : input | |
| 17 | ) | |
| 18 | } | |
| 19 | ||
| 20 | exports.constants = { | |
| 21 | /** | |
| 22 | * CommonJS `require()`. | |
| 23 | */ | |
| 24 | REQUIRE: binding.REQUIRE, | |
| 25 | ||
| 26 | /** | |
| 27 | * ES module `import`. | |
| 28 | */ | |
| 29 | IMPORT: binding.IMPORT, | |
| 30 | ||
| 31 | /** | |
| 32 | * ES module `import()` if `IMPORT` is set. | |
| 33 | */ | |
| 34 | DYNAMIC: binding.DYNAMIC, | |
| 35 | ||
| 36 | /** | |
| 37 | * CommonJS `require.addon()` if `REQUIRE` is set, or ES module `import.meta.addon()` if `IMPORT` is set. | |
| 38 | */ | |
| 39 | ADDON: binding.ADDON, | |
| 40 | ||
| 41 | /** | |
| 42 | * CommonJS `require.asset()` if `REQUIRE` is set, or ES module `import.meta.asset()` if `IMPORT` is set. | |
| 43 | */ | |
| 44 | ASSET: binding.ASSET, | |
| 45 | ||
| 46 | /** | |
| 47 | * CommonJS `require.resolve()` or `require.addon.resolve()` if `REQUIRE` and optionally `ADDON` are set, or ES module | |
| 48 | * `import.meta.resolve()` or `import.meta.addon.resolve()` if `IMPORT` and optionally `ADDON` are set. | |
| 49 | */ | |
| 50 | RESOLVE: binding.RESOLVE, | |
| 51 | ||
| 52 | /** | |
| 53 | * Re-export of a CommonJS `require()` if `REQUIRE` is set. | |
| 54 | */ | |
| 55 | REEXPORT: binding.REEXPORT | |
| 56 | } |
| … | … | @@ -0,0 +1 @@ |
| 1 | module.exports = require.addon('../..') |
| … | … | @@ -0,0 +1,3 @@ |
| 1 | require.addon = require('require-addon') | |
| 2 | ||
| 3 | module.exports = require.addon('../..', __filename) |
| … | … | @@ -0,0 +1,1124 @@ |
| 1 | #ifndef BARE_MODULE_LEXER_H | |
| 2 | #define BARE_MODULE_LEXER_H | |
| 3 | ||
| 4 | #include <assert.h> | |
| 5 | #include <js.h> | |
| 6 | #include <stdbool.h> | |
| 7 | #include <stddef.h> | |
| 8 | #include <string.h> | |
| 9 | #include <utf.h> | |
| 10 | ||
| 11 | enum { | |
| 12 | bare_module_lexer_require = 0x1, | |
| 13 | bare_module_lexer_import = 0x2, | |
| 14 | bare_module_lexer_dynamic = 0x4, | |
| 15 | bare_module_lexer_addon = 0x8, | |
| 16 | bare_module_lexer_asset = 0x10, | |
| 17 | bare_module_lexer_resolve = 0x20, | |
| 18 | bare_module_lexer_reexport = 0x40, | |
| 19 | }; | |
| 20 | ||
| 21 | static inline bool | |
| 22 | bare_module_lexer__is_line_terminator(uint8_t c) { | |
| 23 | return c == 0xa || c == 0xd; | |
| 24 | } | |
| 25 | ||
| 26 | static inline bool | |
| 27 | bare_module_lexer__is_whitespace(uint8_t c) { | |
| 28 | return c == ' ' || c == '\t' || c == 0xb || c == 0xc || c == 0xa0 || bare_module_lexer__is_line_terminator(c); | |
| 29 | } | |
| 30 | ||
| 31 | static inline bool | |
| 32 | bare_module_lexer__is_id_start(uint8_t c) { | |
| 33 | return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' || c == '$'; | |
| 34 | } | |
| 35 | ||
| 36 | static inline bool | |
| 37 | bare_module_lexer__is_id(uint8_t c) { | |
| 38 | return bare_module_lexer__is_id_start(c) || (c >= '0' && c <= '9'); | |
| 39 | } | |
| 40 | ||
| 41 | static inline int | |
| 42 | bare_module_lexer__add_position(js_env_t *env, js_value_t *entry, size_t statement_start, size_t input_start, size_t input_end) { | |
| 43 | int err; | |
| 44 | ||
| 45 | js_value_t *position; | |
| 46 | err = js_create_array_with_length(env, 3, &position); | |
| 47 | assert(err == 0); | |
| 48 | ||
| 49 | #define V(i, n) \ | |
| 50 | { \ | |
| 51 | js_value_t *val; \ | |
| 52 | err = js_create_int64(env, n, &val); \ | |
| 53 | assert(err == 0); \ | |
| 54 | err = js_set_element(env, position, i, val); \ | |
| 55 | assert(err == 0); \ | |
| 56 | } | |
| 57 | ||
| 58 | V(0, statement_start); | |
| 59 | V(1, input_start); | |
| 60 | V(2, input_end); | |
| 61 | #undef V | |
| 62 | ||
| 63 | err = js_set_named_property(env, entry, "position", position); | |
| 64 | assert(err == 0); | |
| 65 | ||
| 66 | return 0; | |
| 67 | } | |
| 68 | ||
| 69 | static inline int | |
| 70 | bare_module_lexer__add_import(js_env_t *env, js_value_t *imports, uint32_t *i, const utf8_t *source, size_t import_start, size_t specifier_start, size_t specifier_end, int type, js_value_t *names, js_value_t *attributes) { | |
| 71 | assert(specifier_end >= specifier_start); | |
| 72 | ||
| 73 | int err; | |
| 74 | ||
| 75 | js_value_t *entry; | |
| 76 | err = js_create_object(env, &entry); | |
| 77 | assert(err == 0); | |
| 78 | ||
| 79 | err = js_set_element(env, imports, *i, entry); | |
| 80 | assert(err == 0); | |
| 81 | ||
| 82 | #define V(key, fn, ...) \ | |
| 83 | { \ | |
| 84 | js_value_t *val; \ | |
| 85 | err = fn(env, __VA_ARGS__, &val); \ | |
| 86 | assert(err == 0); \ | |
| 87 | err = js_set_named_property(env, entry, key, val); \ | |
| 88 | assert(err == 0); \ | |
| 89 | } | |
| 90 | ||
| 91 | V("specifier", js_create_string_utf8, &source[specifier_start], specifier_end - specifier_start); | |
| 92 | V("type", js_create_uint32, type); | |
| 93 | #undef V | |
| 94 | ||
| 95 | if (names == NULL) { | |
| 96 | err = js_create_array(env, &names); | |
| 97 | assert(err == 0); | |
| 98 | } | |
| 99 | ||
| 100 | err = js_set_named_property(env, entry, "names", names); | |
| 101 | assert(err == 0); | |
| 102 | ||
| 103 | if (attributes == NULL) { | |
| 104 | err = js_create_object(env, &attributes); | |
| 105 | assert(err == 0); | |
| 106 | } | |
| 107 | ||
| 108 | err = js_set_named_property(env, entry, "attributes", attributes); | |
| 109 | assert(err == 0); | |
| 110 | ||
| 111 | err = bare_module_lexer__add_position(env, entry, import_start, specifier_start, specifier_end); | |
| 112 | assert(err == 0); | |
| 113 | ||
| 114 | *i += 1; | |
| 115 | ||
| 116 | return 0; | |
| 117 | } | |
| 118 | ||
| 119 | static inline int | |
| 120 | bare_module_lexer__add_export(js_env_t *env, js_value_t *exports, uint32_t *i, const utf8_t *source, size_t export_start, size_t name_start, size_t name_end) { | |
| 121 | assert(name_end >= name_start); | |
| 122 | ||
| 123 | int err; | |
| 124 | ||
| 125 | js_value_t *entry; | |
| 126 | err = js_create_object(env, &entry); | |
| 127 | assert(err == 0); | |
| 128 | ||
| 129 | err = js_set_element(env, exports, *i, entry); | |
| 130 | assert(err == 0); | |
| 131 | ||
| 132 | #define V(key, fn, ...) \ | |
| 133 | { \ | |
| 134 | js_value_t *val; \ | |
| 135 | err = fn(env, __VA_ARGS__, &val); \ | |
| 136 | assert(err == 0); \ | |
| 137 | err = js_set_named_property(env, entry, key, val); \ | |
| 138 | assert(err == 0); \ | |
| 139 | } | |
| 140 | ||
| 141 | V("name", js_create_string_utf8, &source[name_start], name_end - name_start); | |
| 142 | #undef V | |
| 143 | ||
| 144 | err = bare_module_lexer__add_position(env, entry, export_start, name_start, name_end); | |
| 145 | assert(err == 0); | |
| 146 | ||
| 147 | *i += 1; | |
| 148 | ||
| 149 | return 0; | |
| 150 | } | |
| 151 | ||
| 152 | static inline int | |
| 153 | bare_module_lexer__add_name(js_env_t *env, js_value_t **names, uint32_t *i, const utf8_t *name, size_t name_len) { | |
| 154 | int err; | |
| 155 | ||
| 156 | if (*names == NULL) { | |
| 157 | err = js_create_array(env, names); | |
| 158 | assert(err == 0); | |
| 159 | ||
| 160 | *i = 0; | |
| 161 | } | |
| 162 | ||
| 163 | js_value_t *string; | |
| 164 | err = js_create_string_utf8(env, name, name_len, &string); | |
| 165 | assert(err == 0); | |
| 166 | ||
| 167 | err = js_set_element(env, *names, *i, string); | |
| 168 | assert(err == 0); | |
| 169 | ||
| 170 | *i += 1; | |
| 171 | ||
| 172 | return 0; | |
| 173 | } | |
| 174 | ||
| 175 | static inline int | |
| 176 | bare_module_lexer__add_attribute(js_env_t *env, js_value_t **attributes, const utf8_t *key, size_t key_len, const utf8_t *value, size_t value_len) { | |
| 177 | int err; | |
| 178 | ||
| 179 | if (*attributes == NULL) { | |
| 180 | err = js_create_object(env, attributes); | |
| 181 | assert(err == 0); | |
| 182 | } | |
| 183 | ||
| 184 | js_value_t *property; | |
| 185 | err = js_create_property_key_utf8(env, key, key_len, &property); | |
| 186 | assert(err == 0); | |
| 187 | ||
| 188 | js_value_t *string; | |
| 189 | err = js_create_string_utf8(env, value, value_len, &string); | |
| 190 | assert(err == 0); | |
| 191 | ||
| 192 | err = js_set_property(env, *attributes, property, string); | |
| 193 | assert(err == 0); | |
| 194 | ||
| 195 | return 0; | |
| 196 | } | |
| 197 | ||
| 198 | // Current character, unchecked | |
| 199 | #define u(offset) ((uint8_t) s[i + offset]) | |
| 200 | ||
| 201 | // Current character, checked | |
| 202 | #define c(offset) (i + offset < n ? u(offset) : -1) | |
| 203 | ||
| 204 | // Begins with string, unchecked | |
| 205 | #define bu(t, l) (strncmp((const char *) &s[i], t, l) == 0) | |
| 206 | ||
| 207 | // Begins with string, checked | |
| 208 | #define bc(t, l) (i + l < n && bu(t, l)) | |
| 209 | ||
| 210 | #define lt bare_module_lexer__is_line_terminator | |
| 211 | #define ws bare_module_lexer__is_whitespace | |
| 212 | #define ids bare_module_lexer__is_id_start | |
| 213 | #define id bare_module_lexer__is_id | |
| 214 | ||
| 215 | static inline int | |
| 216 | bare_module_lexer__lex_regex(js_env_t *env, const utf8_t *s, size_t n, size_t i, size_t *result) { | |
| 217 | i++; | |
| 218 | ||
| 219 | bool in_character_class = false; | |
| 220 | ||
| 221 | while (i < n) { | |
| 222 | if (in_character_class) { | |
| 223 | if (u(0) == ']') { | |
| 224 | in_character_class = false; | |
| 225 | } else if (c(0) == '\\') { | |
| 226 | i++; | |
| 227 | } | |
| 228 | } else if (u(0) == '/') { | |
| 229 | i++; | |
| 230 | ||
| 231 | while (i < n && u(0) >= 'a' && u(0) <= 'z') i++; | |
| 232 | ||
| 233 | *result = i; | |
| 234 | ||
| 235 | return 0; | |
| 236 | } else if (u(0) == '[') { | |
| 237 | in_character_class = true; | |
| 238 | } else if (u(0) == '\\') { | |
| 239 | i++; | |
| 240 | } else if (u(0) == ';' || lt(u(0))) { | |
| 241 | return -1; | |
| 242 | } | |
| 243 | ||
| 244 | i++; | |
| 245 | } | |
| 246 | ||
| 247 | return -1; | |
| 248 | } | |
| 249 | ||
| 250 | static inline int | |
| 251 | bare_module_lexer__lex_import_attributes(js_env_t *env, js_value_t **attributes, const utf8_t *s, size_t n, size_t i, size_t *result) { | |
| 252 | int err; | |
| 253 | ||
| 254 | size_t ks; // Key start | |
| 255 | size_t ke; // Key end | |
| 256 | ||
| 257 | size_t vs; // Value start | |
| 258 | size_t ve; // Value end | |
| 259 | ||
| 260 | while (i < n && ws(u(0))) i++; | |
| 261 | ||
| 262 | if (c(0) == '{') { | |
| 263 | i++; | |
| 264 | ||
| 265 | while (c(0) != '}') { | |
| 266 | while (i < n && ws(u(0))) i++; | |
| 267 | ||
| 268 | if (ids(c(0))) { | |
| 269 | ks = i++; | |
| 270 | ||
| 271 | while (id(c(0))) i++; | |
| 272 | ||
| 273 | ke = i; | |
| 274 | } else if (c(0) == '\'' || c(0) == '"') { | |
| 275 | utf8_t e = u(0); | |
| 276 | ||
| 277 | ks = ++i; | |
| 278 | ||
| 279 | while (i < n && u(0) != e) i++; | |
| 280 | ||
| 281 | if (c(0) != e) break; | |
| 282 | ||
| 283 | ke = i++; | |
| 284 | } else break; | |
| 285 | ||
| 286 | while (i < n && ws(u(0))) i++; | |
| 287 | ||
| 288 | if (c(0) != ':') break; | |
| 289 | ||
| 290 | i++; | |
| 291 | ||
| 292 | while (i < n && ws(u(0))) i++; | |
| 293 | ||
| 294 | if (c(0) != '\'' && c(0) != '"') break; | |
| 295 | ||
| 296 | utf8_t e = u(0); | |
| 297 | ||
| 298 | vs = ++i; | |
| 299 | ||
| 300 | while (i < n && u(0) != e) i++; | |
| 301 | ||
| 302 | if (c(0) != e) break; | |
| 303 | ||
| 304 | ve = i++; | |
| 305 | ||
| 306 | err = bare_module_lexer__add_attribute(env, attributes, &s[ks], ke - ks, &s[vs], ve - vs); | |
| 307 | if (err < 0) return err; | |
| 308 | ||
| 309 | if (c(0) != ',') break; | |
| 310 | ||
| 311 | i++; | |
| 312 | } | |
| 313 | } | |
| 314 | ||
| 315 | *result = i; | |
| 316 | ||
| 317 | return 0; | |
| 318 | } | |
| 319 | ||
| 320 | static inline int | |
| 321 | bare_module_lexer__lex(js_env_t *env, js_value_t *imports, js_value_t *exports, const utf8_t *s, size_t n) { | |
| 322 | int err; | |
| 323 | ||
| 324 | size_t i = 0; | |
| 325 | ||
| 326 | size_t is; // Import start | |
| 327 | uint32_t il = 0; // Import count | |
| 328 | ||
| 329 | size_t es; // Export start | |
| 330 | uint32_t el = 0; // Export count | |
| 331 | ||
| 332 | size_t ss; // Source start | |
| 333 | size_t se; // Source end | |
| 334 | ||
| 335 | int type; | |
| 336 | ||
| 337 | js_value_t *names; | |
| 338 | uint32_t nl; // Names count | |
| 339 | ||
| 340 | js_value_t *attributes; | |
| 341 | ||
| 342 | while (i < n) { | |
| 343 | while (i < n && ws(u(0))) i++; | |
| 344 | ||
| 345 | if (i + 8 >= n) break; | |
| 346 | ||
| 347 | if (bu("//", 2)) { | |
| 348 | i += 2; | |
| 349 | ||
| 350 | while (i < n && !lt(u(0))) i++; | |
| 351 | ||
| 352 | if (lt(c(0))) i++; | |
| 353 | ||
| 354 | continue; | |
| 355 | } | |
| 356 | ||
| 357 | if (bu("/*", 2)) { | |
| 358 | i += 2; | |
| 359 | ||
| 360 | while (i + 1 < n && !bu("*/", 2)) i++; | |
| 361 | ||
| 362 | if (bc("*/", 2)) i += 2; | |
| 363 | ||
| 364 | continue; | |
| 365 | } | |
| 366 | ||
| 367 | if (u(0) == '\'' || u(0) == '"' || u(0) == '`') { | |
| 368 | utf8_t e = u(0); | |
| 369 | ||
| 370 | i++; | |
| 371 | ||
| 372 | while (i < n) { | |
| 373 | if (u(0) == '\\') i += 2; | |
| 374 | else if (u(0) != e) i++; | |
| 375 | else break; | |
| 376 | } | |
| 377 | ||
| 378 | if (c(0) == e) i++; | |
| 379 | ||
| 380 | continue; | |
| 381 | } | |
| 382 | ||
| 383 | if (u(0) == '/') { | |
| 384 | err = bare_module_lexer__lex_regex(env, s, n, i, &i); | |
| 385 | if (err < 0) i++; | |
| 386 | ||
| 387 | continue; | |
| 388 | } | |
| 389 | ||
| 390 | type = 0; | |
| 391 | names = NULL; | |
| 392 | attributes = NULL; | |
| 393 | ||
| 394 | if (bu("require", 7)) { | |
| 395 | is = i; | |
| 396 | ||
| 397 | i += 7; | |
| 398 | ||
| 399 | goto require; | |
| 400 | } | |
| 401 | ||
| 402 | else if (bu("import", 6)) { | |
| 403 | type |= bare_module_lexer_import; | |
| 404 | is = i; | |
| 405 | ||
| 406 | i += 6; | |
| 407 | ||
| 408 | while (i < n && ws(u(0))) i++; | |
| 409 | ||
| 410 | // import \* | |
| 411 | if (c(0) == '*') { | |
| 412 | i++; | |
| 413 | ||
| 414 | while (i < n && ws(u(0))) i++; | |
| 415 | ||
| 416 | // import \* as | |
| 417 | if (i + 3 < n && bu("as", 2) && ws(u(2))) { | |
| 418 | i += 3; | |
| 419 | ||
| 420 | while (i < n && ws(u(0))) i++; | |
| 421 | ||
| 422 | // import \* as [^\s]+ | |
| 423 | while (i < n && !ws(u(0))) i++; | |
| 424 | ||
| 425 | while (i < n && ws(u(0))) i++; | |
| 426 | ||
| 427 | // import \* as [^\s]+ from | |
| 428 | if (bc("from", 4)) { | |
| 429 | i += 4; | |
| 430 | ||
| 431 | err = bare_module_lexer__add_name(env, &names, &nl, (const utf8_t *) "*", -1); | |
| 432 | assert(err == 0); | |
| 433 | ||
| 434 | goto from; | |
| 435 | } | |
| 436 | } | |
| 437 | } | |
| 438 | ||
| 439 | // import { | |
| 440 | else if (c(0) == '{') { | |
| 441 | i++; | |
| 442 | ||
| 443 | // import {[^}]* | |
| 444 | while (i < n && u(0) != '}') i++; | |
| 445 | ||
| 446 | // import {[^}]*} | |
| 447 | if (c(0) == '}') { | |
| 448 | i++; | |
| 449 | ||
| 450 | while (i < n && ws(u(0))) i++; | |
| 451 | ||
| 452 | // import {[^}]*} from | |
| 453 | if (bc("from", 4)) { | |
| 454 | i += 4; | |
| 455 | ||
| 456 | goto from; | |
| 457 | } | |
| 458 | } | |
| 459 | } | |
| 460 | ||
| 461 | // import ['"] | |
| 462 | else if (c(0) == '\'' || c(0) == '"') { | |
| 463 | utf8_t e = u(0); | |
| 464 | ||
| 465 | ss = ++i; | |
| 466 | ||
| 467 | while (i < n && u(0) != e) i++; | |
| 468 | ||
| 469 | // import ['"].*['"] | |
| 470 | if (c(0) == e) { | |
| 471 | se = i; | |
| 472 | ||
| 473 | i++; | |
| 474 | ||
| 475 | err = bare_module_lexer__add_import(env, imports, &il, s, is, ss, se, type, names, attributes); | |
| 476 | if (err < 0) goto err; | |
| 477 | } | |
| 478 | } | |
| 479 | ||
| 480 | // import\( | |
| 481 | else if (c(0) == '(') { | |
| 482 | type |= bare_module_lexer_dynamic; | |
| 483 | ||
| 484 | i++; | |
| 485 | ||
| 486 | while (i < n && ws(u(0))) i++; | |
| 487 | ||
| 488 | // import\(['"] | |
| 489 | if (c(0) == '\'' || c(0) == '"') { | |
| 490 | utf8_t e = u(0); | |
| 491 | ||
| 492 | ss = ++i; | |
| 493 | ||
| 494 | while (i < n && u(0) != e) i++; | |
| 495 | ||
| 496 | // import\(['"].*['"] | |
| 497 | if (c(0) == e) { | |
| 498 | se = i; | |
| 499 | ||
| 500 | i++; | |
| 501 | ||
| 502 | while (i < n && ws(u(0))) i++; | |
| 503 | ||
| 504 | while (i < n && u(0) != ')') i++; | |
| 505 | ||
| 506 | // import\(['"].*['"][^)]*\) | |
| 507 | if (c(0) == ')') { | |
| 508 | i++; | |
| 509 | ||
| 510 | err = bare_module_lexer__add_import(env, imports, &il, s, is, ss, se, type, names, attributes); | |
| 511 | if (err < 0) goto err; | |
| 512 | } | |
| 513 | } | |
| 514 | } | |
| 515 | } | |
| 516 | ||
| 517 | // import\. | |
| 518 | else if (c(0) == '.') { | |
| 519 | i++; | |
| 520 | ||
| 521 | while (i < n && ws(u(0))) i++; | |
| 522 | ||
| 523 | // import\.meta | |
| 524 | if (bc("meta", 4)) { | |
| 525 | i += 4; | |
| 526 | ||
| 527 | while (i < n && ws(u(0))) i++; | |
| 528 | ||
| 529 | // import\.meta\. | |
| 530 | if (c(0) == '.') { | |
| 531 | i++; | |
| 532 | ||
| 533 | // import\.meta\.a | |
| 534 | if (i + 5 < n && u(0) == 'a') { | |
| 535 | i++; | |
| 536 | ||
| 537 | // import\.meta\.addon | |
| 538 | if (bu("ddon", 4)) { | |
| 539 | i += 4; | |
| 540 | ||
| 541 | while (i < n && ws(u(0))) i++; | |
| 542 | ||
| 543 | type |= bare_module_lexer_addon; | |
| 544 | ||
| 545 | // import\.meta\.addon\. | |
| 546 | if (c(0) == '.') { | |
| 547 | i++; | |
| 548 | ||
| 549 | while (i < n && ws(u(0))) i++; | |
| 550 | ||
| 551 | // import\.meta\.addon\.resolve | |
| 552 | if (i + 7 < n && bu("resolve", 7)) { | |
| 553 | i += 7; | |
| 554 | ||
| 555 | while (i < n && ws(u(0))) i++; | |
| 556 | ||
| 557 | type |= bare_module_lexer_resolve; | |
| 558 | } | |
| 559 | ||
| 560 | else continue; | |
| 561 | } | |
| 562 | } | |
| 563 | ||
| 564 | // import\.meta\.asset | |
| 565 | else if (bu("sset", 4)) { | |
| 566 | i += 4; | |
| 567 | ||
| 568 | while (i < n && ws(u(0))) i++; | |
| 569 | ||
| 570 | type |= bare_module_lexer_asset; | |
| 571 | } | |
| 572 | } | |
| 573 | ||
| 574 | // import\.meta\.resolve | |
| 575 | else if (i + 7 < n && bu("resolve", 7)) { | |
| 576 | i += 7; | |
| 577 | ||
| 578 | while (i < n && ws(u(0))) i++; | |
| 579 | ||
| 580 | type |= bare_module_lexer_resolve; | |
| 581 | } | |
| 582 | ||
| 583 | else continue; | |
| 584 | ||
| 585 | // import\.meta\.(resolve|addon(\.resolve)?|asset)\( | |
| 586 | if (c(0) == '(') { | |
| 587 | i++; | |
| 588 | ||
| 589 | while (i < n && ws(u(0))) i++; | |
| 590 | ||
| 591 | // import\.meta\.(resolve|addon(\.resolve)?|asset)\(['"] | |
| 592 | if (c(0) == '\'' || c(0) == '"') { | |
| 593 | utf8_t e = u(0); | |
| 594 | ||
| 595 | ss = ++i; | |
| 596 | ||
| 597 | while (i < n && u(0) != e) i++; | |
| 598 | ||
| 599 | // import\.meta\.(resolve|addon(\.resolve)?|asset)\(['"].*['"] | |
| 600 | if (c(0) == e) { | |
| 601 | se = i; | |
| 602 | ||
| 603 | i++; | |
| 604 | ||
| 605 | while (i < n && u(0) != ')') i++; | |
| 606 | ||
| 607 | // import\.meta\.(resolve|addon(\.resolve)?|asset)\(['"].*['"][^)]*\) | |
| 608 | if (c(0) == ')') { | |
| 609 | i++; | |
| 610 | ||
| 611 | err = bare_module_lexer__add_import(env, imports, &il, s, is, ss, se, type, names, attributes); | |
| 612 | if (err < 0) goto err; | |
| 613 | } | |
| 614 | } | |
| 615 | } | |
| 616 | ||
| 617 | // import\.meta\.addon(\.resolve)?\(\) | |
| 618 | else if (c(0) == ')' && (type & bare_module_lexer_addon)) { | |
| 619 | ss = se = i++; | |
| 620 | ||
| 621 | err = bare_module_lexer__add_import(env, imports, &il, s, is, ss, se, type, names, attributes); | |
| 622 | if (err < 0) goto err; | |
| 623 | } | |
| 624 | } | |
| 625 | } | |
| 626 | } | |
| 627 | } | |
| 628 | ||
| 629 | // import\s | |
| 630 | else if (ws(c(-1))) { | |
| 631 | size_t j = i; | |
| 632 | ||
| 633 | // import [^\s,]+ | |
| 634 | while (i < n && !ws(u(0)) && u(0) != ',') i++; | |
| 635 | ||
| 636 | if (j < i) { | |
| 637 | while (i < n && ws(u(0))) i++; | |
| 638 | ||
| 639 | // import [^\s,]+ from | |
| 640 | if (bc("from", 4)) { | |
| 641 | i += 4; | |
| 642 | ||
| 643 | err = bare_module_lexer__add_name(env, &names, &nl, (const utf8_t *) "default", -1); | |
| 644 | assert(err == 0); | |
| 645 | ||
| 646 | goto from; | |
| 647 | } | |
| 648 | ||
| 649 | // import [^\s,]+, | |
| 650 | else if (c(0) == ',') { | |
| 651 | i++; | |
| 652 | ||
| 653 | while (i < n && ws(u(0))) i++; | |
| 654 | ||
| 655 | // import [^\s,]+, { | |
| 656 | if (c(0) == '{') { | |
| 657 | i++; | |
| 658 | ||
| 659 | // import [^\s,]+, {[^}]* | |
| 660 | while (i < n && u(0) != '}') i++; | |
| 661 | ||
| 662 | // import [^\s,]+, {[^}]*} | |
| 663 | if (c(0) == '}') { | |
| 664 | i++; | |
| 665 | ||
| 666 | while (i < n && ws(u(0))) i++; | |
| 667 | ||
| 668 | // import [^\s,]+, {[^}]*} from | |
| 669 | if (bc("from", 4)) { | |
| 670 | i += 4; | |
| 671 | ||
| 672 | err = bare_module_lexer__add_name(env, &names, &nl, (const utf8_t *) "default", -1); | |
| 673 | assert(err == 0); | |
| 674 | ||
| 675 | goto from; | |
| 676 | } | |
| 677 | } | |
| 678 | } | |
| 679 | } | |
| 680 | } | |
| 681 | } | |
| 682 | } | |
| 683 | ||
| 684 | else if (bu("module", 6)) { | |
| 685 | es = i; | |
| 686 | ||
| 687 | i += 6; | |
| 688 | ||
| 689 | while (i < n && ws(u(0))) i++; | |
| 690 | ||
| 691 | // module\. | |
| 692 | if (c(0) == '.') { | |
| 693 | i++; | |
| 694 | ||
| 695 | while (i < n && ws(u(0))) i++; | |
| 696 | ||
| 697 | // module\.exports | |
| 698 | if (bc("exports", 7)) { | |
| 699 | i += 7; | |
| 700 | ||
| 701 | goto exports; | |
| 702 | } | |
| 703 | } | |
| 704 | } | |
| 705 | ||
| 706 | else if (bu("export", 6)) { | |
| 707 | es = i; | |
| 708 | ||
| 709 | i += 6; | |
| 710 | ||
| 711 | // exports | |
| 712 | if (c(0) == 's') { | |
| 713 | i++; | |
| 714 | ||
| 715 | goto exports; | |
| 716 | } | |
| 717 | ||
| 718 | while (i < n && ws(u(0))) i++; | |
| 719 | ||
| 720 | // export \* | |
| 721 | if (c(0) == '*') { | |
| 722 | i++; | |
| 723 | ||
| 724 | while (i < n && ws(u(0))) i++; | |
| 725 | ||
| 726 | // export \* as | |
| 727 | if (i + 3 < n && bu("as", 2) && ws(u(2))) { | |
| 728 | i += 3; | |
| 729 | ||
| 730 | while (i < n && ws(u(0))) i++; | |
| 731 | ||
| 732 | // export \* as [^\s]+ | |
| 733 | while (i < n && !ws(u(0))) i++; | |
| 734 | ||
| 735 | while (i < n && ws(u(0))) i++; | |
| 736 | ||
| 737 | // export \* as [^\s]+ from | |
| 738 | if (bc("from", 4)) { | |
| 739 | type |= bare_module_lexer_import | bare_module_lexer_reexport; | |
| 740 | is = es; | |
| 741 | ||
| 742 | i += 4; | |
| 743 | ||
| 744 | err = bare_module_lexer__add_name(env, &names, &nl, (const utf8_t *) "*", -1); | |
| 745 | assert(err == 0); | |
| 746 | ||
| 747 | goto from; | |
| 748 | } | |
| 749 | } | |
| 750 | ||
| 751 | // export \* from | |
| 752 | else if (bc("from", 4)) { | |
| 753 | type |= bare_module_lexer_import | bare_module_lexer_reexport; | |
| 754 | is = es; | |
| 755 | ||
| 756 | i += 4; | |
| 757 | ||
| 758 | err = bare_module_lexer__add_name(env, &names, &nl, (const utf8_t *) "*", -1); | |
| 759 | assert(err == 0); | |
| 760 | ||
| 761 | goto from; | |
| 762 | } | |
| 763 | } | |
| 764 | ||
| 765 | // export { | |
| 766 | else if (c(0) == '{') { | |
| 767 | i++; | |
| 768 | ||
| 769 | // export {[^}]* | |
| 770 | while (i < n && u(0) != '}') i++; | |
| 771 | ||
| 772 | // export {[^}]*} | |
| 773 | if (c(0) == '}') { | |
| 774 | i++; | |
| 775 | ||
| 776 | while (i < n && ws(u(0))) i++; | |
| 777 | ||
| 778 | // export {[^}]*} from | |
| 779 | if (bc("from", 4)) { | |
| 780 | type |= bare_module_lexer_import | bare_module_lexer_reexport; | |
| 781 | is = es; | |
| 782 | ||
| 783 | i += 4; | |
| 784 | ||
| 785 | goto from; | |
| 786 | } | |
| 787 | } | |
| 788 | } | |
| 789 | } | |
| 790 | ||
| 791 | else if (bu("__export", 8)) { | |
| 792 | es = i; | |
| 793 | ||
| 794 | i += 8; | |
| 795 | ||
| 796 | // __exportStar | |
| 797 | if (bc("Star", 4)) i += 4; | |
| 798 | ||
| 799 | while (i < n && ws(u(0))) i++; | |
| 800 | ||
| 801 | // __export(Star)?\( | |
| 802 | if (c(0) == '(') { | |
| 803 | i++; | |
| 804 | ||
| 805 | while (i < n && ws(u(0))) i++; | |
| 806 | ||
| 807 | // __export(Star)?\(require | |
| 808 | if (bc("require", 7)) { | |
| 809 | type |= bare_module_lexer_reexport; | |
| 810 | is = i; | |
| 811 | ||
| 812 | i += 7; | |
| 813 | ||
| 814 | goto require; | |
| 815 | } | |
| 816 | } | |
| 817 | } | |
| 818 | ||
| 819 | else i++; | |
| 820 | ||
| 821 | continue; | |
| 822 | ||
| 823 | require: | |
| 824 | type |= bare_module_lexer_require; | |
| 825 | ||
| 826 | while (i < n && ws(u(0))) i++; | |
| 827 | ||
| 828 | // require\. | |
| 829 | if (c(0) == '.') { | |
| 830 | i++; | |
| 831 | ||
| 832 | while (i < n && ws(u(0))) i++; | |
| 833 | ||
| 834 | // require\.a | |
| 835 | if (i + 5 < n && u(0) == 'a') { | |
| 836 | i++; | |
| 837 | ||
| 838 | // require\.addon | |
| 839 | if (bu("ddon", 4)) { | |
| 840 | i += 4; | |
| 841 | ||
| 842 | while (i < n && ws(u(0))) i++; | |
| 843 | ||
| 844 | type |= bare_module_lexer_addon; | |
| 845 | ||
| 846 | // require\.addon\. | |
| 847 | if (c(0) == '.') { | |
| 848 | i++; | |
| 849 | ||
| 850 | while (i < n && ws(u(0))) i++; | |
| 851 | ||
| 852 | // require\.addon\.resolve | |
| 853 | if (i + 7 < n && bu("resolve", 7)) { | |
| 854 | i += 7; | |
| 855 | ||
| 856 | while (i < n && ws(u(0))) i++; | |
| 857 | ||
| 858 | type |= bare_module_lexer_resolve; | |
| 859 | } | |
| 860 | ||
| 861 | else continue; | |
| 862 | } | |
| 863 | } | |
| 864 | ||
| 865 | // require\.asset | |
| 866 | else if (bu("sset", 4)) { | |
| 867 | i += 4; | |
| 868 | ||
| 869 | while (i < n && ws(u(0))) i++; | |
| 870 | ||
| 871 | type |= bare_module_lexer_asset; | |
| 872 | } | |
| 873 | } | |
| 874 | ||
| 875 | // require\.resolve | |
| 876 | else if (i + 7 < n && bu("resolve", 7)) { | |
| 877 | i += 7; | |
| 878 | ||
| 879 | while (i < n && ws(u(0))) i++; | |
| 880 | ||
| 881 | type |= bare_module_lexer_resolve; | |
| 882 | } | |
| 883 | ||
| 884 | else continue; | |
| 885 | } | |
| 886 | ||
| 887 | // require(\.(resolve|addon(\.resolve)?|asset))?\( | |
| 888 | if (c(0) == '(') { | |
| 889 | i++; | |
| 890 | ||
| 891 | while (i < n && ws(u(0))) i++; | |
| 892 | ||
| 893 | // require(\.(resolve|addon(\.resolve)?|asset))?\(['"] | |
| 894 | if (c(0) == '\'' || c(0) == '"') { | |
| 895 | utf8_t e = u(0); | |
| 896 | ||
| 897 | ss = ++i; | |
| 898 | ||
| 899 | while (i < n && u(0) != e) i++; | |
| 900 | ||
| 901 | // require(\.(resolve|addon(\.resolve)?|asset))?\(['"].*['"] | |
| 902 | if (c(0) == e) { | |
| 903 | se = i; | |
| 904 | ||
| 905 | i++; | |
| 906 | ||
| 907 | while (i < n && ws(u(0))) i++; | |
| 908 | ||
| 909 | if (c(0) == ',') { | |
| 910 | i++; | |
| 911 | ||
| 912 | while (i < n && ws(u(0))) i++; | |
| 913 | ||
| 914 | if (c(0) == '{') { | |
| 915 | i++; | |
| 916 | ||
| 917 | while (i < n && ws(u(0))) i++; | |
| 918 | ||
| 919 | if (bc("with", 4)) { | |
| 920 | i += 4; | |
| 921 | ||
| 922 | while (i < n && ws(u(0))) i++; | |
| 923 | ||
| 924 | if (c(0) == ':') { | |
| 925 | i++; | |
| 926 | ||
| 927 | err = bare_module_lexer__lex_import_attributes(env, &attributes, s, n, i, &i); | |
| 928 | if (err < 0) goto err; | |
| 929 | } | |
| 930 | } | |
| 931 | } | |
| 932 | } | |
| 933 | ||
| 934 | while (i < n && u(0) != ')') i++; | |
| 935 | ||
| 936 | // require(\.(resolve|addon(\.resolve)?|asset))?\(['"].*['"][^)]*\) | |
| 937 | if (c(0) == ')') { | |
| 938 | i++; | |
| 939 | ||
| 940 | err = bare_module_lexer__add_import(env, imports, &il, s, is, ss, se, type, names, attributes); | |
| 941 | if (err < 0) goto err; | |
| 942 | } | |
| 943 | } | |
| 944 | } | |
| 945 | ||
| 946 | // require\.addon(\.resolve)?\(\) | |
| 947 | else if (c(0) == ')' && (type & bare_module_lexer_addon)) { | |
| 948 | ss = se = i++; | |
| 949 | ||
| 950 | err = bare_module_lexer__add_import(env, imports, &il, s, is, ss, se, type, names, attributes); | |
| 951 | if (err < 0) goto err; | |
| 952 | } | |
| 953 | } | |
| 954 | ||
| 955 | continue; | |
| 956 | ||
| 957 | exports: | |
| 958 | while (i < n && ws(u(0))) i++; | |
| 959 | ||
| 960 | // exports = | |
| 961 | if (c(0) == '=') { | |
| 962 | i++; | |
| 963 | ||
| 964 | while (i < n && ws(u(0))) i++; | |
| 965 | ||
| 966 | // exports = \{ | |
| 967 | if (c(0) == '{') { | |
| 968 | i++; | |
| 969 | ||
| 970 | while (c(0) != '}') { | |
| 971 | while (i < n && ws(u(0))) i++; | |
| 972 | ||
| 973 | if (ids(c(0))) { | |
| 974 | ss = i++; | |
| 975 | ||
| 976 | while (id(c(0))) i++; | |
| 977 | ||
| 978 | se = i; | |
| 979 | ||
| 980 | err = bare_module_lexer__add_export(env, exports, &el, s, es, ss, se); | |
| 981 | if (err < 0) goto err; | |
| 982 | } | |
| 983 | ||
| 984 | while (i < n && ws(u(0))) i++; | |
| 985 | ||
| 986 | if (c(0) != ',') break; | |
| 987 | ||
| 988 | i++; | |
| 989 | } | |
| 990 | ||
| 991 | // exports = \{[^}]*\} | |
| 992 | if (c(0) == '}') i++; | |
| 993 | } | |
| 994 | ||
| 995 | // exports = require | |
| 996 | else if (bc("require", 7)) { | |
| 997 | type |= bare_module_lexer_reexport; | |
| 998 | is = i; | |
| 999 | ||
| 1000 | i += 7; | |
| 1001 | ||
| 1002 | goto require; | |
| 1003 | } | |
| 1004 | } | |
| 1005 | ||
| 1006 | // exports\. | |
| 1007 | else if (c(0) == '.') { | |
| 1008 | i++; | |
| 1009 | ||
| 1010 | while (i < n && ws(u(0))) i++; | |
| 1011 | ||
| 1012 | ss = i; | |
| 1013 | ||
| 1014 | while (i < n && !ws(u(0)) && u(0) != '=') i++; | |
| 1015 | ||
| 1016 | se = i; | |
| 1017 | ||
| 1018 | while (i < n && ws(u(0))) i++; | |
| 1019 | ||
| 1020 | // exports\.[^\s=] = | |
| 1021 | if (c(0) == '=') { | |
| 1022 | i++; | |
| 1023 | ||
| 1024 | err = bare_module_lexer__add_export(env, exports, &el, s, es, ss, se); | |
| 1025 | if (err < 0) goto err; | |
| 1026 | } | |
| 1027 | } | |
| 1028 | ||
| 1029 | // exports\[ | |
| 1030 | else if (c(0) == '[') { | |
| 1031 | i++; | |
| 1032 | ||
| 1033 | while (i < n && ws(u(0))) i++; | |
| 1034 | ||
| 1035 | // exports\[['"] | |
| 1036 | if (c(0) == '\'' || c(0) == '"') { | |
| 1037 | utf8_t e = u(0); | |
| 1038 | ||
| 1039 | ss = ++i; | |
| 1040 | ||
| 1041 | while (i < n && u(0) != e) i++; | |
| 1042 | ||
| 1043 | // exports\[['"].*['"] | |
| 1044 | if (c(0) == e) { | |
| 1045 | se = i; | |
| 1046 | ||
| 1047 | i++; | |
| 1048 | ||
| 1049 | while (i < n && ws(u(0))) i++; | |
| 1050 | ||
| 1051 | while (i < n && u(0) != ']') i++; | |
| 1052 | ||
| 1053 | // exports\[['"].*['"][^\]]*\] | |
| 1054 | if (c(0) == ']') { | |
| 1055 | i++; | |
| 1056 | ||
| 1057 | while (i < n && ws(u(0))) i++; | |
| 1058 | ||
| 1059 | // exports\[['"].*['"][^\]]*\] = | |
| 1060 | if (c(0) == '=') { | |
| 1061 | i++; | |
| 1062 | ||
| 1063 | err = bare_module_lexer__add_export(env, exports, &el, s, es, ss, se); | |
| 1064 | if (err < 0) goto err; | |
| 1065 | } | |
| 1066 | } | |
| 1067 | } | |
| 1068 | } | |
| 1069 | } | |
| 1070 | ||
| 1071 | continue; | |
| 1072 | ||
| 1073 | from: | |
| 1074 | while (i < n && ws(u(0))) i++; | |
| 1075 | ||
| 1076 | // from ['"] | |
| 1077 | if (c(0) == '\'' || c(0) == '"') { | |
| 1078 | utf8_t e = u(0); | |
| 1079 | ||
| 1080 | ss = ++i; | |
| 1081 | ||
| 1082 | while (i < n && u(0) != e) i++; | |
| 1083 | ||
| 1084 | // from ['"].*['"] | |
| 1085 | if (c(0) == e) { | |
| 1086 | se = i; | |
| 1087 | ||
| 1088 | i++; | |
| 1089 | ||
| 1090 | while (i < n && ws(u(0))) i++; | |
| 1091 | ||
| 1092 | if (bc("with", 4)) { | |
| 1093 | i += 4; | |
| 1094 | ||
| 1095 | while (i < n && ws(u(0))) i++; | |
| 1096 | ||
| 1097 | err = bare_module_lexer__lex_import_attributes(env, &attributes, s, n, i, &i); | |
| 1098 | if (err < 0) goto err; | |
| 1099 | } | |
| 1100 | ||
| 1101 | err = bare_module_lexer__add_import(env, imports, &il, s, is, ss, se, type, names, attributes); | |
| 1102 | if (err < 0) goto err; | |
| 1103 | } | |
| 1104 | } | |
| 1105 | ||
| 1106 | continue; | |
| 1107 | } | |
| 1108 | ||
| 1109 | #undef u | |
| 1110 | #undef c | |
| 1111 | #undef bu | |
| 1112 | #undef bc | |
| 1113 | #undef lt | |
| 1114 | #undef ws | |
| 1115 | #undef ids | |
| 1116 | #undef id | |
| 1117 | ||
| 1118 | return 0; | |
| 1119 | ||
| 1120 | err: | |
| 1121 | return -1; | |
| 1122 | } | |
| 1123 | ||
| 1124 | #endif // BARE_MODULE_LEXER_H |
| … | … | @@ -0,0 +1,64 @@ |
| 1 | { | |
| 2 | "name": "bare-module-lexer", | |
| 3 | "version": "1.4.7", | |
| 4 | "description": "Heuristic lexer for detecting imports and exports in JavaScript modules", | |
| 5 | "exports": { | |
| 6 | ".": { | |
| 7 | "types": "./index.d.ts", | |
| 8 | "default": "./index.js" | |
| 9 | }, | |
| 10 | "./package": "./package.json" | |
| 11 | }, | |
| 12 | "imports": { | |
| 13 | "#binding": { | |
| 14 | "bare": "./lib/binding/bare.js", | |
| 15 | "node": "./lib/binding/node.js" | |
| 16 | } | |
| 17 | }, | |
| 18 | "files": [ | |
| 19 | "index.js", | |
| 20 | "index.d.ts", | |
| 21 | "binding.c", | |
| 22 | "CMakeLists.txt", | |
| 23 | "lib", | |
| 24 | "prebuilds" | |
| 25 | ], | |
| 26 | "addon": true, | |
| 27 | "scripts": { | |
| 28 | "test": "npm run lint && npm run test:bare && npm run test:node", | |
| 29 | "test:bare": "bare test.js", | |
| 30 | "test:node": "node test.js", | |
| 31 | "lint": "prettier . --check" | |
| 32 | }, | |
| 33 | "repository": { | |
| 34 | "type": "git", | |
| 35 | "url": "git+https://github.com/holepunchto/bare-module-lexer.git" | |
| 36 | }, | |
| 37 | "author": "Holepunch", | |
| 38 | "license": "Apache-2.0", | |
| 39 | "bugs": { | |
| 40 | "url": "https://github.com/holepunchto/bare-module-lexer/issues" | |
| 41 | }, | |
| 42 | "homepage": "https://github.com/holepunchto/bare-module-lexer#readme", | |
| 43 | "dependencies": { | |
| 44 | "require-addon": "^1.0.2" | |
| 45 | }, | |
| 46 | "devDependencies": { | |
| 47 | "bare-buffer": "^3.0.0", | |
| 48 | "bare-compat-napi": "^1.0.0", | |
| 49 | "brittle": "^3.1.1", | |
| 50 | "cmake-bare": "^1.1.7", | |
| 51 | "cmake-napi": "^1.0.5", | |
| 52 | "cmake-npm": "^1.0.2", | |
| 53 | "prettier": "^3.4.1", | |
| 54 | "prettier-config-standard": "^7.0.0" | |
| 55 | }, | |
| 56 | "peerDependencies": { | |
| 57 | "bare-buffer": "*" | |
| 58 | }, | |
| 59 | "peerDependenciesMeta": { | |
| 60 | "bare-buffer": { | |
| 61 | "optional": true | |
| 62 | } | |
| 63 | } | |
| 64 | } |
| … | … | @@ -0,0 +1,13 @@ |
| 1 | cmake_minimum_required(VERSION 4.0) | |
| 2 | ||
| 3 | find_package(cmake-bare REQUIRED PATHS node_modules/cmake-bare) | |
| 4 | ||
| 5 | project(bare_module C) | |
| 6 | ||
| 7 | add_bare_module(bare_module) | |
| 8 | ||
| 9 | target_sources( | |
| 10 | ${bare_module} | |
| 11 | PRIVATE | |
| 12 | binding.c | |
| 13 | ) |
| … | … | @@ -0,0 +1,201 @@ |
| 1 | Apache License | |
| 2 | Version 2.0, January 2004 | |
| 3 | http://www.apache.org/licenses/ | |
| 4 | ||
| 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | |
| 6 | ||
| 7 | 1. Definitions. | |
| 8 | ||
| 9 | "License" shall mean the terms and conditions for use, reproduction, | |
| 10 | and distribution as defined by Sections 1 through 9 of this document. | |
| 11 | ||
| 12 | "Licensor" shall mean the copyright owner or entity authorized by | |
| 13 | the copyright owner that is granting the License. | |
| 14 | ||
| 15 | "Legal Entity" shall mean the union of the acting entity and all | |
| 16 | other entities that control, are controlled by, or are under common | |
| 17 | control with that entity. For the purposes of this definition, | |
| 18 | "control" means (i) the power, direct or indirect, to cause the | |
| 19 | direction or management of such entity, whether by contract or | |
| 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the | |
| 21 | outstanding shares, or (iii) beneficial ownership of such entity. | |
| 22 | ||
| 23 | "You" (or "Your") shall mean an individual or Legal Entity | |
| 24 | exercising permissions granted by this License. | |
| 25 | ||
| 26 | "Source" form shall mean the preferred form for making modifications, | |
| 27 | including but not limited to software source code, documentation | |
| 28 | source, and configuration files. | |
| 29 | ||
| 30 | "Object" form shall mean any form resulting from mechanical | |
| 31 | transformation or translation of a Source form, including but | |
| 32 | not limited to compiled object code, generated documentation, | |
| 33 | and conversions to other media types. | |
| 34 | ||
| 35 | "Work" shall mean the work of authorship, whether in Source or | |
| 36 | Object form, made available under the License, as indicated by a | |
| 37 | copyright notice that is included in or attached to the work | |
| 38 | (an example is provided in the Appendix below). | |
| 39 | ||
| 40 | "Derivative Works" shall mean any work, whether in Source or Object | |
| 41 | form, that is based on (or derived from) the Work and for which the | |
| 42 | editorial revisions, annotations, elaborations, or other modifications | |
| 43 | represent, as a whole, an original work of authorship. For the purposes | |
| 44 | of this License, Derivative Works shall not include works that remain | |
| 45 | separable from, or merely link (or bind by name) to the interfaces of, | |
| 46 | the Work and Derivative Works thereof. | |
| 47 | ||
| 48 | "Contribution" shall mean any work of authorship, including | |
| 49 | the original version of the Work and any modifications or additions | |
| 50 | to that Work or Derivative Works thereof, that is intentionally | |
| 51 | submitted to Licensor for inclusion in the Work by the copyright owner | |
| 52 | or by an individual or Legal Entity authorized to submit on behalf of | |
| 53 | the copyright owner. For the purposes of this definition, "submitted" | |
| 54 | means any form of electronic, verbal, or written communication sent | |
| 55 | to the Licensor or its representatives, including but not limited to | |
| 56 | communication on electronic mailing lists, source code control systems, | |
| 57 | and issue tracking systems that are managed by, or on behalf of, the | |
| 58 | Licensor for the purpose of discussing and improving the Work, but | |
| 59 | excluding communication that is conspicuously marked or otherwise | |
| 60 | designated in writing by the copyright owner as "Not a Contribution." | |
| 61 | ||
| 62 | "Contributor" shall mean Licensor and any individual or Legal Entity | |
| 63 | on behalf of whom a Contribution has been received by Licensor and | |
| 64 | subsequently incorporated within the Work. | |
| 65 | ||
| 66 | 2. Grant of Copyright License. Subject to the terms and conditions of | |
| 67 | this License, each Contributor hereby grants to You a perpetual, | |
| 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 69 | copyright license to reproduce, prepare Derivative Works of, | |
| 70 | publicly display, publicly perform, sublicense, and distribute the | |
| 71 | Work and such Derivative Works in Source or Object form. | |
| 72 | ||
| 73 | 3. Grant of Patent License. Subject to the terms and conditions of | |
| 74 | this License, each Contributor hereby grants to You a perpetual, | |
| 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 76 | (except as stated in this section) patent license to make, have made, | |
| 77 | use, offer to sell, sell, import, and otherwise transfer the Work, | |
| 78 | where such license applies only to those patent claims licensable | |
| 79 | by such Contributor that are necessarily infringed by their | |
| 80 | Contribution(s) alone or by combination of their Contribution(s) | |
| 81 | with the Work to which such Contribution(s) was submitted. If You | |
| 82 | institute patent litigation against any entity (including a | |
| 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work | |
| 84 | or a Contribution incorporated within the Work constitutes direct | |
| 85 | or contributory patent infringement, then any patent licenses | |
| 86 | granted to You under this License for that Work shall terminate | |
| 87 | as of the date such litigation is filed. | |
| 88 | ||
| 89 | 4. Redistribution. You may reproduce and distribute copies of the | |
| 90 | Work or Derivative Works thereof in any medium, with or without | |
| 91 | modifications, and in Source or Object form, provided that You | |
| 92 | meet the following conditions: | |
| 93 | ||
| 94 | (a) You must give any other recipients of the Work or | |
| 95 | Derivative Works a copy of this License; and | |
| 96 | ||
| 97 | (b) You must cause any modified files to carry prominent notices | |
| 98 | stating that You changed the files; and | |
| 99 | ||
| 100 | (c) You must retain, in the Source form of any Derivative Works | |
| 101 | that You distribute, all copyright, patent, trademark, and | |
| 102 | attribution notices from the Source form of the Work, | |
| 103 | excluding those notices that do not pertain to any part of | |
| 104 | the Derivative Works; and | |
| 105 | ||
| 106 | (d) If the Work includes a "NOTICE" text file as part of its | |
| 107 | distribution, then any Derivative Works that You distribute must | |
| 108 | include a readable copy of the attribution notices contained | |
| 109 | within such NOTICE file, excluding those notices that do not | |
| 110 | pertain to any part of the Derivative Works, in at least one | |
| 111 | of the following places: within a NOTICE text file distributed | |
| 112 | as part of the Derivative Works; within the Source form or | |
| 113 | documentation, if provided along with the Derivative Works; or, | |
| 114 | within a display generated by the Derivative Works, if and | |
| 115 | wherever such third-party notices normally appear. The contents | |
| 116 | of the NOTICE file are for informational purposes only and | |
| 117 | do not modify the License. You may add Your own attribution | |
| 118 | notices within Derivative Works that You distribute, alongside | |
| 119 | or as an addendum to the NOTICE text from the Work, provided | |
| 120 | that such additional attribution notices cannot be construed | |
| 121 | as modifying the License. | |
| 122 | ||
| 123 | You may add Your own copyright statement to Your modifications and | |
| 124 | may provide additional or different license terms and conditions | |
| 125 | for use, reproduction, or distribution of Your modifications, or | |
| 126 | for any such Derivative Works as a whole, provided Your use, | |
| 127 | reproduction, and distribution of the Work otherwise complies with | |
| 128 | the conditions stated in this License. | |
| 129 | ||
| 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, | |
| 131 | any Contribution intentionally submitted for inclusion in the Work | |
| 132 | by You to the Licensor shall be under the terms and conditions of | |
| 133 | this License, without any additional terms or conditions. | |
| 134 | Notwithstanding the above, nothing herein shall supersede or modify | |
| 135 | the terms of any separate license agreement you may have executed | |
| 136 | with Licensor regarding such Contributions. | |
| 137 | ||
| 138 | 6. Trademarks. This License does not grant permission to use the trade | |
| 139 | names, trademarks, service marks, or product names of the Licensor, | |
| 140 | except as required for reasonable and customary use in describing the | |
| 141 | origin of the Work and reproducing the content of the NOTICE file. | |
| 142 | ||
| 143 | 7. Disclaimer of Warranty. Unless required by applicable law or | |
| 144 | agreed to in writing, Licensor provides the Work (and each | |
| 145 | Contributor provides its Contributions) on an "AS IS" BASIS, | |
| 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |
| 147 | implied, including, without limitation, any warranties or conditions | |
| 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | |
| 149 | PARTICULAR PURPOSE. You are solely responsible for determining the | |
| 150 | appropriateness of using or redistributing the Work and assume any | |
| 151 | risks associated with Your exercise of permissions under this License. | |
| 152 | ||
| 153 | 8. Limitation of Liability. In no event and under no legal theory, | |
| 154 | whether in tort (including negligence), contract, or otherwise, | |
| 155 | unless required by applicable law (such as deliberate and grossly | |
| 156 | negligent acts) or agreed to in writing, shall any Contributor be | |
| 157 | liable to You for damages, including any direct, indirect, special, | |
| 158 | incidental, or consequential damages of any character arising as a | |
| 159 | result of this License or out of the use or inability to use the | |
| 160 | Work (including but not limited to damages for loss of goodwill, | |
| 161 | work stoppage, computer failure or malfunction, or any and all | |
| 162 | other commercial damages or losses), even if such Contributor | |
| 163 | has been advised of the possibility of such damages. | |
| 164 | ||
| 165 | 9. Accepting Warranty or Additional Liability. While redistributing | |
| 166 | the Work or Derivative Works thereof, You may choose to offer, | |
| 167 | and charge a fee for, acceptance of support, warranty, indemnity, | |
| 168 | or other liability obligations and/or rights consistent with this | |
| 169 | License. However, in accepting such obligations, You may act only | |
| 170 | on Your own behalf and on Your sole responsibility, not on behalf | |
| 171 | of any other Contributor, and only if You agree to indemnify, | |
| 172 | defend, and hold each Contributor harmless for any liability | |
| 173 | incurred by, or claims asserted against, such Contributor by reason | |
| 174 | of your accepting any such warranty or additional liability. | |
| 175 | ||
| 176 | END OF TERMS AND CONDITIONS | |
| 177 | ||
| 178 | APPENDIX: How to apply the Apache License to your work. | |
| 179 | ||
| 180 | To apply the Apache License to your work, attach the following | |
| 181 | boilerplate notice, with the fields enclosed by brackets "[]" | |
| 182 | replaced with your own identifying information. (Don't include | |
| 183 | the brackets!) The text should be enclosed in the appropriate | |
| 184 | comment syntax for the file format. We also recommend that a | |
| 185 | file or class name and description of purpose be included on the | |
| 186 | same "printed page" as the copyright notice for easier | |
| 187 | identification within third-party archives. | |
| 188 | ||
| 189 | Copyright [yyyy] [name of copyright owner] | |
| 190 | ||
| 191 | Licensed under the Apache License, Version 2.0 (the "License"); | |
| 192 | you may not use this file except in compliance with the License. | |
| 193 | You may obtain a copy of the License at | |
| 194 | ||
| 195 | http://www.apache.org/licenses/LICENSE-2.0 | |
| 196 | ||
| 197 | Unless required by applicable law or agreed to in writing, software | |
| 198 | distributed under the License is distributed on an "AS IS" BASIS, | |
| 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 200 | See the License for the specific language governing permissions and | |
| 201 | limitations under the License. |
| … | … | @@ -0,0 +1,647 @@ |
| 1 | # bare-module | |
| 2 | ||
| 3 | Module support for JavaScript. | |
| 4 | ||
| 5 | ``` | |
| 6 | npm i bare-module | |
| 7 | ``` | |
| 8 | ||
| 9 | ## Usage | |
| 10 | ||
| 11 | ```js | |
| 12 | const Module = require('bare-module') | |
| 13 | ``` | |
| 14 | ||
| 15 | ## Packages | |
| 16 | ||
| 17 | A package is a directory with a `package.json` file. | |
| 18 | ||
| 19 | ### Fields | |
| 20 | ||
| 21 | #### `"name"` | |
| 22 | ||
| 23 | ```json | |
| 24 | { | |
| 25 | "name": "my-package" | |
| 26 | } | |
| 27 | ``` | |
| 28 | ||
| 29 | The name of the package. This is used for [addon resolution](https://github.com/holepunchto/bare-addon-resolve#algorithm), [self-referencing](#self-referencing), and importing packages by name. | |
| 30 | ||
| 31 | #### `"version"` | |
| 32 | ||
| 33 | ```json | |
| 34 | { | |
| 35 | "version": "1.2.3" | |
| 36 | } | |
| 37 | ``` | |
| 38 | ||
| 39 | The current version of the package. This is used for [addon resolution](https://github.com/holepunchto/bare-addon-resolve#algorithm). | |
| 40 | ||
| 41 | #### `"type"` | |
| 42 | ||
| 43 | ```json | |
| 44 | { | |
| 45 | "type": "module" | |
| 46 | } | |
| 47 | ``` | |
| 48 | ||
| 49 | The module format used for `.js` files. If not defined, `.js` files are interpreted as CommonJS. If set to `"module"`, `.js` files are instead interpreted as ES modules. | |
| 50 | ||
| 51 | #### `"exports"` | |
| 52 | ||
| 53 | ```json | |
| 54 | { | |
| 55 | "exports": { | |
| 56 | ".": "./index.js" | |
| 57 | } | |
| 58 | } | |
| 59 | ``` | |
| 60 | ||
| 61 | The entry points of the package. If defined, only the modules explicitly exported by the package may be imported when importing the package by name. | |
| 62 | ||
| 63 | ##### Subpath exports | |
| 64 | ||
| 65 | A package may define more than one entry point by declaring several subpaths with the main export being `"."`: | |
| 66 | ||
| 67 | ```json | |
| 68 | { | |
| 69 | "exports": { | |
| 70 | ".": "./index.js", | |
| 71 | "./submodule": "./lib/submodule.js" | |
| 72 | } | |
| 73 | } | |
| 74 | ``` | |
| 75 | ||
| 76 | When importing the package by name, `require('my-package')` will resolve to `<modules>/my-package/index.js` whereas `require('my-package/submodule')` will resolve to `<modules>/my-package/lib/submodule.js`. | |
| 77 | ||
| 78 | ##### Conditional exports | |
| 79 | ||
| 80 | Conditional exports allow packages to provide different exports for different conditions, such as the loading method the importing module uses (e.g. `require()` vs `import`): | |
| 81 | ||
| 82 | ```json | |
| 83 | { | |
| 84 | "exports": { | |
| 85 | ".": { | |
| 86 | "import": "./index.mjs", | |
| 87 | "require": "./index.cjs" | |
| 88 | } | |
| 89 | } | |
| 90 | } | |
| 91 | ``` | |
| 92 | ||
| 93 | When importing the package by name, `require('my-package')` will resolve to `<modules>/my-package/index.cjs` whereas `import 'my-package'` will resolve to `<modules>/my-package/index.mjs`. | |
| 94 | ||
| 95 | Similarly, conditional exports can be used to provide different entry points for different runtimes: | |
| 96 | ||
| 97 | ```json | |
| 98 | { | |
| 99 | "exports": { | |
| 100 | ".": { | |
| 101 | "bare": "./bare.js", | |
| 102 | "node": "./node.js" | |
| 103 | } | |
| 104 | } | |
| 105 | } | |
| 106 | ``` | |
| 107 | ||
| 108 | To provide a fallback for when no other conditions match, the `"default"` condition can be declared: | |
| 109 | ||
| 110 | ```json | |
| 111 | { | |
| 112 | "exports": { | |
| 113 | ".": { | |
| 114 | "bare": "./bare.js", | |
| 115 | "node": "./node.js", | |
| 116 | "default": "./fallback.js" | |
| 117 | } | |
| 118 | } | |
| 119 | } | |
| 120 | ``` | |
| 121 | ||
| 122 | The following conditions are supported, listed in order from most specific to least specific as conditions should be defined: | |
| 123 | ||
| 124 | | Condition | Description | | |
| 125 | | :------------- | :---------------------------------------------------------------------------------------------------------------------------------- | | |
| 126 | | `"import"` | Matches when the package is loaded via `import` or `import()`. | | |
| 127 | | `"require"` | Matches when the package is loaded via `require()`. | | |
| 128 | | `"asset"` | Matches when the package is loaded via `require.asset()`. | | |
| 129 | | `"addon"` | Matches when the package is loaded via `require.addon()`. | | |
| 130 | | `"bare"` | Matches for any [Bare](https://github.com/holepunchto/bare) environment. | | |
| 131 | | `"node"` | Matches for any Node.js environment. | | |
| 132 | | `"<platform>"` | Matches when equal to `Bare.platform`. See [`Bare.platform`](https://github.com/holepunchto/bare#bareplatform) for possible values. | | |
| 133 | | `"<arch>"` | Matches when equal to `Bare.arch`. See [`Bare.arch`](https://github.com/holepunchto/bare#barearch) for possible values. | | |
| 134 | | `"simulator"` | Matches when Bare was compiled for a simulator. | | |
| 135 | | `"default"` | The fallback that always matches. This condition should always be last. | | |
| 136 | ||
| 137 | Export conditions are evaluated in the order they are defined in the `"exports"` field. This means that less specific conditionals defined first will override more specific conditions define later. For example, the following will always call `./fallback.js` because `"default"` always matches and is defined first. | |
| 138 | ||
| 139 | ```json | |
| 140 | { | |
| 141 | "exports": { | |
| 142 | ".": { | |
| 143 | "default": "./fallback.js", | |
| 144 | "bare": "./bare.js" | |
| 145 | } | |
| 146 | } | |
| 147 | } | |
| 148 | ``` | |
| 149 | ||
| 150 | This is why the general rule is that conditions should be from most specific to least specific when defined. | |
| 151 | ||
| 152 | ##### Self-referencing | |
| 153 | ||
| 154 | Within a package, exports defined in the `"exports"` field can be referenced by importing the package by name. For example, given the following `package.json`... | |
| 155 | ||
| 156 | ```json | |
| 157 | { | |
| 158 | "name": "my-package", | |
| 159 | "exports": { | |
| 160 | ".": "./index.js", | |
| 161 | "./submodule": "./lib/submodule.js" | |
| 162 | } | |
| 163 | } | |
| 164 | ``` | |
| 165 | ||
| 166 | ...any module within `my-package` may reference these entry points using either `require('my-package')` or `require('my-package/submodule')`. | |
| 167 | ||
| 168 | ##### Exports sugar | |
| 169 | ||
| 170 | If a package defines only a single export, `"."`, it may leave out the subpath entirely: | |
| 171 | ||
| 172 | ```json | |
| 173 | { | |
| 174 | "exports": "./index.js" | |
| 175 | } | |
| 176 | ``` | |
| 177 | ||
| 178 | #### `"imports"` | |
| 179 | ||
| 180 | A private mapping for import specifiers within the package itself. Similar to `"exports"`, the `"imports"` field can be used to conditional import other packages within the package. But unlike `"exports"`, `"imports"` permits mapping to external packages. | |
| 181 | ||
| 182 | The rules are otherwise analogous to the [`"exports"`](#conditional-exports) field. | |
| 183 | ||
| 184 | ##### Subpath imports | |
| 185 | ||
| 186 | Just like exports, subpaths can be used when importing a module internally. | |
| 187 | ||
| 188 | ```json | |
| 189 | { | |
| 190 | "imports": { | |
| 191 | ".": "./index.js", | |
| 192 | "./submodule": "./lib/submodule.js" | |
| 193 | } | |
| 194 | } | |
| 195 | ``` | |
| 196 | ||
| 197 | ##### Conditional imports | |
| 198 | ||
| 199 | Adding conditional imports allows importing different packages based on the configured conditions. As an example: | |
| 200 | ||
| 201 | ```json | |
| 202 | { | |
| 203 | "imports": { | |
| 204 | "bar": { | |
| 205 | "require": "./baz.cjs", | |
| 206 | "import": "./baz.mjs" | |
| 207 | } | |
| 208 | } | |
| 209 | } | |
| 210 | ``` | |
| 211 | ||
| 212 | When importing the package `bar` as `require('bar')` will resolve to `./baz.cjs`, but when importing with `import('bar')` will resolve to `./baz.mjs`. | |
| 213 | ||
| 214 | To provide a fallback for when no other conditions are met, the `"default"` condition can be configured like so: | |
| 215 | ||
| 216 | ```json | |
| 217 | { | |
| 218 | "imports": { | |
| 219 | "bar": { | |
| 220 | "require": "./baz.cjs", | |
| 221 | "asset": "./baz.txt", | |
| 222 | "default": "./baz.mjs" | |
| 223 | } | |
| 224 | } | |
| 225 | } | |
| 226 | ``` | |
| 227 | ||
| 228 | The following conditions are supported, listed in order from most specific to least specific as conditions should be defined: | |
| 229 | ||
| 230 | | Condition | Description | | |
| 231 | | :------------- | :---------------------------------------------------------------------------------------------------------------------------------- | | |
| 232 | | `"import"` | Matches when the package is loaded via `import` or `import()`. | | |
| 233 | | `"require"` | Matches when the package is loaded via `require()`. | | |
| 234 | | `"asset"` | Matches when the package is loaded via `require.asset()`. | | |
| 235 | | `"addon"` | Matches when the package is loaded via `require.addon()`. | | |
| 236 | | `"bare"` | Matches for any [Bare](https://github.com/holepunchto/bare) environment. | | |
| 237 | | `"node"` | Matches for any Node.js environment. | | |
| 238 | | `"<platform>"` | Matches when equal to `Bare.platform`. See [`Bare.platform`](https://github.com/holepunchto/bare#bareplatform) for possible values. | | |
| 239 | | `"<arch>"` | Matches when equal to `Bare.arch`. See [`Bare.arch`](https://github.com/holepunchto/bare#barearch) for possible values. | | |
| 240 | | `"simulator"` | Matches when Bare was compiled for a simulator. | | |
| 241 | | `"default"` | The fallback that always matches. This condition should always be last. | | |
| 242 | ||
| 243 | The general rule is that conditions should be from most specific to least specific when defined. | |
| 244 | ||
| 245 | ##### `#` Prefix | |
| 246 | ||
| 247 | All import maps are private to the package and allow mapping to external packages. Entries in `"imports"` may start with `#` to disambiguate from external packages, but it is not required unlike in Node.js. | |
| 248 | ||
| 249 | #### `"engines"` | |
| 250 | ||
| 251 | ```json | |
| 252 | { | |
| 253 | "engines": { | |
| 254 | "bare": ">=1.0.5" | |
| 255 | } | |
| 256 | } | |
| 257 | ``` | |
| 258 | ||
| 259 | The `"engines"` field defines the engine requirements of the package. During module resolution, the versions declared by `Bare.versions` will be tested against the requirements declared by the package and resolution fail if they're not satisfied. | |
| 260 | ||
| 261 | ## API | |
| 262 | ||
| 263 | #### `Module.constants.states` | |
| 264 | ||
| 265 | The flags for the current state of a module. | |
| 266 | ||
| 267 | | Constant | Description | | |
| 268 | | :------------ | :------------------------------------------- | | |
| 269 | | `EVALUATED` | The module has been evaluated. | | |
| 270 | | `SYNTHESIZED` | The module named exports have been detected. | | |
| 271 | ||
| 272 | #### `Module.constants.types` | |
| 273 | ||
| 274 | | Constant | Description | | |
| 275 | | :------- | :--------------------------------------------------------------------------- | | |
| 276 | | `SCRIPT` | The module is a CommonJS module. | | |
| 277 | | `MODULE` | The module is a ECMAScript module. | | |
| 278 | | `JSON` | The module is a JSON file. | | |
| 279 | | `BUNDLE` | The module is a [`bare-bundle`](https://github.com/holepunchto/bare-bundle). | | |
| 280 | | `ADDON` | The module is a native addon. | | |
| 281 | | `BINARY` | The module is a binary file. | | |
| 282 | | `TEXT` | The module is a text file. | | |
| 283 | ||
| 284 | #### `Module.protocol` | |
| 285 | ||
| 286 | The default `ModuleProtocol` class for resolving, reading and loading modules. See [Protocols](#protocols) for usage. | |
| 287 | ||
| 288 | #### `Module.cache` | |
| 289 | ||
| 290 | The global cache of loaded modules. | |
| 291 | ||
| 292 | > [!WARNING] | |
| 293 | > The cache may contain `Module` instances from several independent versions of `bare-module` as the cache is inherited through the module graph. | |
| 294 | ||
| 295 | #### `const url = Module.resolve(specifier, parentURL[, options])` | |
| 296 | ||
| 297 | Resolve the module `specifier` relative to the `parentURL`. `specifier` is a string and `parentURL` is a WHATWG `URL`. | |
| 298 | ||
| 299 | Options include: | |
| 300 | ||
| 301 | ```js | |
| 302 | options = { | |
| 303 | // Whether the module is called via `import` or `import()`. | |
| 304 | isImport: false, | |
| 305 | // The referring module. | |
| 306 | referrer: null, | |
| 307 | // The type of the module. See Module.constants.types for possible values. The | |
| 308 | // default is the equivalent constant of the `attributes`'s `type` property. | |
| 309 | type, | |
| 310 | // A list of file extensions to look for. The default is based on the `type` | |
| 311 | // option. | |
| 312 | extensions: [], | |
| 313 | // The ModuleProtocol to resolve the specifier. Defaults to referrer's | |
| 314 | // protocol if defined, otherwise defaults to Module.protocol | |
| 315 | protocol, | |
| 316 | // A default "imports" map to apply to all specifiers. Follows the same | |
| 317 | // syntax and rules as the "imports" property defined in `package.json`. | |
| 318 | imports, | |
| 319 | // A map of preresolved imports with keys being serialized parent URLs and | |
| 320 | // values being "imports" maps. | |
| 321 | resolutions, | |
| 322 | // A map of builtin module specifiers to loaded modules. If matched by the | |
| 323 | // default resolver, the protocol of the resolved URL will be `builtin:`. | |
| 324 | builtins, | |
| 325 | // The supported import conditions. "default" is always recognized. | |
| 326 | conditions: [], | |
| 327 | // The import attributes, e.g. the `{ type: "json" }` in: | |
| 328 | // `import foo from 'foo' with { type: "json" }` | |
| 329 | // or in: | |
| 330 | // `require('foo', { with: { type: "json" } })` | |
| 331 | attributes | |
| 332 | } | |
| 333 | ``` | |
| 334 | ||
| 335 | #### `const url = Module.asset(specifier, parentURL[, options])` | |
| 336 | ||
| 337 | Get the asset URL by resolving `specifier` relative to `parentURL`. `specifier` is a string and `parentURL` is a WHATWG `URL`. | |
| 338 | ||
| 339 | Options include: | |
| 340 | ||
| 341 | ```js | |
| 342 | options = { | |
| 343 | // The referring module. | |
| 344 | referrer: null, | |
| 345 | // The ModuleProtocol to use resolve the specifier. Defaults to referrer's | |
| 346 | // protocol if defined, otherwise defaults to Module.protocol | |
| 347 | protocol, | |
| 348 | // A default "imports" map to apply to all specifiers. Follows the same | |
| 349 | // syntax and rules as the "imports" property defined in `package.json`. | |
| 350 | imports, | |
| 351 | // A map of preresolved imports with keys being serialized parent URLs and | |
| 352 | // values being "imports" maps. | |
| 353 | resolutions, | |
| 354 | // The supported import conditions. "default" is always recognized. | |
| 355 | conditions | |
| 356 | } | |
| 357 | ``` | |
| 358 | ||
| 359 | #### `const module = Module.load(url[, source][, options])` | |
| 360 | ||
| 361 | Load a module with the provided `url`. `url` is a WHATWG `URL`. If provided, the `source` will be passed to the matching `extension` for the `url`. | |
| 362 | ||
| 363 | Options include: | |
| 364 | ||
| 365 | ```js | |
| 366 | options = { | |
| 367 | // Whether the module is called via `import` or `import()`. | |
| 368 | isImport: false, | |
| 369 | // Whether the module is called via `import()`. | |
| 370 | isDynamicImport: false, | |
| 371 | // The referring module. | |
| 372 | referrer: null, | |
| 373 | // The type of the module. See Module.constants.types for possible values. The | |
| 374 | // default is the equivalent constant of the `attributes`'s `type` property. | |
| 375 | type, | |
| 376 | // The assumed type of a module without a type using an ambiguous extension | |
| 377 | // such as `.js`. See Module.constants.types. Inherited from `referrer` if it | |
| 378 | // is defined. | |
| 379 | defaultType: Module.constants.types.SCRIPT, | |
| 380 | // Cache to use to load the Module. Defaults to `Module.cache`. | |
| 381 | cache, | |
| 382 | // The module representing the entry script where the program was launched. | |
| 383 | main, | |
| 384 | // The ModuleProtocol to use resolve the specifier. Defaults to referrer's | |
| 385 | // `protocol` if defined, otherwise defaults to `Module.protocol`. | |
| 386 | protocol, | |
| 387 | // A default "imports" map to apply to all specifiers. Follows the same | |
| 388 | // syntax and rules as the "imports" property defined in `package.json`. | |
| 389 | imports, | |
| 390 | // A map of preresolved imports with keys being serialized parent URLs and | |
| 391 | // values being "imports" maps. | |
| 392 | resolutions, | |
| 393 | // A map of builtin module specifiers to loaded modules. If the `url`'s | |
| 394 | // protocol is `builtin:`, the module's exports will be set to the matching | |
| 395 | // value in the map for `url.pathname`. | |
| 396 | builtins, | |
| 397 | // The supported import conditions. "default" is always recognized. | |
| 398 | conditions, | |
| 399 | // The import attributes, e.g. the `{ type: "json" }` in: | |
| 400 | // `import foo from 'foo' with { type: "json" }` | |
| 401 | // or in: | |
| 402 | // `require('foo', { with: { type: "json" } })` | |
| 403 | attributes | |
| 404 | } | |
| 405 | ``` | |
| 406 | ||
| 407 | #### `module.url` | |
| 408 | ||
| 409 | The WHATWG `URL` identifier of the module. | |
| 410 | ||
| 411 | #### `module.filename` | |
| 412 | ||
| 413 | The file portion of `module.url`. | |
| 414 | ||
| 415 | #### `module.dirname` | |
| 416 | ||
| 417 | The directory portion of `module.url`. | |
| 418 | ||
| 419 | #### `module.type` | |
| 420 | ||
| 421 | The type of the module. See [`Module.constants.types`](#module.constants.types) for possible values. | |
| 422 | ||
| 423 | #### `module.defaultType` | |
| 424 | ||
| 425 | The assumed type of a module without a `type` using an ambiguous extension, such as `.js`. See [`Module.constants.types`](#module.constants.types) for possible values. | |
| 426 | ||
| 427 | #### `module.cache` | |
| 428 | ||
| 429 | A cache of loaded modules for this module. Defaults to `Module.cache`. | |
| 430 | ||
| 431 | #### `module.main` | |
| 432 | ||
| 433 | The module representing the entry script where the program was launched. | |
| 434 | ||
| 435 | #### `module.exports` | |
| 436 | ||
| 437 | The exports from the module. | |
| 438 | ||
| 439 | #### `module.imports` | |
| 440 | ||
| 441 | The import map when the module was loaded. | |
| 442 | ||
| 443 | #### `module.resolutions` | |
| 444 | ||
| 445 | A map of preresolved imports with keys being serialized parent URLs and values being `"imports"` maps. | |
| 446 | ||
| 447 | #### `module.builtins` | |
| 448 | ||
| 449 | A map of builtin module specifiers mapped to the loaded module. | |
| 450 | ||
| 451 | #### `module.conditions` | |
| 452 | ||
| 453 | An array of conditions used to resolve dependencies while loading the module. See [Conditional Exports](#conditional-exports) for possible values. | |
| 454 | ||
| 455 | #### `module.protocol` | |
| 456 | ||
| 457 | The `ModuleProtocol` class used for resolving, reading, and loading modules. See [Protocols](#protocols). | |
| 458 | ||
| 459 | ### CommonJS modules | |
| 460 | ||
| 461 | #### `require(specifier[, options])` | |
| 462 | ||
| 463 | Used to import JavaScript or JSON modules and local files. Relative paths such as `./`, `./foo`, `./bar/baz`, and `../foo` will be resolved against the directory named by `__dirname`. POSIX style paths are resolved in an OS independent fashion, meaning that the examples above will work on Windows in the same way they would on POSIX systems. | |
| 464 | ||
| 465 | Returns the exported module contents. | |
| 466 | ||
| 467 | Options include: | |
| 468 | ||
| 469 | ```js | |
| 470 | options = { | |
| 471 | // The import attributes which instruct how the file or module should be loaded. | |
| 472 | // Possible values for `type` are `script`, `module`, `json`, `bundle`, | |
| 473 | // `addon`, `binary` and `text`. | |
| 474 | with: { type: 'json' } | |
| 475 | } | |
| 476 | ``` | |
| 477 | ||
| 478 | #### `require.main` | |
| 479 | ||
| 480 | The module representing the entry script where the program was launched. The same value as [`module.main`](#modulemain) for the current module. | |
| 481 | ||
| 482 | #### `require.cache` | |
| 483 | ||
| 484 | A cache of loaded modules for this module. The same value as `module.cache` for the current module. | |
| 485 | ||
| 486 | #### `const path = require.resolve(specifier[, parentURL])` | |
| 487 | ||
| 488 | Use the internal machinery of `require()` to resolve the `specifier` string relative to the URL `parentURL` and return the path string. | |
| 489 | ||
| 490 | #### `require.addon([specifier][, parentURL])` | |
| 491 | ||
| 492 | Also used to import modules but specifically loads only addon modules. `specifier` is resolved relative to `parentURL` using the [addon resolution](https://github.com/holepunchto/bare-addon-resolve#algorithm) algorithm. | |
| 493 | ||
| 494 | Returns the exported module contents. | |
| 495 | ||
| 496 | A common pattern for writing an addon module is to use `require.addon()` as the JavaScript module exports: | |
| 497 | ||
| 498 | ```js | |
| 499 | module.exports = require.addon() | |
| 500 | ``` | |
| 501 | ||
| 502 | See [`bare-addon`](https://github.com/holepunchto/bare-addon) for a template of building native addon modules. | |
| 503 | ||
| 504 | #### `require.addon.host` | |
| 505 | ||
| 506 | Returns the string representation of the platform and architecture used when resolving addons with the pattern `<platform>-<arch>[-<environment>]`. Returns the same value as `Bare.Addon.host`. | |
| 507 | ||
| 508 | #### `const path = require.addon.resolve([specifier][, parentURL])` | |
| 509 | ||
| 510 | Resolve the `specifier` string relative to the URL `parentURL` as an addon and returns the path string. The `specifier` is resolved using the [addon resolution algorithm](https://github.com/holepunchto/bare-addon-resolve#algorithm). | |
| 511 | ||
| 512 | #### `const path = require.asset(specifier[, parentURL])` | |
| 513 | ||
| 514 | Resolve the `specifier` relative to the `parentURL` and return the path of the asset as a string. | |
| 515 | ||
| 516 | Can be used to load assets, for example the following loads `./foo.txt` from the local files: | |
| 517 | ||
| 518 | ```js | |
| 519 | const fs = require('bare-fs') | |
| 520 | const contents = fs.readFileSync(require.asset('./foo.txt')) | |
| 521 | ``` | |
| 522 | ||
| 523 | ### ECMAScript modules | |
| 524 | ||
| 525 | #### `import defaultExport, * as name, { export1, export2 as alias2, ... } from 'specifier' with { type: 'json' }` | |
| 526 | ||
| 527 | The static `import` declaration is used to import read-only live bindings that are exported by another module. The imported bindings are called _live_ bindings because they are updated by the module that exported the binding, but cannot be re-assigned by the importing module. In brief, you can import what is exported from another module. | |
| 528 | ||
| 529 | For more information on `import` syntax, see [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import). | |
| 530 | ||
| 531 | #### `import.meta.url` | |
| 532 | ||
| 533 | The string representation of the URL for the current module. | |
| 534 | ||
| 535 | #### `import.meta.main` | |
| 536 | ||
| 537 | A boolean representing whether the current module is the entry script where the program was launched. | |
| 538 | ||
| 539 | #### `import.meta.cache` | |
| 540 | ||
| 541 | A cache of loaded modules for this module. The same value as `module.cache` for the current module. | |
| 542 | ||
| 543 | #### `const href = import.meta.resolve(specifier[, parentURL])` | |
| 544 | ||
| 545 | A module-relative resolution function which returns the URL string for the module. The `specifier` is a string which is resolved relative to the `parentURL` which is a WHATWG URL. | |
| 546 | ||
| 547 | #### `import.meta.addon([specifier][, parentURL])` | |
| 548 | ||
| 549 | Also used to import modules but specifically loads only addon modules. `specifier` is resolved relative to `parentURL` using the [addon resolution](https://github.com/holepunchto/bare-addon-resolve#algorithm) algorithm. | |
| 550 | ||
| 551 | Returns the exported module contents. | |
| 552 | ||
| 553 | #### `import.meta.addon.host` | |
| 554 | ||
| 555 | Returns the string representation of the platform and architecture used when resolving addons with the pattern `<platform>-<arch>[-<environment>]`. Returns the same value as `Bare.Addon.host`. | |
| 556 | ||
| 557 | #### `const href = import.meta.addon.resolve([specifier][, parentURL])` | |
| 558 | ||
| 559 | Resolve the `specifier` string relative to the URL `parentURL` as an addon and returns the URL string. The `specifier` is resolved using the [addon resolution algorithm](https://github.com/holepunchto/bare-addon-resolve#algorithm). | |
| 560 | ||
| 561 | #### `const href = import.meta.asset(specifier[, parentURL])` | |
| 562 | ||
| 563 | Resolve the `specifier` relative to the `parentURL` and return the URL of the asset as a string. | |
| 564 | ||
| 565 | ### Custom `require()` | |
| 566 | ||
| 567 | Creating a custom require allows one to create a preconfigured `require()`. This can be useful in scenarios such as a Read-Evaluate-Print-Loop (REPL) where the parent URL is set to a directory so requiring relative paths to work correctly. | |
| 568 | ||
| 569 | #### `const require = Module.createRequire(parentURL[, options])` | |
| 570 | ||
| 571 | Options include: | |
| 572 | ||
| 573 | ```js | |
| 574 | options = { | |
| 575 | // The module to become the `referrer` for the returned `require()`. Defaults | |
| 576 | // to creating a new module instance from the `parentURL` with the same | |
| 577 | // options. | |
| 578 | module: null, | |
| 579 | // The referring module. | |
| 580 | referrer: null, | |
| 581 | // The type of the module. See Module.constants.types for possible values. | |
| 582 | type: Module.constants.types.SCRIPT, | |
| 583 | // The assumed type of a module without a type using an ambiguous extension | |
| 584 | // such as `.js`. See Module.constants.types. Inherited from `referrer` if it | |
| 585 | // is defined, otherwise defaults to SCRIPT. | |
| 586 | defaultType: Module.constants.types.SCRIPT, | |
| 587 | // A cache of loaded modules. Inherited from `referrer` if it is defined, | |
| 588 | // otherwise defaults to `Module.cache` | |
| 589 | cache, | |
| 590 | // The module representing the entry script where the program was launched. | |
| 591 | main, | |
| 592 | // The ModuleProtocol to use resolve the specifier and/or the module. Defaults to | |
| 593 | // referrer's protocol if defined, otherwise defaults to Module.protocol | |
| 594 | protocol, | |
| 595 | // A default "imports" map to apply to all specifiers. Follows the same | |
| 596 | // syntax and rules as the "imports" property defined in `package.json`. | |
| 597 | imports, | |
| 598 | // A map of preresolved imports with keys being serialized parent URLs and | |
| 599 | // values being "imports" maps. | |
| 600 | resolutions, | |
| 601 | // A map of builtin module specifiers to loaded modules. | |
| 602 | builtins, | |
| 603 | // The supported import conditions. "default" is always recognized. | |
| 604 | conditions | |
| 605 | } | |
| 606 | ``` | |
| 607 | ||
| 608 | ### Protocols | |
| 609 | ||
| 610 | Protocols define how to resolve, access and load modules. Custom protocols can be defined to extend or replace how module are resolved and loaded to support things like loading modules via a [`Hyperdrive`](https://github.com/holepunchto/hyperdrive). | |
| 611 | ||
| 612 | #### `const protocol = new Module.Protocol(methods, context = null)` | |
| 613 | ||
| 614 | Methods include: | |
| 615 | ||
| 616 | ```js | |
| 617 | methods = { | |
| 618 | // function (specifier, parentURL): string | |
| 619 | // A function to preprocess the `specifier` and `parentURL` before the resolve | |
| 620 | // algorithm is called. | |
| 621 | preresolve, | |
| 622 | // function (url): string | |
| 623 | // A function to process the resolved URL. Can be used to convert file paths, | |
| 624 | // etc. | |
| 625 | postresolve, | |
| 626 | // function* (specifier, parentURL, imports): [URL] | |
| 627 | // A generator to resolve the `specifier` to a URL. | |
| 628 | resolve, | |
| 629 | // function (url): boolean | |
| 630 | // A function that returns whether the URL exists as a boolean. | |
| 631 | exists, | |
| 632 | // function (url): string | Buffer | |
| 633 | // A function that returns the source code of a URL represented as a string or | |
| 634 | // buffer. | |
| 635 | read, | |
| 636 | // function (url): URL | |
| 637 | // A function used to post process URLs for addons before `postresolve()`. | |
| 638 | addon, | |
| 639 | // function (url): URL | |
| 640 | // A function used to post process URLs for assets before `postresolve()`. | |
| 641 | asset | |
| 642 | } | |
| 643 | ``` | |
| 644 | ||
| 645 | ## License | |
| 646 | ||
| 647 | Apache-2.0 |
| … | … | @@ -0,0 +1,712 @@ |
| 1 | #include <assert.h> | |
| 2 | #include <bare.h> | |
| 3 | #include <js.h> | |
| 4 | #include <stdbool.h> | |
| 5 | #include <stddef.h> | |
| 6 | #include <stdint.h> | |
| 7 | #include <stdlib.h> | |
| 8 | #include <utf.h> | |
| 9 | #include <uv.h> | |
| 10 | ||
| 11 | typedef struct { | |
| 12 | js_env_t *env; | |
| 13 | js_ref_t *ctx; | |
| 14 | js_ref_t *on_import; | |
| 15 | js_ref_t *on_evaluate; | |
| 16 | js_ref_t *on_meta; | |
| 17 | } bare_module_context_t; | |
| 18 | ||
| 19 | static js_module_t * | |
| 20 | bare_module__on_static_import(js_env_t *env, js_value_t *specifier, js_value_t *assertions, js_module_t *referrer, void *data) { | |
| 21 | bare_module_context_t *context = (bare_module_context_t *) data; | |
| 22 | ||
| 23 | int err; | |
| 24 | ||
| 25 | js_handle_scope_t *scope; | |
| 26 | err = js_open_handle_scope(env, &scope); | |
| 27 | assert(err == 0); | |
| 28 | ||
| 29 | js_value_t *ctx; | |
| 30 | err = js_get_reference_value(env, context->ctx, &ctx); | |
| 31 | assert(err == 0); | |
| 32 | ||
| 33 | js_value_t *on_import; | |
| 34 | err = js_get_reference_value(env, context->on_import, &on_import); | |
| 35 | assert(err == 0); | |
| 36 | ||
| 37 | const char *name; | |
| 38 | err = js_get_module_name(env, referrer, &name); | |
| 39 | assert(err == 0); | |
| 40 | ||
| 41 | js_value_t *args[4] = {specifier, assertions}; | |
| 42 | ||
| 43 | err = js_create_string_utf8(env, (utf8_t *) name, -1, &args[2]); | |
| 44 | if (err < 0) goto err; | |
| 45 | ||
| 46 | err = js_get_boolean(env, false, &args[3]); | |
| 47 | assert(err == 0); | |
| 48 | ||
| 49 | js_value_t *result; | |
| 50 | err = js_call_function(env, ctx, on_import, 4, args, &result); | |
| 51 | if (err < 0) goto err; | |
| 52 | ||
| 53 | js_module_t *module; | |
| 54 | err = js_get_value_external(env, result, (void **) &module); | |
| 55 | if (err < 0) goto err; | |
| 56 | ||
| 57 | err = js_close_handle_scope(env, scope); | |
| 58 | assert(err == 0); | |
| 59 | ||
| 60 | return module; | |
| 61 | ||
| 62 | err: | |
| 63 | err = js_close_handle_scope(env, scope); | |
| 64 | assert(err == 0); | |
| 65 | ||
| 66 | return NULL; | |
| 67 | } | |
| 68 | ||
| 69 | static js_value_t * | |
| 70 | bare_module__on_dynamic_import(js_env_t *env, js_value_t *specifier, js_value_t *assertions, js_value_t *referrer, void *data) { | |
| 71 | bare_module_context_t *context = (bare_module_context_t *) data; | |
| 72 | ||
| 73 | int err; | |
| 74 | ||
| 75 | js_escapable_handle_scope_t *scope; | |
| 76 | err = js_open_escapable_handle_scope(env, &scope); | |
| 77 | assert(err == 0); | |
| 78 | ||
| 79 | js_value_t *ctx; | |
| 80 | err = js_get_reference_value(env, context->ctx, &ctx); | |
| 81 | assert(err == 0); | |
| 82 | ||
| 83 | js_value_t *on_import; | |
| 84 | err = js_get_reference_value(env, context->on_import, &on_import); | |
| 85 | assert(err == 0); | |
| 86 | ||
| 87 | js_value_t *args[4] = {specifier, assertions, referrer}; | |
| 88 | ||
| 89 | err = js_get_boolean(env, true, &args[3]); | |
| 90 | assert(err == 0); | |
| 91 | ||
| 92 | js_value_t *result; | |
| 93 | err = js_call_function(env, ctx, on_import, 4, args, &result); | |
| 94 | if (err < 0) goto err; | |
| 95 | ||
| 96 | err = js_escape_handle(env, scope, result, &result); | |
| 97 | assert(err == 0); | |
| 98 | ||
| 99 | err = js_close_escapable_handle_scope(env, scope); | |
| 100 | assert(err == 0); | |
| 101 | ||
| 102 | return result; | |
| 103 | ||
| 104 | err: | |
| 105 | err = js_close_escapable_handle_scope(env, scope); | |
| 106 | assert(err == 0); | |
| 107 | ||
| 108 | return NULL; | |
| 109 | } | |
| 110 | ||
| 111 | static void | |
| 112 | bare_module__on_evaluate(js_env_t *env, js_module_t *module, void *data) { | |
| 113 | bare_module_context_t *context = (bare_module_context_t *) data; | |
| 114 | ||
| 115 | int err; | |
| 116 | ||
| 117 | js_handle_scope_t *scope; | |
| 118 | err = js_open_handle_scope(env, &scope); | |
| 119 | assert(err == 0); | |
| 120 | ||
| 121 | js_value_t *ctx; | |
| 122 | err = js_get_reference_value(env, context->ctx, &ctx); | |
| 123 | assert(err == 0); | |
| 124 | ||
| 125 | js_value_t *on_evaluate; | |
| 126 | err = js_get_reference_value(env, context->on_evaluate, &on_evaluate); | |
| 127 | assert(err == 0); | |
| 128 | ||
| 129 | const char *name; | |
| 130 | err = js_get_module_name(env, module, &name); | |
| 131 | assert(err == 0); | |
| 132 | ||
| 133 | js_value_t *args[1]; | |
| 134 | ||
| 135 | err = js_create_string_utf8(env, (utf8_t *) name, -1, &args[0]); | |
| 136 | if (err < 0) goto err; | |
| 137 | ||
| 138 | js_value_t *result; | |
| 139 | err = js_call_function(env, ctx, on_evaluate, 1, args, &result); | |
| 140 | if (err < 0) goto err; | |
| 141 | ||
| 142 | err = js_close_handle_scope(env, scope); | |
| 143 | assert(err == 0); | |
| 144 | ||
| 145 | return; | |
| 146 | ||
| 147 | err: | |
| 148 | err = js_close_handle_scope(env, scope); | |
| 149 | assert(err == 0); | |
| 150 | } | |
| 151 | ||
| 152 | static void | |
| 153 | bare_module__on_meta(js_env_t *env, js_module_t *module, js_value_t *meta, void *data) { | |
| 154 | bare_module_context_t *context = (bare_module_context_t *) data; | |
| 155 | ||
| 156 | int err; | |
| 157 | ||
| 158 | js_handle_scope_t *scope; | |
| 159 | err = js_open_handle_scope(env, &scope); | |
| 160 | assert(err == 0); | |
| 161 | ||
| 162 | js_value_t *ctx; | |
| 163 | err = js_get_reference_value(env, context->ctx, &ctx); | |
| 164 | assert(err == 0); | |
| 165 | ||
| 166 | js_value_t *on_meta; | |
| 167 | err = js_get_reference_value(env, context->on_meta, &on_meta); | |
| 168 | assert(err == 0); | |
| 169 | ||
| 170 | const char *name; | |
| 171 | err = js_get_module_name(env, module, &name); | |
| 172 | assert(err == 0); | |
| 173 | ||
| 174 | js_value_t *args[2]; | |
| 175 | ||
| 176 | err = js_create_string_utf8(env, (utf8_t *) name, -1, &args[0]); | |
| 177 | if (err < 0) goto err; | |
| 178 | ||
| 179 | args[1] = meta; | |
| 180 | ||
| 181 | js_value_t *result; | |
| 182 | err = js_call_function(env, ctx, on_meta, 2, args, &result); | |
| 183 | if (err < 0) goto err; | |
| 184 | ||
| 185 | err = js_close_handle_scope(env, scope); | |
| 186 | assert(err == 0); | |
| 187 | ||
| 188 | return; | |
| 189 | ||
| 190 | err: | |
| 191 | err = js_close_handle_scope(env, scope); | |
| 192 | assert(err == 0); | |
| 193 | } | |
| 194 | ||
| 195 | static void | |
| 196 | bare_module__on_teardown(void *data) { | |
| 197 | int err; | |
| 198 | ||
| 199 | bare_module_context_t *context = data; | |
| 200 | ||
| 201 | js_env_t *env = context->env; | |
| 202 | ||
| 203 | err = js_delete_reference(env, context->on_import); | |
| 204 | assert(err == 0); | |
| 205 | ||
| 206 | err = js_delete_reference(env, context->on_evaluate); | |
| 207 | assert(err == 0); | |
| 208 | ||
| 209 | err = js_delete_reference(env, context->on_meta); | |
| 210 | assert(err == 0); | |
| 211 | ||
| 212 | err = js_delete_reference(env, context->ctx); | |
| 213 | assert(err == 0); | |
| 214 | } | |
| 215 | ||
| 216 | static js_value_t * | |
| 217 | bare_module_init(js_env_t *env, js_callback_info_t *info) { | |
| 218 | int err; | |
| 219 | ||
| 220 | size_t argc = 4; | |
| 221 | js_value_t *argv[4]; | |
| 222 | ||
| 223 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 224 | assert(err == 0); | |
| 225 | ||
| 226 | assert(argc == 4); | |
| 227 | ||
| 228 | bare_module_context_t *context; | |
| 229 | ||
| 230 | js_value_t *result; | |
| 231 | err = js_create_unsafe_arraybuffer(env, sizeof(bare_module_context_t), (void **) &context, &result); | |
| 232 | assert(err == 0); | |
| 233 | ||
| 234 | context->env = env; | |
| 235 | ||
| 236 | err = js_create_reference(env, argv[0], 1, &context->ctx); | |
| 237 | assert(err == 0); | |
| 238 | ||
| 239 | err = js_create_reference(env, argv[1], 1, &context->on_import); | |
| 240 | assert(err == 0); | |
| 241 | ||
| 242 | err = js_create_reference(env, argv[2], 1, &context->on_evaluate); | |
| 243 | assert(err == 0); | |
| 244 | ||
| 245 | err = js_create_reference(env, argv[3], 1, &context->on_meta); | |
| 246 | assert(err == 0); | |
| 247 | ||
| 248 | err = js_add_teardown_callback(env, bare_module__on_teardown, context); | |
| 249 | assert(err == 0); | |
| 250 | ||
| 251 | err = js_on_dynamic_import(env, NULL, NULL); | |
| 252 | assert(err == 0); | |
| 253 | ||
| 254 | err = js_on_dynamic_import_transitional(env, bare_module__on_dynamic_import, (void *) context); | |
| 255 | assert(err == 0); | |
| 256 | ||
| 257 | return result; | |
| 258 | } | |
| 259 | ||
| 260 | static js_value_t * | |
| 261 | bare_module_create_function(js_env_t *env, js_callback_info_t *info) { | |
| 262 | int err; | |
| 263 | ||
| 264 | size_t argc = 4; | |
| 265 | js_value_t *argv[4]; | |
| 266 | ||
| 267 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 268 | assert(err == 0); | |
| 269 | ||
| 270 | assert(argc == 4); | |
| 271 | ||
| 272 | size_t file_len; | |
| 273 | utf8_t file[1024]; | |
| 274 | err = js_get_value_string_utf8(env, argv[0], file, 1024, &file_len); | |
| 275 | if (err < 0) return NULL; | |
| 276 | ||
| 277 | uint32_t args_len; | |
| 278 | err = js_get_array_length(env, argv[1], &args_len); | |
| 279 | if (err < 0) return NULL; | |
| 280 | ||
| 281 | js_value_t **args = malloc(sizeof(js_value_t *) * args_len); | |
| 282 | ||
| 283 | err = js_get_array_elements(env, argv[1], args, args_len, 0, NULL); | |
| 284 | if (err < 0) goto err; | |
| 285 | ||
| 286 | js_value_t *source = argv[2]; | |
| 287 | ||
| 288 | int32_t offset; | |
| 289 | err = js_get_value_int32(env, argv[3], &offset); | |
| 290 | if (err < 0) goto err; | |
| 291 | ||
| 292 | js_value_t *result; | |
| 293 | err = js_create_function_with_source(env, NULL, 0, (char *) file, file_len, args, args_len, 0, source, &result); | |
| 294 | if (err < 0) goto err; | |
| 295 | ||
| 296 | free(args); | |
| 297 | ||
| 298 | return result; | |
| 299 | ||
| 300 | err: | |
| 301 | free(args); | |
| 302 | ||
| 303 | return NULL; | |
| 304 | } | |
| 305 | ||
| 306 | static void | |
| 307 | bare_module__on_finalize(js_env_t *env, void *data, void *finalize_hint) { | |
| 308 | int err; | |
| 309 | ||
| 310 | js_module_t *module = data; | |
| 311 | ||
| 312 | err = js_delete_module(env, module); | |
| 313 | assert(err == 0); | |
| 314 | } | |
| 315 | ||
| 316 | static js_value_t * | |
| 317 | bare_module_create_module(js_env_t *env, js_callback_info_t *info) { | |
| 318 | int err; | |
| 319 | ||
| 320 | size_t argc = 4; | |
| 321 | js_value_t *argv[4]; | |
| 322 | ||
| 323 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 324 | assert(err == 0); | |
| 325 | ||
| 326 | assert(argc == 4); | |
| 327 | ||
| 328 | size_t file_len; | |
| 329 | utf8_t file[1024]; | |
| 330 | err = js_get_value_string_utf8(env, argv[0], file, 1024, &file_len); | |
| 331 | if (err < 0) return NULL; | |
| 332 | ||
| 333 | js_value_t *source = argv[1]; | |
| 334 | ||
| 335 | int32_t offset; | |
| 336 | err = js_get_value_int32(env, argv[2], &offset); | |
| 337 | if (err < 0) return NULL; | |
| 338 | ||
| 339 | bare_module_context_t *context; | |
| 340 | err = js_get_arraybuffer_info(env, argv[3], (void **) &context, NULL); | |
| 341 | if (err < 0) return NULL; | |
| 342 | ||
| 343 | js_module_t *module; | |
| 344 | err = js_create_module(env, (char *) file, file_len, offset, source, bare_module__on_meta, (void *) context, &module); | |
| 345 | if (err < 0) return NULL; | |
| 346 | ||
| 347 | js_value_t *result; | |
| 348 | err = js_create_external(env, (void *) module, bare_module__on_finalize, NULL, &result); | |
| 349 | if (err < 0) return NULL; | |
| 350 | ||
| 351 | return result; | |
| 352 | } | |
| 353 | ||
| 354 | static js_value_t * | |
| 355 | bare_module_create_synthetic_module(js_env_t *env, js_callback_info_t *info) { | |
| 356 | int err; | |
| 357 | ||
| 358 | size_t argc = 3; | |
| 359 | js_value_t *argv[3]; | |
| 360 | ||
| 361 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 362 | assert(err == 0); | |
| 363 | ||
| 364 | assert(argc == 3); | |
| 365 | ||
| 366 | size_t file_len; | |
| 367 | utf8_t file[1024]; | |
| 368 | err = js_get_value_string_utf8(env, argv[0], file, 1024, &file_len); | |
| 369 | if (err < 0) return NULL; | |
| 370 | ||
| 371 | uint32_t names_len; | |
| 372 | err = js_get_array_length(env, argv[1], &names_len); | |
| 373 | if (err < 0) return NULL; | |
| 374 | ||
| 375 | js_value_t **export_names = malloc(sizeof(js_value_t *) * names_len); | |
| 376 | ||
| 377 | err = js_get_array_elements(env, argv[1], export_names, names_len, 0, NULL); | |
| 378 | if (err < 0) goto err; | |
| 379 | ||
| 380 | bare_module_context_t *context; | |
| 381 | err = js_get_arraybuffer_info(env, argv[2], (void **) &context, NULL); | |
| 382 | if (err < 0) goto err; | |
| 383 | ||
| 384 | js_module_t *module; | |
| 385 | err = js_create_synthetic_module(env, (char *) file, file_len, export_names, names_len, bare_module__on_evaluate, (void *) context, &module); | |
| 386 | if (err < 0) goto err; | |
| 387 | ||
| 388 | js_value_t *result; | |
| 389 | err = js_create_external(env, (void *) module, bare_module__on_finalize, NULL, &result); | |
| 390 | if (err < 0) goto err; | |
| 391 | ||
| 392 | free(export_names); | |
| 393 | ||
| 394 | return result; | |
| 395 | ||
| 396 | err: | |
| 397 | free(export_names); | |
| 398 | ||
| 399 | return NULL; | |
| 400 | } | |
| 401 | ||
| 402 | static js_value_t * | |
| 403 | bare_module_set_export(js_env_t *env, js_callback_info_t *info) { | |
| 404 | int err; | |
| 405 | ||
| 406 | size_t argc = 3; | |
| 407 | js_value_t *argv[3]; | |
| 408 | ||
| 409 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 410 | assert(err == 0); | |
| 411 | ||
| 412 | assert(argc == 3); | |
| 413 | ||
| 414 | js_module_t *module; | |
| 415 | err = js_get_value_external(env, argv[0], (void **) &module); | |
| 416 | if (err < 0) return NULL; | |
| 417 | ||
| 418 | js_set_module_export(env, module, argv[1], argv[2]); | |
| 419 | ||
| 420 | return NULL; | |
| 421 | } | |
| 422 | ||
| 423 | static js_value_t * | |
| 424 | bare_module_run_module(js_env_t *env, js_callback_info_t *info) { | |
| 425 | int err; | |
| 426 | ||
| 427 | size_t argc = 3; | |
| 428 | js_value_t *argv[3]; | |
| 429 | ||
| 430 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 431 | assert(err == 0); | |
| 432 | ||
| 433 | assert(argc == 3); | |
| 434 | ||
| 435 | js_module_t *module; | |
| 436 | err = js_get_value_external(env, argv[0], (void **) &module); | |
| 437 | if (err < 0) return NULL; | |
| 438 | ||
| 439 | bare_module_context_t *context; | |
| 440 | err = js_get_arraybuffer_info(env, argv[1], (void **) &context, NULL); | |
| 441 | if (err < 0) return NULL; | |
| 442 | ||
| 443 | err = js_instantiate_module(env, module, bare_module__on_static_import, (void *) context); | |
| 444 | if (err < 0) return NULL; | |
| 445 | ||
| 446 | js_value_t *promise; | |
| 447 | err = js_run_module(env, module, &promise); | |
| 448 | if (err < 0) return NULL; | |
| 449 | ||
| 450 | bool is_promise; | |
| 451 | err = js_is_promise(env, promise, &is_promise); | |
| 452 | assert(err == 0); | |
| 453 | ||
| 454 | if (is_promise) { | |
| 455 | js_promise_state_t state; | |
| 456 | err = js_get_promise_state(env, promise, &state); | |
| 457 | assert(err == 0); | |
| 458 | ||
| 459 | js_value_t *reason; | |
| 460 | ||
| 461 | if (state == js_promise_rejected) { | |
| 462 | err = js_get_promise_result(env, promise, &reason); | |
| 463 | if (err < 0) return NULL; | |
| 464 | } else { | |
| 465 | err = js_get_null(env, &reason); | |
| 466 | assert(err == 0); | |
| 467 | } | |
| 468 | ||
| 469 | js_value_t *exception; | |
| 470 | err = js_get_and_clear_last_exception(env, &exception); | |
| 471 | assert(err == 0); | |
| 472 | ||
| 473 | js_value_t *ctx; | |
| 474 | err = js_get_reference_value(env, context->ctx, &ctx); | |
| 475 | assert(err == 0); | |
| 476 | ||
| 477 | js_value_t *args[3] = {reason, promise, exception}; | |
| 478 | ||
| 479 | js_call_function(env, ctx, argv[2], 3, args, NULL); | |
| 480 | } | |
| 481 | ||
| 482 | return promise; | |
| 483 | } | |
| 484 | ||
| 485 | static js_value_t * | |
| 486 | bare_module_get_namespace(js_env_t *env, js_callback_info_t *info) { | |
| 487 | int err; | |
| 488 | ||
| 489 | size_t argc = 1; | |
| 490 | js_value_t *argv[1]; | |
| 491 | ||
| 492 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 493 | assert(err == 0); | |
| 494 | ||
| 495 | js_module_t *module; | |
| 496 | err = js_get_value_external(env, argv[0], (void **) &module); | |
| 497 | if (err < 0) return NULL; | |
| 498 | ||
| 499 | js_value_t *result; | |
| 500 | err = js_get_module_namespace(env, module, &result); | |
| 501 | if (err < 0) return NULL; | |
| 502 | ||
| 503 | return result; | |
| 504 | } | |
| 505 | ||
| 506 | static js_value_t * | |
| 507 | bare_module_exists(js_env_t *env, js_callback_info_t *info) { | |
| 508 | int err; | |
| 509 | ||
| 510 | uv_loop_t *loop; | |
| 511 | err = js_get_env_loop(env, &loop); | |
| 512 | assert(err == 0); | |
| 513 | ||
| 514 | js_value_t *argv[2]; | |
| 515 | size_t argc = 2; | |
| 516 | ||
| 517 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 518 | assert(err == 0); | |
| 519 | ||
| 520 | assert(argc == 2); | |
| 521 | ||
| 522 | utf8_t path[4096]; | |
| 523 | err = js_get_value_string_utf8(env, argv[0], path, 4096, NULL); | |
| 524 | assert(err == 0); | |
| 525 | ||
| 526 | uint32_t mode; | |
| 527 | err = js_get_value_uint32(env, argv[1], &mode); | |
| 528 | assert(err == 0); | |
| 529 | ||
| 530 | uv_fs_t req; | |
| 531 | uv_fs_stat(loop, &req, (char *) path, NULL); | |
| 532 | ||
| 533 | uv_stat_t *st = req.result < 0 ? NULL : req.ptr; | |
| 534 | ||
| 535 | js_value_t *result; | |
| 536 | err = js_get_boolean(env, st && st->st_mode & mode, &result); | |
| 537 | assert(err == 0); | |
| 538 | ||
| 539 | uv_fs_req_cleanup(&req); | |
| 540 | ||
| 541 | return result; | |
| 542 | } | |
| 543 | ||
| 544 | static js_value_t * | |
| 545 | bare_module_realpath(js_env_t *env, js_callback_info_t *info) { | |
| 546 | int err; | |
| 547 | ||
| 548 | uv_loop_t *loop; | |
| 549 | err = js_get_env_loop(env, &loop); | |
| 550 | assert(err == 0); | |
| 551 | ||
| 552 | js_value_t *argv[1]; | |
| 553 | size_t argc = 1; | |
| 554 | ||
| 555 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 556 | assert(err == 0); | |
| 557 | ||
| 558 | assert(argc == 1); | |
| 559 | ||
| 560 | utf8_t path[4096]; | |
| 561 | err = js_get_value_string_utf8(env, argv[0], path, 4096, NULL); | |
| 562 | assert(err == 0); | |
| 563 | ||
| 564 | uv_fs_t req; | |
| 565 | uv_fs_realpath(loop, &req, (char *) path, NULL); | |
| 566 | ||
| 567 | int res = req.result; | |
| 568 | ||
| 569 | if (res < 0) { | |
| 570 | uv_fs_req_cleanup(&req); | |
| 571 | err = res; | |
| 572 | goto err; | |
| 573 | } | |
| 574 | ||
| 575 | js_value_t *result; | |
| 576 | err = js_create_string_utf8(env, (utf8_t *) req.ptr, -1, &result); | |
| 577 | assert(err == 0); | |
| 578 | ||
| 579 | uv_fs_req_cleanup(&req); | |
| 580 | ||
| 581 | return result; | |
| 582 | ||
| 583 | err: | |
| 584 | err = js_throw_error(env, uv_err_name(err), uv_strerror(err)); | |
| 585 | assert(err == 0); | |
| 586 | ||
| 587 | return NULL; | |
| 588 | } | |
| 589 | ||
| 590 | static js_value_t * | |
| 591 | bare_module_read(js_env_t *env, js_callback_info_t *info) { | |
| 592 | int err; | |
| 593 | ||
| 594 | uv_loop_t *loop; | |
| 595 | err = js_get_env_loop(env, &loop); | |
| 596 | assert(err == 0); | |
| 597 | ||
| 598 | js_value_t *argv[1]; | |
| 599 | size_t argc = 1; | |
| 600 | ||
| 601 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 602 | assert(err == 0); | |
| 603 | ||
| 604 | assert(argc == 1); | |
| 605 | ||
| 606 | utf8_t path[4096]; | |
| 607 | err = js_get_value_string_utf8(env, argv[0], path, 4096, NULL); | |
| 608 | assert(err == 0); | |
| 609 | ||
| 610 | uv_fs_t req; | |
| 611 | uv_fs_open(loop, &req, (char *) path, UV_FS_O_RDONLY, 0, NULL); | |
| 612 | ||
| 613 | int fd = req.result; | |
| 614 | uv_fs_req_cleanup(&req); | |
| 615 | ||
| 616 | if (fd < 0) { | |
| 617 | err = fd; | |
| 618 | goto err; | |
| 619 | } | |
| 620 | ||
| 621 | uv_fs_fstat(loop, &req, fd, NULL); | |
| 622 | uv_stat_t *st = req.ptr; | |
| 623 | ||
| 624 | size_t len = st->st_size; | |
| 625 | char *base; | |
| 626 | ||
| 627 | js_value_t *result; | |
| 628 | err = js_create_arraybuffer(env, len, (void **) &base, &result); | |
| 629 | assert(err == 0); | |
| 630 | ||
| 631 | uv_buf_t buffer = uv_buf_init(base, len); | |
| 632 | ||
| 633 | uv_fs_req_cleanup(&req); | |
| 634 | ||
| 635 | int64_t read = 0; | |
| 636 | ||
| 637 | while (true) { | |
| 638 | uv_fs_read(loop, &req, fd, &buffer, 1, read, NULL); | |
| 639 | ||
| 640 | int res = req.result; | |
| 641 | uv_fs_req_cleanup(&req); | |
| 642 | ||
| 643 | if (res < 0) { | |
| 644 | uv_fs_close(loop, &req, fd, NULL); | |
| 645 | uv_fs_req_cleanup(&req); | |
| 646 | err = res; | |
| 647 | goto err; | |
| 648 | } | |
| 649 | ||
| 650 | buffer.base += res; | |
| 651 | buffer.len -= res; | |
| 652 | ||
| 653 | read += res; | |
| 654 | if (res == 0 || read == len) break; | |
| 655 | } | |
| 656 | ||
| 657 | uv_fs_close(loop, &req, fd, NULL); | |
| 658 | uv_fs_req_cleanup(&req); | |
| 659 | ||
| 660 | return result; | |
| 661 | ||
| 662 | err: | |
| 663 | err = js_throw_error(env, uv_err_name(err), uv_strerror(err)); | |
| 664 | assert(err == 0); | |
| 665 | ||
| 666 | return NULL; | |
| 667 | } | |
| 668 | ||
| 669 | static js_value_t * | |
| 670 | bare_module_exports(js_env_t *env, js_value_t *exports) { | |
| 671 | int err; | |
| 672 | ||
| 673 | #define V(name, fn) \ | |
| 674 | { \ | |
| 675 | js_value_t *val; \ | |
| 676 | err = js_create_function(env, name, -1, fn, NULL, &val); \ | |
| 677 | assert(err == 0); \ | |
| 678 | err = js_set_named_property(env, exports, name, val); \ | |
| 679 | assert(err == 0); \ | |
| 680 | } | |
| 681 | ||
| 682 | V("init", bare_module_init) | |
| 683 | ||
| 684 | V("createFunction", bare_module_create_function) | |
| 685 | V("createModule", bare_module_create_module) | |
| 686 | V("createSyntheticModule", bare_module_create_synthetic_module) | |
| 687 | V("setExport", bare_module_set_export) | |
| 688 | V("runModule", bare_module_run_module) | |
| 689 | V("getNamespace", bare_module_get_namespace) | |
| 690 | ||
| 691 | V("exists", bare_module_exists) | |
| 692 | V("realpath", bare_module_realpath) | |
| 693 | V("read", bare_module_read) | |
| 694 | #undef V | |
| 695 | ||
| 696 | #define V(name, n) \ | |
| 697 | { \ | |
| 698 | js_value_t *val; \ | |
| 699 | err = js_create_uint32(env, n, &val); \ | |
| 700 | assert(err == 0); \ | |
| 701 | err = js_set_named_property(env, exports, name, val); \ | |
| 702 | assert(err == 0); \ | |
| 703 | } | |
| 704 | ||
| 705 | V("FILE", S_IFREG) | |
| 706 | V("DIR", S_IFDIR) | |
| 707 | #undef V | |
| 708 | ||
| 709 | return exports; | |
| 710 | } | |
| 711 | ||
| 712 | BARE_MODULE(bare_module, bare_module_exports) |
| … | … | @@ -0,0 +1 @@ |
| 1 | module.exports = require.addon() |
| … | … | @@ -0,0 +1,117 @@ |
| 1 | import Buffer from 'bare-buffer' | |
| 2 | import URL from 'bare-url' | |
| 3 | import Bundle from 'bare-bundle' | |
| 4 | import { | |
| 5 | type Builtins, | |
| 6 | type Conditions, | |
| 7 | type ImportsMap, | |
| 8 | type ResolutionsMap | |
| 9 | } from 'bare-module-resolve' | |
| 10 | import Protocol from './lib/protocol' | |
| 11 | import constants from './lib/constants' | |
| 12 | ||
| 13 | interface Cache { | |
| 14 | [href: string]: Module | |
| 15 | } | |
| 16 | ||
| 17 | interface Attributes { | |
| 18 | type: Lowercase<keyof typeof constants.types> | |
| 19 | } | |
| 20 | ||
| 21 | interface Options { | |
| 22 | attributes?: Attributes | |
| 23 | builtins?: Builtins | |
| 24 | cache?: Cache | |
| 25 | conditions?: Conditions | |
| 26 | defaultType?: number | |
| 27 | imports?: ImportsMap | |
| 28 | main?: Module | |
| 29 | protocol?: Protocol | |
| 30 | referrer?: Module | |
| 31 | resolutions?: ResolutionsMap | |
| 32 | type?: number | |
| 33 | } | |
| 34 | ||
| 35 | interface LoadOptions extends Options { | |
| 36 | isDynamicImport?: boolean | |
| 37 | isImport?: boolean | |
| 38 | } | |
| 39 | ||
| 40 | interface ResolveOptions extends Options { | |
| 41 | isImport?: boolean | |
| 42 | } | |
| 43 | ||
| 44 | interface Module { | |
| 45 | readonly builtins: Builtins | |
| 46 | readonly cache: Cache | |
| 47 | readonly conditions: Conditions | |
| 48 | readonly defaultType: number | |
| 49 | readonly dirname: string | |
| 50 | exports: unknown | |
| 51 | readonly filename: string | |
| 52 | readonly id: string | |
| 53 | readonly imports: ImportsMap | |
| 54 | readonly main: Module | |
| 55 | readonly path: string | |
| 56 | readonly protocol: Protocol | |
| 57 | readonly resolutions: ResolutionsMap | |
| 58 | readonly type: number | |
| 59 | readonly url: URL | |
| 60 | } | |
| 61 | ||
| 62 | declare class Module { | |
| 63 | static readonly protocol: Protocol | |
| 64 | static readonly cache: Cache | |
| 65 | ||
| 66 | static load(url: URL, opts: LoadOptions): Module | |
| 67 | static load(url: URL, source?: Buffer | string | Bundle | null, opts?: LoadOptions): Module | |
| 68 | ||
| 69 | static resolve(specifier: string, parentURL: URL, opts?: ResolveOptions): URL | |
| 70 | ||
| 71 | static asset(specifier: string, parentURL: URL, opts?: Options): URL | |
| 72 | ||
| 73 | constructor(url: URL) | |
| 74 | } | |
| 75 | ||
| 76 | declare namespace Module { | |
| 77 | export { | |
| 78 | type Attributes, | |
| 79 | type Cache, | |
| 80 | type Options, | |
| 81 | type LoadOptions, | |
| 82 | type ResolveOptions, | |
| 83 | Protocol, | |
| 84 | constants | |
| 85 | } | |
| 86 | ||
| 87 | export const builtinModules: Module[] | |
| 88 | ||
| 89 | export function isBuiltin(): boolean | |
| 90 | ||
| 91 | export interface CreateRequireOptions extends Options { | |
| 92 | module?: Module | |
| 93 | } | |
| 94 | ||
| 95 | export interface RequireOptions { | |
| 96 | with?: Attributes | |
| 97 | } | |
| 98 | ||
| 99 | export interface RequireAddon { | |
| 100 | (specifier?: string, parentURL?: URL): string | |
| 101 | host: string | |
| 102 | resolve: (specifier: string, parentURL?: URL) => unknown | |
| 103 | } | |
| 104 | ||
| 105 | export interface Require { | |
| 106 | (parentURL: string | URL, opts?: RequireOptions): unknown | |
| 107 | main: Module | |
| 108 | cache: Cache | |
| 109 | resolve: (specifier: string, parentURL?: URL) => string | |
| 110 | addon: RequireAddon | |
| 111 | asset: (specifier: string, parentURL?: URL) => string | |
| 112 | } | |
| 113 | ||
| 114 | export function createRequire(parentURL: string | URL, opts?: CreateRequireOptions): Require | |
| 115 | } | |
| 116 | ||
| 117 | export = Module |
| … | … | @@ -0,0 +1,1034 @@ |
| 1 | const path = require('bare-path') | |
| 2 | const resolve = require('bare-module-resolve') | |
| 3 | const lex = require('bare-module-lexer') | |
| 4 | const { isURL, fileURLToPath, pathToFileURL } = require('bare-url') | |
| 5 | const Bundle = require('bare-bundle') | |
| 6 | const Protocol = require('./lib/protocol') | |
| 7 | const constants = require('./lib/constants') | |
| 8 | const errors = require('./lib/errors') | |
| 9 | const binding = require('./binding') | |
| 10 | ||
| 11 | const kind = Symbol.for('bare.module.kind') | |
| 12 | ||
| 13 | const isWindows = Bare.platform === 'win32' | |
| 14 | ||
| 15 | const { startsWithWindowsDriveLetter } = resolve | |
| 16 | ||
| 17 | module.exports = exports = class Module { | |
| 18 | static get [kind]() { | |
| 19 | return 1 // Compatibility version | |
| 20 | } | |
| 21 | ||
| 22 | constructor(url) { | |
| 23 | this._url = url | |
| 24 | this._state = 0 | |
| 25 | this._type = 0 | |
| 26 | this._defaultType = 0 | |
| 27 | this._main = null | |
| 28 | this._exports = null | |
| 29 | this._imports = null | |
| 30 | this._resolutions = null | |
| 31 | this._builtins = null | |
| 32 | this._conditions = null | |
| 33 | this._protocol = null | |
| 34 | this._cache = null | |
| 35 | this._source = null | |
| 36 | this._function = null | |
| 37 | this._names = null | |
| 38 | this._promise = null | |
| 39 | this._handle = null | |
| 40 | ||
| 41 | Object.preventExtensions(this) | |
| 42 | } | |
| 43 | ||
| 44 | get [kind]() { | |
| 45 | return Module[kind] | |
| 46 | } | |
| 47 | ||
| 48 | get url() { | |
| 49 | return this._url | |
| 50 | } | |
| 51 | ||
| 52 | get type() { | |
| 53 | return this._type | |
| 54 | } | |
| 55 | ||
| 56 | get defaultType() { | |
| 57 | return this._defaultType | |
| 58 | } | |
| 59 | ||
| 60 | get main() { | |
| 61 | return this._main | |
| 62 | } | |
| 63 | ||
| 64 | get exports() { | |
| 65 | return this._exports | |
| 66 | } | |
| 67 | ||
| 68 | set exports(value) { | |
| 69 | this._exports = value | |
| 70 | } | |
| 71 | ||
| 72 | get imports() { | |
| 73 | return this._imports | |
| 74 | } | |
| 75 | ||
| 76 | get resolutions() { | |
| 77 | return this._resolutions | |
| 78 | } | |
| 79 | ||
| 80 | get builtins() { | |
| 81 | return this._builtins | |
| 82 | } | |
| 83 | ||
| 84 | get conditions() { | |
| 85 | return Array.from(this._conditions) | |
| 86 | } | |
| 87 | ||
| 88 | get protocol() { | |
| 89 | return this._protocol | |
| 90 | } | |
| 91 | ||
| 92 | get cache() { | |
| 93 | return this._cache | |
| 94 | } | |
| 95 | ||
| 96 | // For Node.js compatibility | |
| 97 | get filename() { | |
| 98 | return urlToPath(this._url) | |
| 99 | } | |
| 100 | ||
| 101 | // For Node.js compatibility | |
| 102 | get dirname() { | |
| 103 | return urlToDirname(this._url) | |
| 104 | } | |
| 105 | ||
| 106 | // For Node.js compatibility | |
| 107 | get id() { | |
| 108 | return this.filename | |
| 109 | } | |
| 110 | ||
| 111 | // For Node.js compatibility | |
| 112 | get path() { | |
| 113 | return this.dirname | |
| 114 | } | |
| 115 | ||
| 116 | _transform(isImport, isDynamicImport) { | |
| 117 | if (isDynamicImport) { | |
| 118 | this._run() | |
| 119 | } else if (isImport) { | |
| 120 | this._synthesize() | |
| 121 | } else { | |
| 122 | this._evaluate() | |
| 123 | } | |
| 124 | ||
| 125 | return this | |
| 126 | } | |
| 127 | ||
| 128 | _synthesize() { | |
| 129 | if ((this._state & constants.states.SYNTHESIZED) !== 0) return | |
| 130 | ||
| 131 | this._state |= constants.states.SYNTHESIZED | |
| 132 | ||
| 133 | if (this._type === constants.types.MODULE) return | |
| 134 | ||
| 135 | const names = new Set(['default']) | |
| 136 | const queue = [this] | |
| 137 | const seen = new Set() | |
| 138 | ||
| 139 | while (queue.length) { | |
| 140 | const module = queue.pop() | |
| 141 | ||
| 142 | if (seen.has(module)) continue | |
| 143 | ||
| 144 | seen.add(module) | |
| 145 | ||
| 146 | switch (module._type) { | |
| 147 | case constants.types.SCRIPT: { | |
| 148 | const result = lex(module._source) | |
| 149 | ||
| 150 | for (const { name } of result.exports) names.add(name) | |
| 151 | ||
| 152 | const referrer = module | |
| 153 | ||
| 154 | for (const { specifier, type } of result.imports) { | |
| 155 | if ( | |
| 156 | (type & lex.constants.REEXPORT) !== 0 && | |
| 157 | (type & lex.constants.ADDON) === 0 && | |
| 158 | (type & lex.constants.ASSET) === 0 | |
| 159 | ) { | |
| 160 | const resolved = Module.resolve(specifier, referrer._url, { | |
| 161 | isImport: true, | |
| 162 | referrer | |
| 163 | }) | |
| 164 | ||
| 165 | const module = Module.load(resolved, { | |
| 166 | isImport: true, | |
| 167 | referrer | |
| 168 | }) | |
| 169 | ||
| 170 | if (module._names) { | |
| 171 | for (const name of module._names) names.add(name) | |
| 172 | } else { | |
| 173 | queue.push(module) | |
| 174 | } | |
| 175 | } | |
| 176 | } | |
| 177 | ||
| 178 | break | |
| 179 | } | |
| 180 | ||
| 181 | case constants.types.MODULE: | |
| 182 | module._evaluate() | |
| 183 | ||
| 184 | for (const name of Object.keys(module._exports)) names.add(name) | |
| 185 | ||
| 186 | break | |
| 187 | ||
| 188 | case constants.types.JSON: | |
| 189 | for (const name of Object.keys(module._exports)) names.add(name) | |
| 190 | } | |
| 191 | } | |
| 192 | ||
| 193 | this._names = Array.from(names) | |
| 194 | ||
| 195 | this._handle = binding.createSyntheticModule(this._url.href, this._names, Module._handle) | |
| 196 | } | |
| 197 | ||
| 198 | _evaluate() { | |
| 199 | if ((this._state & constants.states.EVALUATED) !== 0) return | |
| 200 | ||
| 201 | this._state |= constants.states.EVALUATED | |
| 202 | ||
| 203 | if (this._type === constants.types.SCRIPT) { | |
| 204 | const require = exports.createRequire(this._url, { module: this }) | |
| 205 | ||
| 206 | this._exports = {} | |
| 207 | ||
| 208 | const fn = this._function // Bind to variable to ensure proper stack trace | |
| 209 | ||
| 210 | fn(require, this, this._exports, urlToPath(this._url), urlToDirname(this._url)) | |
| 211 | } else if (this._type === constants.types.MODULE) { | |
| 212 | this._run() | |
| 213 | ||
| 214 | this._exports = binding.getNamespace(this._handle) | |
| 215 | } | |
| 216 | } | |
| 217 | ||
| 218 | _run() { | |
| 219 | if ((this._state & constants.states.RUN) !== 0) return | |
| 220 | ||
| 221 | this._state |= constants.states.RUN | |
| 222 | ||
| 223 | this._synthesize() | |
| 224 | ||
| 225 | this._promise = binding.runModule(this._handle, Module._handle, Module._onrun) | |
| 226 | } | |
| 227 | ||
| 228 | [Symbol.for('bare.inspect')]() { | |
| 229 | return { | |
| 230 | __proto__: { constructor: Module }, | |
| 231 | ||
| 232 | url: this.url, | |
| 233 | type: this.type, | |
| 234 | defaultType: this.defaultType, | |
| 235 | main: this.main, | |
| 236 | exports: this.exports, | |
| 237 | imports: this.imports, | |
| 238 | resolutions: this.resolutions, | |
| 239 | builtins: this.builtins, | |
| 240 | conditions: this.conditions | |
| 241 | } | |
| 242 | } | |
| 243 | ||
| 244 | static _extensions = Object.create(null) | |
| 245 | ||
| 246 | static _protocol = null | |
| 247 | ||
| 248 | static _cache = | |
| 249 | module[kind] === Module[kind] ? module.cache || Object.create(null) : Object.create(null) | |
| 250 | ||
| 251 | static _conditions = ['bare', 'node', ...Bare.Addon.host.split('-')] | |
| 252 | ||
| 253 | static _handle = binding.init(this, this._onimport, this._onevaluate, this._onmeta) | |
| 254 | ||
| 255 | static _onimport(specifier, attributes, referrerHref, isDynamicImport) { | |
| 256 | const referrer = this._cache[referrerHref] || null | |
| 257 | ||
| 258 | if (referrer === null) { | |
| 259 | throw errors.MODULE_NOT_FOUND( | |
| 260 | `Cannot find referrer for module '${specifier}' imported from '${referrerHref}'`, | |
| 261 | specifier, | |
| 262 | referrer | |
| 263 | ) | |
| 264 | } | |
| 265 | ||
| 266 | const resolved = this.resolve(specifier, referrer._url, { | |
| 267 | isImport: true, | |
| 268 | referrer, | |
| 269 | attributes | |
| 270 | }) | |
| 271 | ||
| 272 | const module = this.load(resolved, { | |
| 273 | isImport: true, | |
| 274 | isDynamicImport, | |
| 275 | referrer, | |
| 276 | attributes | |
| 277 | }) | |
| 278 | ||
| 279 | return isDynamicImport | |
| 280 | ? module._promise.then(() => binding.getNamespace(module._handle)) | |
| 281 | : module._handle | |
| 282 | } | |
| 283 | ||
| 284 | static _onevaluate(href) { | |
| 285 | const module = this._cache[href] || null | |
| 286 | ||
| 287 | if (module === null) { | |
| 288 | throw errors.MODULE_NOT_FOUND(`Cannot find module '${href}'`, href) | |
| 289 | } | |
| 290 | ||
| 291 | module._evaluate() | |
| 292 | ||
| 293 | for (const name of module._names) { | |
| 294 | let value | |
| 295 | ||
| 296 | if ( | |
| 297 | name === 'default' && | |
| 298 | (typeof module._exports !== 'object' || | |
| 299 | module._exports === null || | |
| 300 | name in module._exports === false) | |
| 301 | ) { | |
| 302 | value = module._exports | |
| 303 | } else { | |
| 304 | value = module._exports[name] | |
| 305 | } | |
| 306 | ||
| 307 | binding.setExport(module._handle, name, value) | |
| 308 | } | |
| 309 | } | |
| 310 | ||
| 311 | static _onmeta(href, meta) { | |
| 312 | const self = Module | |
| 313 | ||
| 314 | const module = this._cache[href] || null | |
| 315 | ||
| 316 | if (module === null) { | |
| 317 | throw errors.MODULE_NOT_FOUND(`Cannot find module '${href}'`, href) | |
| 318 | } | |
| 319 | ||
| 320 | const referrer = module | |
| 321 | ||
| 322 | meta.url = module._url.href | |
| 323 | meta.main = module._main === module | |
| 324 | meta.cache = module._cache | |
| 325 | ||
| 326 | meta.resolve = function resolve(specifier, parentURL = referrer._url) { | |
| 327 | return self.resolve(specifier, toURL(parentURL, referrer._url), { | |
| 328 | referrer | |
| 329 | }).href | |
| 330 | } | |
| 331 | ||
| 332 | meta.addon = function addon(specifier = '.', parentURL = referrer._url) { | |
| 333 | const resolved = Bare.Addon.resolve(specifier, toURL(parentURL, referrer._url), { referrer }) | |
| 334 | ||
| 335 | const addon = Bare.Addon.load(resolved, { referrer }) | |
| 336 | ||
| 337 | return addon._exports | |
| 338 | } | |
| 339 | ||
| 340 | meta.addon.resolve = function resolve(specifier = '.', parentURL = referrer._url) { | |
| 341 | return Bare.Addon.resolve(specifier, toURL(parentURL, referrer._url), { | |
| 342 | referrer | |
| 343 | }).href | |
| 344 | } | |
| 345 | ||
| 346 | meta.addon.host = Bare.Addon.host | |
| 347 | ||
| 348 | meta.asset = function asset(specifier, parentURL = referrer._url) { | |
| 349 | return self.asset(specifier, toURL(parentURL, referrer._url), { | |
| 350 | referrer | |
| 351 | }).href | |
| 352 | } | |
| 353 | } | |
| 354 | ||
| 355 | static _onrun(reason, promise, err = reason) { | |
| 356 | if (err) { | |
| 357 | promise.catch(() => {}) // Don't leak the rejection | |
| 358 | ||
| 359 | throw err | |
| 360 | } else { | |
| 361 | promise.catch((err) => | |
| 362 | queueMicrotask(() => { | |
| 363 | throw err | |
| 364 | }) | |
| 365 | ) | |
| 366 | } | |
| 367 | } | |
| 368 | ||
| 369 | static get protocol() { | |
| 370 | return this._protocol | |
| 371 | } | |
| 372 | ||
| 373 | static get cache() { | |
| 374 | return this._cache | |
| 375 | } | |
| 376 | ||
| 377 | static load(url, source = null, opts = {}) { | |
| 378 | const self = Module | |
| 379 | ||
| 380 | if ( | |
| 381 | !ArrayBuffer.isView(source) && | |
| 382 | !Bundle.isBundle(source) && | |
| 383 | typeof source !== 'string' && | |
| 384 | source !== null | |
| 385 | ) { | |
| 386 | opts = source | |
| 387 | source = null | |
| 388 | } | |
| 389 | ||
| 390 | const { | |
| 391 | isImport = false, | |
| 392 | isDynamicImport = false, | |
| 393 | referrer = null, | |
| 394 | attributes, | |
| 395 | type = typeForAttributes(attributes), | |
| 396 | defaultType = referrer ? referrer._defaultType : 0, | |
| 397 | cache = referrer ? referrer._cache : self._cache, | |
| 398 | main = referrer ? referrer._main : null, | |
| 399 | protocol = referrer ? referrer._protocol : self._protocol, | |
| 400 | imports = referrer ? referrer._imports : null, | |
| 401 | resolutions = referrer ? referrer._resolutions : null, | |
| 402 | builtins = referrer ? referrer._builtins : null, | |
| 403 | conditions = referrer ? referrer._conditions : self._conditions | |
| 404 | } = opts | |
| 405 | ||
| 406 | let module = cache[url.href] || null | |
| 407 | ||
| 408 | if (module !== null) { | |
| 409 | if (type !== 0 && type !== module._type) { | |
| 410 | throw errors.TYPE_INCOMPATIBLE( | |
| 411 | `Module '${module.url.href}' is not of type '${nameOfType(type)}'` | |
| 412 | ) | |
| 413 | } | |
| 414 | ||
| 415 | return module._transform(isImport, isDynamicImport) | |
| 416 | } | |
| 417 | ||
| 418 | module = cache[url.href] = new Module(url) | |
| 419 | ||
| 420 | try { | |
| 421 | switch (url.protocol) { | |
| 422 | case 'builtin:': | |
| 423 | module._exports = builtins[url.pathname] | |
| 424 | break | |
| 425 | ||
| 426 | default: { | |
| 427 | module._defaultType = defaultType | |
| 428 | module._cache = cache | |
| 429 | module._main = main || module | |
| 430 | module._protocol = protocol | |
| 431 | module._imports = imports | |
| 432 | module._resolutions = resolutions | |
| 433 | module._builtins = builtins | |
| 434 | module._conditions = conditions | |
| 435 | ||
| 436 | if ( | |
| 437 | typeof attributes === 'object' && | |
| 438 | attributes !== null && | |
| 439 | typeof attributes.imports === 'string' | |
| 440 | ) { | |
| 441 | const resolved = self.resolve(attributes.imports, referrer._url, { | |
| 442 | referrer: module | |
| 443 | }) | |
| 444 | ||
| 445 | const imports = self.load(resolved, { | |
| 446 | referrer: module, | |
| 447 | type: constants.types.JSON | |
| 448 | }) | |
| 449 | ||
| 450 | module._imports = mixinImports(module._imports, imports._exports, resolved) | |
| 451 | } | |
| 452 | ||
| 453 | let extension = canonicalExtensionForType(type) || path.extname(url.pathname) | |
| 454 | ||
| 455 | if (extension in self._extensions === false) { | |
| 456 | if (defaultType) { | |
| 457 | extension = canonicalExtensionForType(defaultType) || '.js' | |
| 458 | } else { | |
| 459 | extension = '.js' | |
| 460 | } | |
| 461 | } | |
| 462 | ||
| 463 | self._extensions[extension](module, source, referrer) | |
| 464 | } | |
| 465 | } | |
| 466 | ||
| 467 | return module._transform(isImport, isDynamicImport) | |
| 468 | } catch (err) { | |
| 469 | delete cache[url.href] | |
| 470 | ||
| 471 | throw err | |
| 472 | } | |
| 473 | } | |
| 474 | ||
| 475 | static resolve(specifier, parentURL, opts = {}) { | |
| 476 | const self = Module | |
| 477 | ||
| 478 | if (typeof specifier !== 'string') { | |
| 479 | throw new TypeError( | |
| 480 | `Specifier must be a string. Received type ${typeof specifier} (${specifier})` | |
| 481 | ) | |
| 482 | } | |
| 483 | ||
| 484 | const { | |
| 485 | isImport = false, | |
| 486 | referrer = null, | |
| 487 | attributes, | |
| 488 | type = typeForAttributes(attributes), | |
| 489 | extensions = extensionsForType(type), | |
| 490 | protocol = referrer ? referrer._protocol : self._protocol, | |
| 491 | imports = referrer ? referrer._imports : null, | |
| 492 | resolutions = referrer ? referrer._resolutions : null, | |
| 493 | builtins = referrer ? referrer._builtins : null, | |
| 494 | conditions = referrer ? referrer._conditions : self._conditions | |
| 495 | } = opts | |
| 496 | ||
| 497 | const resolved = protocol.preresolve(specifier, parentURL) | |
| 498 | ||
| 499 | const [resolution] = protocol.resolve(resolved, parentURL, imports) | |
| 500 | ||
| 501 | if (resolution) return protocol.postresolve(resolution) | |
| 502 | ||
| 503 | const candidates = [] | |
| 504 | ||
| 505 | for (const resolution of resolve( | |
| 506 | resolved, | |
| 507 | parentURL, | |
| 508 | { | |
| 509 | conditions: isImport ? ['import', ...conditions] : ['require', ...conditions], | |
| 510 | imports, | |
| 511 | resolutions, | |
| 512 | extensions, | |
| 513 | builtins: builtins ? Object.keys(builtins) : [], | |
| 514 | engines: Bare.versions | |
| 515 | }, | |
| 516 | readPackage | |
| 517 | )) { | |
| 518 | candidates.push(resolution) | |
| 519 | ||
| 520 | switch (resolution.protocol) { | |
| 521 | case 'builtin:': | |
| 522 | return resolution | |
| 523 | default: | |
| 524 | if (protocol.exists(resolution, type)) { | |
| 525 | return protocol.postresolve(resolution) | |
| 526 | } | |
| 527 | } | |
| 528 | } | |
| 529 | ||
| 530 | let message = `Cannot find module '${specifier}' imported from '${parentURL.href}'` | |
| 531 | ||
| 532 | if (candidates.length > 0) { | |
| 533 | message += '\nCandidates:' | |
| 534 | message += '\n' + candidates.map((url) => '- ' + url.href).join('\n') | |
| 535 | } | |
| 536 | ||
| 537 | throw errors.MODULE_NOT_FOUND(message, specifier, parentURL, candidates) | |
| 538 | ||
| 539 | function readPackage(packageURL) { | |
| 540 | if (protocol.exists(packageURL, constants.types.JSON)) { | |
| 541 | return Module.load(packageURL, { protocol })._exports | |
| 542 | } | |
| 543 | ||
| 544 | return null | |
| 545 | } | |
| 546 | } | |
| 547 | ||
| 548 | static asset(specifier, parentURL, opts = {}) { | |
| 549 | const self = Module | |
| 550 | ||
| 551 | if (typeof specifier !== 'string') { | |
| 552 | throw new TypeError( | |
| 553 | `Specifier must be a string. Received type ${typeof specifier} (${specifier})` | |
| 554 | ) | |
| 555 | } | |
| 556 | ||
| 557 | const { | |
| 558 | referrer = null, | |
| 559 | protocol = referrer ? referrer._protocol : self._protocol, | |
| 560 | imports = referrer ? referrer._imports : null, | |
| 561 | resolutions = referrer ? referrer._resolutions : null, | |
| 562 | conditions = referrer ? referrer._conditions : self._conditions | |
| 563 | } = opts | |
| 564 | ||
| 565 | const resolved = protocol.preresolve(specifier, parentURL) | |
| 566 | ||
| 567 | const [resolution] = protocol.resolve(resolved, parentURL, imports) | |
| 568 | ||
| 569 | if (resolution) return protocol.postresolve(resolution) | |
| 570 | ||
| 571 | const candidates = [] | |
| 572 | ||
| 573 | for (const resolution of resolve( | |
| 574 | resolved, | |
| 575 | parentURL, | |
| 576 | { | |
| 577 | conditions: ['asset', ...conditions], | |
| 578 | imports, | |
| 579 | resolutions, | |
| 580 | engines: Bare.versions | |
| 581 | }, | |
| 582 | readPackage | |
| 583 | )) { | |
| 584 | candidates.push(resolution) | |
| 585 | ||
| 586 | if (protocol.exists(resolution, constants.types.ASSET)) { | |
| 587 | return protocol.postresolve(protocol.asset ? protocol.asset(resolution) : resolution) | |
| 588 | } | |
| 589 | } | |
| 590 | ||
| 591 | let message = `Cannot find asset '${specifier}' imported from '${parentURL.href}'` | |
| 592 | ||
| 593 | if (candidates.length > 0) { | |
| 594 | message += '\nCandidates:' | |
| 595 | message += '\n' + candidates.map((url) => '- ' + url.href).join('\n') | |
| 596 | } | |
| 597 | ||
| 598 | throw errors.ASSET_NOT_FOUND(message, specifier, parentURL, candidates) | |
| 599 | ||
| 600 | function readPackage(packageURL) { | |
| 601 | if (protocol.exists(packageURL, constants.types.JSON)) { | |
| 602 | return Module.load(packageURL, { protocol })._exports | |
| 603 | } | |
| 604 | ||
| 605 | return null | |
| 606 | } | |
| 607 | } | |
| 608 | } | |
| 609 | ||
| 610 | const Module = exports | |
| 611 | ||
| 612 | function extensionsForType(type) { | |
| 613 | switch (type) { | |
| 614 | case constants.types.SCRIPT: | |
| 615 | return ['.js', '.cjs'] | |
| 616 | case constants.types.MODULE: | |
| 617 | return ['.js', '.mjs'] | |
| 618 | case constants.types.JSON: | |
| 619 | return ['.json'] | |
| 620 | case constants.types.BUNDLE: | |
| 621 | return ['.bundle'] | |
| 622 | case constants.types.ADDON: | |
| 623 | return ['.bare', '.node'] | |
| 624 | case constants.types.BINARY: | |
| 625 | return ['.bin'] | |
| 626 | case constants.types.TEXT: | |
| 627 | return ['.txt'] | |
| 628 | default: | |
| 629 | return ['.js', '.cjs', '.mjs', '.json', '.bare', '.node'] | |
| 630 | } | |
| 631 | } | |
| 632 | ||
| 633 | function canonicalExtensionForType(type) { | |
| 634 | switch (type) { | |
| 635 | case constants.types.SCRIPT: | |
| 636 | return '.cjs' | |
| 637 | case constants.types.MODULE: | |
| 638 | return '.esm' | |
| 639 | case constants.types.JSON: | |
| 640 | return '.json' | |
| 641 | case constants.types.BUNDLE: | |
| 642 | return '.bundle' | |
| 643 | case constants.types.ADDON: | |
| 644 | return '.bare' | |
| 645 | case constants.types.BINARY: | |
| 646 | return '.bin' | |
| 647 | case constants.types.TEXT: | |
| 648 | return '.txt' | |
| 649 | default: | |
| 650 | return null | |
| 651 | } | |
| 652 | } | |
| 653 | ||
| 654 | function nameOfType(type) { | |
| 655 | switch (type) { | |
| 656 | case constants.types.SCRIPT: | |
| 657 | return 'script' | |
| 658 | case constants.types.MODULE: | |
| 659 | return 'module' | |
| 660 | case constants.types.JSON: | |
| 661 | return 'json' | |
| 662 | case constants.types.BUNDLE: | |
| 663 | return 'bundle' | |
| 664 | case constants.types.ADDON: | |
| 665 | return 'bare' | |
| 666 | case constants.types.BINARY: | |
| 667 | return 'binary' | |
| 668 | case constants.types.TEXT: | |
| 669 | return 'text' | |
| 670 | default: | |
| 671 | return null | |
| 672 | } | |
| 673 | } | |
| 674 | ||
| 675 | function typeForAttributes(attributes) { | |
| 676 | if (typeof attributes !== 'object' || attributes === null) return 0 | |
| 677 | ||
| 678 | switch (attributes.type) { | |
| 679 | case 'script': | |
| 680 | return constants.types.SCRIPT | |
| 681 | case 'module': | |
| 682 | return constants.types.MODULE | |
| 683 | case 'json': | |
| 684 | return constants.types.JSON | |
| 685 | case 'bundle': | |
| 686 | return constants.types.BUNDLE | |
| 687 | case 'addon': | |
| 688 | return constants.types.ADDON | |
| 689 | case 'binary': | |
| 690 | return constants.types.BINARY | |
| 691 | case 'text': | |
| 692 | return constants.types.TEXT | |
| 693 | default: | |
| 694 | return 0 | |
| 695 | } | |
| 696 | } | |
| 697 | ||
| 698 | function mixinImports(target, imports, url) { | |
| 699 | if (typeof imports === 'object' && imports !== null && 'imports' in imports) { | |
| 700 | imports = imports.imports | |
| 701 | } | |
| 702 | ||
| 703 | if (typeof imports !== 'object' || imports === null) { | |
| 704 | throw errors.INVALID_IMPORTS_MAP(`Imports map at '${url.href}' is not valid`) | |
| 705 | } | |
| 706 | ||
| 707 | return { ...target, ...imports } | |
| 708 | } | |
| 709 | ||
| 710 | exports.Protocol = Protocol | |
| 711 | ||
| 712 | exports.constants = constants | |
| 713 | ||
| 714 | // For Node.js compatibility | |
| 715 | exports.builtinModules = [] | |
| 716 | ||
| 717 | // For Node.js compatibility | |
| 718 | exports.isBuiltin = function isBuiltin() { | |
| 719 | return false | |
| 720 | } | |
| 721 | ||
| 722 | exports.createRequire = function createRequire(parentURL, opts = {}) { | |
| 723 | const self = Module | |
| 724 | ||
| 725 | let { | |
| 726 | module = null, | |
| 727 | referrer = null, | |
| 728 | type = constants.types.SCRIPT, | |
| 729 | defaultType = referrer ? referrer._defaultType : constants.types.SCRIPT, | |
| 730 | cache = referrer ? referrer._cache : self._cache, | |
| 731 | main = referrer ? referrer._main : null, | |
| 732 | protocol = referrer ? referrer._protocol : self._protocol, | |
| 733 | imports = referrer ? referrer._imports : null, | |
| 734 | resolutions = referrer ? referrer._resolutions : null, | |
| 735 | builtins = referrer ? referrer._builtins : null, | |
| 736 | conditions = referrer ? referrer._conditions : self._conditions | |
| 737 | } = opts | |
| 738 | ||
| 739 | if (module === null) { | |
| 740 | module = new Module(toURL(parentURL)) | |
| 741 | ||
| 742 | module._type = type | |
| 743 | module._defaultType = defaultType | |
| 744 | module._cache = cache | |
| 745 | module._main = main || module | |
| 746 | module._protocol = protocol | |
| 747 | module._imports = imports | |
| 748 | module._resolutions = resolutions | |
| 749 | module._builtins = builtins | |
| 750 | module._conditions = conditions | |
| 751 | } | |
| 752 | ||
| 753 | referrer = module | |
| 754 | ||
| 755 | function require(specifier, opts = {}) { | |
| 756 | const attributes = opts && opts.with | |
| 757 | ||
| 758 | const resolved = self.resolve(specifier, referrer._url, { | |
| 759 | referrer, | |
| 760 | attributes | |
| 761 | }) | |
| 762 | ||
| 763 | const module = self.load(resolved, { referrer, attributes }) | |
| 764 | ||
| 765 | return module._exports | |
| 766 | } | |
| 767 | ||
| 768 | require.main = module._main | |
| 769 | require.cache = module._cache | |
| 770 | ||
| 771 | require.resolve = function resolve(specifier, parentURL = referrer._url) { | |
| 772 | return urlToPath(self.resolve(specifier, toURL(parentURL, referrer._url), { referrer })) | |
| 773 | } | |
| 774 | ||
| 775 | require.addon = function addon(specifier = '.', parentURL = referrer._url) { | |
| 776 | const resolved = Bare.Addon.resolve(specifier, toURL(parentURL, referrer._url), { referrer }) | |
| 777 | ||
| 778 | const addon = Bare.Addon.load(resolved, { referrer }) | |
| 779 | ||
| 780 | return addon._exports | |
| 781 | } | |
| 782 | ||
| 783 | require.addon.host = Bare.Addon.host | |
| 784 | ||
| 785 | require.addon.resolve = function resolve(specifier = '.', parentURL = referrer._url) { | |
| 786 | return urlToPath( | |
| 787 | Bare.Addon.resolve(specifier, toURL(parentURL, referrer._url), { | |
| 788 | referrer | |
| 789 | }) | |
| 790 | ) | |
| 791 | } | |
| 792 | ||
| 793 | require.asset = function asset(specifier, parentURL = referrer._url) { | |
| 794 | return urlToPath(self.asset(specifier, toURL(parentURL, referrer._url), { referrer })) | |
| 795 | } | |
| 796 | ||
| 797 | return require | |
| 798 | } | |
| 799 | ||
| 800 | Module._extensions['.js'] = function (module, source, referrer) { | |
| 801 | const self = Module | |
| 802 | ||
| 803 | const protocol = module._protocol | |
| 804 | const resolutions = module._resolutions | |
| 805 | ||
| 806 | let pkg | |
| 807 | ||
| 808 | for (const packageURL of resolve.lookupPackageScope(module._url, { | |
| 809 | resolutions | |
| 810 | })) { | |
| 811 | if (self._cache[packageURL.href]) { | |
| 812 | pkg = self._cache[packageURL.href] | |
| 813 | break | |
| 814 | } | |
| 815 | ||
| 816 | if (protocol.exists(packageURL, constants.types.JSON)) { | |
| 817 | pkg = self.load(packageURL, { protocol }) | |
| 818 | break | |
| 819 | } | |
| 820 | } | |
| 821 | ||
| 822 | const info = (pkg && pkg._exports) || {} | |
| 823 | ||
| 824 | const isESM = | |
| 825 | // The default type is ES modules. | |
| 826 | constants.types.MODULE === module._defaultType || | |
| 827 | // The package is explicitly declared as an ES module. | |
| 828 | (info && info.type === 'module') | |
| 829 | ||
| 830 | return self._extensions[isESM ? '.mjs' : '.cjs'](module, source, referrer) | |
| 831 | } | |
| 832 | ||
| 833 | Module._extensions['.cjs'] = function (module, source, referrer) { | |
| 834 | const protocol = module._protocol | |
| 835 | ||
| 836 | module._type = constants.types.SCRIPT | |
| 837 | ||
| 838 | if (source === null) source = protocol.read(module._url) | |
| 839 | ||
| 840 | if (typeof source === 'string') source = Buffer.from(source) | |
| 841 | ||
| 842 | module._source = source | |
| 843 | ||
| 844 | module._function = binding.createFunction( | |
| 845 | module._url.href, | |
| 846 | ['require', 'module', 'exports', '__filename', '__dirname'], | |
| 847 | source.toString(), | |
| 848 | 0 | |
| 849 | ) | |
| 850 | } | |
| 851 | ||
| 852 | Module._extensions['.mjs'] = function (module, source, referrer) { | |
| 853 | const self = Module | |
| 854 | ||
| 855 | const protocol = module._protocol | |
| 856 | ||
| 857 | module._type = constants.types.MODULE | |
| 858 | ||
| 859 | if (source === null) source = protocol.read(module._url) | |
| 860 | ||
| 861 | if (typeof source === 'string') source = Buffer.from(source) | |
| 862 | ||
| 863 | module._source = source | |
| 864 | ||
| 865 | module._handle = binding.createModule(module._url.href, source.toString(), 0, self._handle) | |
| 866 | } | |
| 867 | ||
| 868 | Module._extensions['.json'] = function (module, source, referrer) { | |
| 869 | const protocol = module._protocol | |
| 870 | ||
| 871 | module._type = constants.types.JSON | |
| 872 | ||
| 873 | if (source === null) source = protocol.read(module._url) | |
| 874 | ||
| 875 | if (typeof source === 'string') source = Buffer.from(source) | |
| 876 | ||
| 877 | module._source = source | |
| 878 | module._exports = JSON.parse(source.toString()) | |
| 879 | } | |
| 880 | ||
| 881 | Module._extensions['.bare'] = function (module, source, referrer) { | |
| 882 | module._type = constants.types.ADDON | |
| 883 | ||
| 884 | referrer = module | |
| 885 | ||
| 886 | module._exports = Bare.Addon.load(module._url, { referrer }).exports | |
| 887 | } | |
| 888 | ||
| 889 | Module._extensions['.node'] = function (module, source, referrer) { | |
| 890 | module._type = constants.types.ADDON | |
| 891 | ||
| 892 | referrer = module | |
| 893 | ||
| 894 | module._exports = Bare.Addon.load(module._url, { referrer }).exports | |
| 895 | } | |
| 896 | ||
| 897 | Module._extensions['.bundle'] = function (module, source, referrer) { | |
| 898 | const self = Module | |
| 899 | ||
| 900 | const protocol = module._protocol | |
| 901 | ||
| 902 | module._type = constants.types.BUNDLE | |
| 903 | ||
| 904 | if (source === null) source = protocol.read(module._url) | |
| 905 | ||
| 906 | if (typeof source === 'string') source = Buffer.from(source) | |
| 907 | ||
| 908 | referrer = module | |
| 909 | ||
| 910 | const bundle = Bundle.from(source).mount(module._url.href + '/') | |
| 911 | ||
| 912 | module._source = source | |
| 913 | module._imports = bundle.imports | |
| 914 | module._resolutions = bundle.resolutions | |
| 915 | ||
| 916 | module._protocol = protocol.extend({ | |
| 917 | postresolve(context, url) { | |
| 918 | return bundle.exists(url.href) ? url : context.postresolve(url) | |
| 919 | }, | |
| 920 | ||
| 921 | exists(context, url, type) { | |
| 922 | return bundle.exists(url.href) || context.exists(url, type) | |
| 923 | }, | |
| 924 | ||
| 925 | read(context, url) { | |
| 926 | return bundle.read(url.href) || context.read(url) | |
| 927 | } | |
| 928 | }) | |
| 929 | ||
| 930 | if (bundle.main) { | |
| 931 | module._exports = self.load(new URL(bundle.main), bundle.read(bundle.main), { | |
| 932 | referrer | |
| 933 | })._exports | |
| 934 | } | |
| 935 | } | |
| 936 | ||
| 937 | Module._extensions['.bin'] = function (module, source, referrer) { | |
| 938 | const protocol = module._protocol | |
| 939 | ||
| 940 | module._type = constants.types.BINARY | |
| 941 | ||
| 942 | if (source === null) source = protocol.read(module._url) | |
| 943 | ||
| 944 | if (typeof source === 'string') source = Buffer.from(source) | |
| 945 | ||
| 946 | module._source = module._exports = source | |
| 947 | } | |
| 948 | ||
| 949 | Module._extensions['.txt'] = function (module, source, referrer) { | |
| 950 | const protocol = module._protocol | |
| 951 | ||
| 952 | module._type = constants.types.TEXT | |
| 953 | ||
| 954 | if (source === null) source = protocol.read(module._url) | |
| 955 | ||
| 956 | if (typeof source === 'string') source = Buffer.from(source) | |
| 957 | ||
| 958 | module._source = source | |
| 959 | module._exports = source.toString() | |
| 960 | } | |
| 961 | ||
| 962 | Module._protocol = new Protocol({ | |
| 963 | postresolve(url) { | |
| 964 | switch (url.protocol) { | |
| 965 | case 'file:': | |
| 966 | return pathToFileURL(binding.realpath(path.toNamespacedPath(fileURLToPath(url)))) | |
| 967 | default: | |
| 968 | return url | |
| 969 | } | |
| 970 | }, | |
| 971 | ||
| 972 | exists(url, type = 0) { | |
| 973 | switch (url.protocol) { | |
| 974 | case 'file:': | |
| 975 | return binding.exists( | |
| 976 | path.toNamespacedPath(fileURLToPath(url)), | |
| 977 | type === constants.types.ASSET ? binding.FILE | binding.DIR : binding.FILE | |
| 978 | ) | |
| 979 | default: | |
| 980 | return false | |
| 981 | } | |
| 982 | }, | |
| 983 | ||
| 984 | read(url) { | |
| 985 | switch (url.protocol) { | |
| 986 | case 'file:': | |
| 987 | return Buffer.from(binding.read(path.toNamespacedPath(fileURLToPath(url)))) | |
| 988 | default: | |
| 989 | throw errors.UNKNOWN_PROTOCOL(`Cannot load module '${url.href}'`) | |
| 990 | } | |
| 991 | } | |
| 992 | }) | |
| 993 | ||
| 994 | function toURL(value, base) { | |
| 995 | if (isURL(value)) return value | |
| 996 | ||
| 997 | if (startsWithWindowsDriveLetter(value)) { | |
| 998 | return pathToFileURL(value) | |
| 999 | } | |
| 1000 | ||
| 1001 | return URL.parse(value, base) || pathToFileURL(value) | |
| 1002 | } | |
| 1003 | ||
| 1004 | function urlToPath(url) { | |
| 1005 | if (url.protocol === 'file:') return fileURLToPath(url) | |
| 1006 | ||
| 1007 | if (isWindows) { | |
| 1008 | if (/%2f|%5c/i.test(url.pathname)) { | |
| 1009 | throw errors.INVALID_URL_PATH('The URL path must not include encoded \\ or / characters') | |
| 1010 | } | |
| 1011 | } else { | |
| 1012 | if (/%2f/i.test(url.pathname)) { | |
| 1013 | throw errors.INVALID_URL_PATH('The URL path must not include encoded / characters') | |
| 1014 | } | |
| 1015 | } | |
| 1016 | ||
| 1017 | return decodeURIComponent(url.pathname) | |
| 1018 | } | |
| 1019 | ||
| 1020 | function urlToDirname(url) { | |
| 1021 | if (url.protocol === 'file:') return path.dirname(fileURLToPath(url)) | |
| 1022 | ||
| 1023 | if (isWindows) { | |
| 1024 | if (/%2f|%5c/i.test(url.pathname)) { | |
| 1025 | throw errors.INVALID_URL_PATH('The URL path must not include encoded \\ or / characters') | |
| 1026 | } | |
| 1027 | } else { | |
| 1028 | if (/%2f/i.test(url.pathname)) { | |
| 1029 | throw errors.INVALID_URL_PATH('The URL path must not include encoded / characters') | |
| 1030 | } | |
| 1031 | } | |
| 1032 | ||
| 1033 | return decodeURIComponent(new URL('.', url).pathname).replace(/\/$/, '') | |
| 1034 | } |
| … | … | @@ -0,0 +1,19 @@ |
| 1 | declare const constants: { | |
| 2 | states: { | |
| 3 | EVALUATED: number | |
| 4 | SYNTHESIZED: number | |
| 5 | RUN: number | |
| 6 | } | |
| 7 | types: { | |
| 8 | SCRIPT: number | |
| 9 | MODULE: number | |
| 10 | JSON: number | |
| 11 | BUNDLE: number | |
| 12 | ADDON: number | |
| 13 | BINARY: number | |
| 14 | TEXT: number | |
| 15 | ASSET: number | |
| 16 | } | |
| 17 | } | |
| 18 | ||
| 19 | export = constants |
| … | … | @@ -0,0 +1,17 @@ |
| 1 | module.exports = { | |
| 2 | states: { | |
| 3 | EVALUATED: 1, | |
| 4 | SYNTHESIZED: 2, | |
| 5 | RUN: 4 | |
| 6 | }, | |
| 7 | types: { | |
| 8 | SCRIPT: 1, | |
| 9 | MODULE: 2, | |
| 10 | JSON: 3, | |
| 11 | BUNDLE: 4, | |
| 12 | ADDON: 5, | |
| 13 | BINARY: 6, | |
| 14 | TEXT: 7, | |
| 15 | ASSET: 8 | |
| 16 | } | |
| 17 | } |
| … | … | @@ -0,0 +1,54 @@ |
| 1 | module.exports = class ModuleError extends Error { | |
| 2 | constructor(msg, fn = ModuleError, code = fn.name) { | |
| 3 | super(`${code}: ${msg}`) | |
| 4 | this.code = code | |
| 5 | ||
| 6 | if (Error.captureStackTrace) { | |
| 7 | Error.captureStackTrace(this, fn) | |
| 8 | } | |
| 9 | } | |
| 10 | ||
| 11 | get name() { | |
| 12 | return 'ModuleError' | |
| 13 | } | |
| 14 | ||
| 15 | static MODULE_NOT_FOUND(msg, specifier, referrer = null, candidates = []) { | |
| 16 | const err = new ModuleError(msg, ModuleError.MODULE_NOT_FOUND) | |
| 17 | ||
| 18 | err.specifier = specifier | |
| 19 | err.referrer = referrer | |
| 20 | err.candidates = candidates | |
| 21 | ||
| 22 | return err | |
| 23 | } | |
| 24 | ||
| 25 | static ASSET_NOT_FOUND(msg, specifier, referrer = null, candidates = []) { | |
| 26 | const err = new ModuleError(msg, ModuleError.ASSET_NOT_FOUND) | |
| 27 | ||
| 28 | err.specifier = specifier | |
| 29 | err.referrer = referrer | |
| 30 | err.candidates = candidates | |
| 31 | ||
| 32 | return err | |
| 33 | } | |
| 34 | ||
| 35 | static UNKNOWN_PROTOCOL(msg) { | |
| 36 | return new ModuleError(msg, ModuleError.UNKNOWN_PROTOCOL) | |
| 37 | } | |
| 38 | ||
| 39 | static INVALID_BUNDLE_EXTENSION(msg) { | |
| 40 | return new ModuleError(msg, ModuleError.INVALID_BUNDLE_EXTENSION) | |
| 41 | } | |
| 42 | ||
| 43 | static INVALID_URL_PATH(msg) { | |
| 44 | return new ModuleError(msg, ModuleError.INVALID_URL_PATH) | |
| 45 | } | |
| 46 | ||
| 47 | static INVALID_IMPORTS_MAP(msg) { | |
| 48 | return new ModuleError(msg, ModuleError.INVALID_IMPORTS_MAP) | |
| 49 | } | |
| 50 | ||
| 51 | static TYPE_INCOMPATIBLE(msg) { | |
| 52 | return new ModuleError(msg, ModuleError.TYPE_INCOMPATIBLE) | |
| 53 | } | |
| 54 | } |
| … | … | @@ -0,0 +1,27 @@ |
| 1 | import URL from 'bare-url' | |
| 2 | import Buffer from 'bare-buffer' | |
| 3 | import { type ImportsMap } from 'bare-module-resolve' | |
| 4 | ||
| 5 | interface ModuleProtocol { | |
| 6 | preresolve(specifier: string, parentURL: URL): string | |
| 7 | ||
| 8 | postresolve(url: URL): URL | |
| 9 | ||
| 10 | resolve(specifier: string, parentURL: URL, imports: ImportsMap): URL | |
| 11 | ||
| 12 | exists(url: URL, type: number): boolean | |
| 13 | ||
| 14 | read(url: URL): Buffer | string | null | |
| 15 | ||
| 16 | addon(url: URL): URL | |
| 17 | ||
| 18 | asset(url: URL): URL | |
| 19 | ||
| 20 | extend(methods: Partial<ModuleProtocol>): ModuleProtocol | |
| 21 | } | |
| 22 | ||
| 23 | declare class ModuleProtocol { | |
| 24 | constructor(methods?: Partial<ModuleProtocol>, context?: ModuleProtocol) | |
| 25 | } | |
| 26 | ||
| 27 | export = ModuleProtocol |
| … | … | @@ -0,0 +1,55 @@ |
| 1 | module.exports = class ModuleProtocol { | |
| 2 | constructor(methods = {}, context = null) { | |
| 3 | for (const name of [ | |
| 4 | 'preresolve', | |
| 5 | 'postresolve', | |
| 6 | 'resolve', | |
| 7 | 'exists', | |
| 8 | 'read', | |
| 9 | 'addon', | |
| 10 | 'asset' | |
| 11 | ]) { | |
| 12 | const method = methods[name] | |
| 13 | ||
| 14 | if (typeof method === 'function') { | |
| 15 | this[name] = context ? method.bind(this, context) : method.bind(this) | |
| 16 | } else if (context) { | |
| 17 | const method = context[name] | |
| 18 | ||
| 19 | if (typeof method === 'function') { | |
| 20 | this[name] = method | |
| 21 | } | |
| 22 | } | |
| 23 | } | |
| 24 | } | |
| 25 | ||
| 26 | preresolve(specifier, parentURL) { | |
| 27 | return specifier | |
| 28 | } | |
| 29 | ||
| 30 | postresolve(url) { | |
| 31 | return url | |
| 32 | } | |
| 33 | ||
| 34 | *resolve(specifier, parentURL, imports) {} | |
| 35 | ||
| 36 | exists(url, type) { | |
| 37 | return false | |
| 38 | } | |
| 39 | ||
| 40 | read(url) { | |
| 41 | return null | |
| 42 | } | |
| 43 | ||
| 44 | addon(url) { | |
| 45 | return url | |
| 46 | } | |
| 47 | ||
| 48 | asset(url) { | |
| 49 | return url | |
| 50 | } | |
| 51 | ||
| 52 | extend(methods) { | |
| 53 | return new ModuleProtocol(methods, this) | |
| 54 | } | |
| 55 | } |
| … | … | @@ -0,0 +1,60 @@ |
| 1 | { | |
| 2 | "name": "bare-module", | |
| 3 | "version": "6.1.3", | |
| 4 | "description": "Module support for JavaScript", | |
| 5 | "exports": { | |
| 6 | ".": { | |
| 7 | "types": "./index.d.ts", | |
| 8 | "default": "./index.js" | |
| 9 | }, | |
| 10 | "./package": "./package.json" | |
| 11 | }, | |
| 12 | "files": [ | |
| 13 | "index.js", | |
| 14 | "index.d.ts", | |
| 15 | "binding.c", | |
| 16 | "binding.js", | |
| 17 | "CMakeLists.txt", | |
| 18 | "lib", | |
| 19 | "prebuilds" | |
| 20 | ], | |
| 21 | "addon": true, | |
| 22 | "scripts": { | |
| 23 | "test": "prettier . --check && bare test.js" | |
| 24 | }, | |
| 25 | "repository": { | |
| 26 | "type": "git", | |
| 27 | "url": "git+https://github.com/holepunchto/bare-module.git" | |
| 28 | }, | |
| 29 | "author": "Holepunch", | |
| 30 | "license": "Apache-2.0", | |
| 31 | "bugs": { | |
| 32 | "url": "https://github.com/holepunchto/bare-module/issues" | |
| 33 | }, | |
| 34 | "homepage": "https://github.com/holepunchto/bare-module#readme", | |
| 35 | "engines": { | |
| 36 | "bare": ">=1.23.0" | |
| 37 | }, | |
| 38 | "dependencies": { | |
| 39 | "bare-bundle": "^1.3.0", | |
| 40 | "bare-module-lexer": "^1.0.0", | |
| 41 | "bare-module-resolve": "^1.8.0", | |
| 42 | "bare-path": "^3.0.0", | |
| 43 | "bare-url": "^2.0.1" | |
| 44 | }, | |
| 45 | "devDependencies": { | |
| 46 | "bare-buffer": "^3.0.2", | |
| 47 | "brittle": "^3.1.1", | |
| 48 | "cmake-bare": "^1.1.6", | |
| 49 | "prettier": "^3.3.3", | |
| 50 | "prettier-config-holepunch": "^2.0.0" | |
| 51 | }, | |
| 52 | "peerDependencies": { | |
| 53 | "bare-buffer": "*" | |
| 54 | }, | |
| 55 | "peerDependenciesMeta": { | |
| 56 | "bare-buffer": { | |
| 57 | "optional": true | |
| 58 | } | |
| 59 | } | |
| 60 | } |
| … | … | @@ -0,0 +1,201 @@ |
| 1 | Apache License | |
| 2 | Version 2.0, January 2004 | |
| 3 | http://www.apache.org/licenses/ | |
| 4 | ||
| 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | |
| 6 | ||
| 7 | 1. Definitions. | |
| 8 | ||
| 9 | "License" shall mean the terms and conditions for use, reproduction, | |
| 10 | and distribution as defined by Sections 1 through 9 of this document. | |
| 11 | ||
| 12 | "Licensor" shall mean the copyright owner or entity authorized by | |
| 13 | the copyright owner that is granting the License. | |
| 14 | ||
| 15 | "Legal Entity" shall mean the union of the acting entity and all | |
| 16 | other entities that control, are controlled by, or are under common | |
| 17 | control with that entity. For the purposes of this definition, | |
| 18 | "control" means (i) the power, direct or indirect, to cause the | |
| 19 | direction or management of such entity, whether by contract or | |
| 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the | |
| 21 | outstanding shares, or (iii) beneficial ownership of such entity. | |
| 22 | ||
| 23 | "You" (or "Your") shall mean an individual or Legal Entity | |
| 24 | exercising permissions granted by this License. | |
| 25 | ||
| 26 | "Source" form shall mean the preferred form for making modifications, | |
| 27 | including but not limited to software source code, documentation | |
| 28 | source, and configuration files. | |
| 29 | ||
| 30 | "Object" form shall mean any form resulting from mechanical | |
| 31 | transformation or translation of a Source form, including but | |
| 32 | not limited to compiled object code, generated documentation, | |
| 33 | and conversions to other media types. | |
| 34 | ||
| 35 | "Work" shall mean the work of authorship, whether in Source or | |
| 36 | Object form, made available under the License, as indicated by a | |
| 37 | copyright notice that is included in or attached to the work | |
| 38 | (an example is provided in the Appendix below). | |
| 39 | ||
| 40 | "Derivative Works" shall mean any work, whether in Source or Object | |
| 41 | form, that is based on (or derived from) the Work and for which the | |
| 42 | editorial revisions, annotations, elaborations, or other modifications | |
| 43 | represent, as a whole, an original work of authorship. For the purposes | |
| 44 | of this License, Derivative Works shall not include works that remain | |
| 45 | separable from, or merely link (or bind by name) to the interfaces of, | |
| 46 | the Work and Derivative Works thereof. | |
| 47 | ||
| 48 | "Contribution" shall mean any work of authorship, including | |
| 49 | the original version of the Work and any modifications or additions | |
| 50 | to that Work or Derivative Works thereof, that is intentionally | |
| 51 | submitted to Licensor for inclusion in the Work by the copyright owner | |
| 52 | or by an individual or Legal Entity authorized to submit on behalf of | |
| 53 | the copyright owner. For the purposes of this definition, "submitted" | |
| 54 | means any form of electronic, verbal, or written communication sent | |
| 55 | to the Licensor or its representatives, including but not limited to | |
| 56 | communication on electronic mailing lists, source code control systems, | |
| 57 | and issue tracking systems that are managed by, or on behalf of, the | |
| 58 | Licensor for the purpose of discussing and improving the Work, but | |
| 59 | excluding communication that is conspicuously marked or otherwise | |
| 60 | designated in writing by the copyright owner as "Not a Contribution." | |
| 61 | ||
| 62 | "Contributor" shall mean Licensor and any individual or Legal Entity | |
| 63 | on behalf of whom a Contribution has been received by Licensor and | |
| 64 | subsequently incorporated within the Work. | |
| 65 | ||
| 66 | 2. Grant of Copyright License. Subject to the terms and conditions of | |
| 67 | this License, each Contributor hereby grants to You a perpetual, | |
| 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 69 | copyright license to reproduce, prepare Derivative Works of, | |
| 70 | publicly display, publicly perform, sublicense, and distribute the | |
| 71 | Work and such Derivative Works in Source or Object form. | |
| 72 | ||
| 73 | 3. Grant of Patent License. Subject to the terms and conditions of | |
| 74 | this License, each Contributor hereby grants to You a perpetual, | |
| 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 76 | (except as stated in this section) patent license to make, have made, | |
| 77 | use, offer to sell, sell, import, and otherwise transfer the Work, | |
| 78 | where such license applies only to those patent claims licensable | |
| 79 | by such Contributor that are necessarily infringed by their | |
| 80 | Contribution(s) alone or by combination of their Contribution(s) | |
| 81 | with the Work to which such Contribution(s) was submitted. If You | |
| 82 | institute patent litigation against any entity (including a | |
| 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work | |
| 84 | or a Contribution incorporated within the Work constitutes direct | |
| 85 | or contributory patent infringement, then any patent licenses | |
| 86 | granted to You under this License for that Work shall terminate | |
| 87 | as of the date such litigation is filed. | |
| 88 | ||
| 89 | 4. Redistribution. You may reproduce and distribute copies of the | |
| 90 | Work or Derivative Works thereof in any medium, with or without | |
| 91 | modifications, and in Source or Object form, provided that You | |
| 92 | meet the following conditions: | |
| 93 | ||
| 94 | (a) You must give any other recipients of the Work or | |
| 95 | Derivative Works a copy of this License; and | |
| 96 | ||
| 97 | (b) You must cause any modified files to carry prominent notices | |
| 98 | stating that You changed the files; and | |
| 99 | ||
| 100 | (c) You must retain, in the Source form of any Derivative Works | |
| 101 | that You distribute, all copyright, patent, trademark, and | |
| 102 | attribution notices from the Source form of the Work, | |
| 103 | excluding those notices that do not pertain to any part of | |
| 104 | the Derivative Works; and | |
| 105 | ||
| 106 | (d) If the Work includes a "NOTICE" text file as part of its | |
| 107 | distribution, then any Derivative Works that You distribute must | |
| 108 | include a readable copy of the attribution notices contained | |
| 109 | within such NOTICE file, excluding those notices that do not | |
| 110 | pertain to any part of the Derivative Works, in at least one | |
| 111 | of the following places: within a NOTICE text file distributed | |
| 112 | as part of the Derivative Works; within the Source form or | |
| 113 | documentation, if provided along with the Derivative Works; or, | |
| 114 | within a display generated by the Derivative Works, if and | |
| 115 | wherever such third-party notices normally appear. The contents | |
| 116 | of the NOTICE file are for informational purposes only and | |
| 117 | do not modify the License. You may add Your own attribution | |
| 118 | notices within Derivative Works that You distribute, alongside | |
| 119 | or as an addendum to the NOTICE text from the Work, provided | |
| 120 | that such additional attribution notices cannot be construed | |
| 121 | as modifying the License. | |
| 122 | ||
| 123 | You may add Your own copyright statement to Your modifications and | |
| 124 | may provide additional or different license terms and conditions | |
| 125 | for use, reproduction, or distribution of Your modifications, or | |
| 126 | for any such Derivative Works as a whole, provided Your use, | |
| 127 | reproduction, and distribution of the Work otherwise complies with | |
| 128 | the conditions stated in this License. | |
| 129 | ||
| 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, | |
| 131 | any Contribution intentionally submitted for inclusion in the Work | |
| 132 | by You to the Licensor shall be under the terms and conditions of | |
| 133 | this License, without any additional terms or conditions. | |
| 134 | Notwithstanding the above, nothing herein shall supersede or modify | |
| 135 | the terms of any separate license agreement you may have executed | |
| 136 | with Licensor regarding such Contributions. | |
| 137 | ||
| 138 | 6. Trademarks. This License does not grant permission to use the trade | |
| 139 | names, trademarks, service marks, or product names of the Licensor, | |
| 140 | except as required for reasonable and customary use in describing the | |
| 141 | origin of the Work and reproducing the content of the NOTICE file. | |
| 142 | ||
| 143 | 7. Disclaimer of Warranty. Unless required by applicable law or | |
| 144 | agreed to in writing, Licensor provides the Work (and each | |
| 145 | Contributor provides its Contributions) on an "AS IS" BASIS, | |
| 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |
| 147 | implied, including, without limitation, any warranties or conditions | |
| 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | |
| 149 | PARTICULAR PURPOSE. You are solely responsible for determining the | |
| 150 | appropriateness of using or redistributing the Work and assume any | |
| 151 | risks associated with Your exercise of permissions under this License. | |
| 152 | ||
| 153 | 8. Limitation of Liability. In no event and under no legal theory, | |
| 154 | whether in tort (including negligence), contract, or otherwise, | |
| 155 | unless required by applicable law (such as deliberate and grossly | |
| 156 | negligent acts) or agreed to in writing, shall any Contributor be | |
| 157 | liable to You for damages, including any direct, indirect, special, | |
| 158 | incidental, or consequential damages of any character arising as a | |
| 159 | result of this License or out of the use or inability to use the | |
| 160 | Work (including but not limited to damages for loss of goodwill, | |
| 161 | work stoppage, computer failure or malfunction, or any and all | |
| 162 | other commercial damages or losses), even if such Contributor | |
| 163 | has been advised of the possibility of such damages. | |
| 164 | ||
| 165 | 9. Accepting Warranty or Additional Liability. While redistributing | |
| 166 | the Work or Derivative Works thereof, You may choose to offer, | |
| 167 | and charge a fee for, acceptance of support, warranty, indemnity, | |
| 168 | or other liability obligations and/or rights consistent with this | |
| 169 | License. However, in accepting such obligations, You may act only | |
| 170 | on Your own behalf and on Your sole responsibility, not on behalf | |
| 171 | of any other Contributor, and only if You agree to indemnify, | |
| 172 | defend, and hold each Contributor harmless for any liability | |
| 173 | incurred by, or claims asserted against, such Contributor by reason | |
| 174 | of your accepting any such warranty or additional liability. | |
| 175 | ||
| 176 | END OF TERMS AND CONDITIONS | |
| 177 | ||
| 178 | APPENDIX: How to apply the Apache License to your work. | |
| 179 | ||
| 180 | To apply the Apache License to your work, attach the following | |
| 181 | boilerplate notice, with the fields enclosed by brackets "[]" | |
| 182 | replaced with your own identifying information. (Don't include | |
| 183 | the brackets!) The text should be enclosed in the appropriate | |
| 184 | comment syntax for the file format. We also recommend that a | |
| 185 | file or class name and description of purpose be included on the | |
| 186 | same "printed page" as the copyright notice for easier | |
| 187 | identification within third-party archives. | |
| 188 | ||
| 189 | Copyright [yyyy] [name of copyright owner] | |
| 190 | ||
| 191 | Licensed under the Apache License, Version 2.0 (the "License"); | |
| 192 | you may not use this file except in compliance with the License. | |
| 193 | You may obtain a copy of the License at | |
| 194 | ||
| 195 | http://www.apache.org/licenses/LICENSE-2.0 | |
| 196 | ||
| 197 | Unless required by applicable law or agreed to in writing, software | |
| 198 | distributed under the License is distributed on an "AS IS" BASIS, | |
| 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 200 | See the License for the specific language governing permissions and | |
| 201 | limitations under the License. |
| … | … | @@ -0,0 +1,25 @@ |
| 1 | # bare-net | |
| 2 | ||
| 3 | TCP and IPC servers and clients for JavaScript. | |
| 4 | ||
| 5 | ``` | |
| 6 | npm i bare-net | |
| 7 | ``` | |
| 8 | ||
| 9 | ## Usage | |
| 10 | ||
| 11 | ```js | |
| 12 | const net = require('bare-net') | |
| 13 | ||
| 14 | const server = net.createServer() | |
| 15 | server.on('connection', (socket) => socket.on('data', console.log)) | |
| 16 | server.listen(() => console.log('server is up')) | |
| 17 | ||
| 18 | const { port } = server.address() | |
| 19 | const socket = net.createConnection(port) | |
| 20 | socket.write('hello world') | |
| 21 | ``` | |
| 22 | ||
| 23 | ## License | |
| 24 | ||
| 25 | Apache-2.0 |
| … | … | @@ -0,0 +1,98 @@ |
| 1 | import EventEmitter, { EventMap } from 'bare-events' | |
| 2 | import { Duplex, DuplexEvents } from 'bare-stream' | |
| 3 | import { | |
| 4 | Pipe, | |
| 5 | PipeServer, | |
| 6 | createConnection as createPipeConnection, | |
| 7 | createServer as createPipeServer | |
| 8 | } from 'bare-pipe' | |
| 9 | import { | |
| 10 | TCPSocket, | |
| 11 | TCPServer, | |
| 12 | createConnection as createTCPConnection, | |
| 13 | createServer as createTCPServer, | |
| 14 | isIP, | |
| 15 | isIPv4, | |
| 16 | isIPv6 | |
| 17 | } from 'bare-tcp' | |
| 18 | import constants from './lib/constants' | |
| 19 | ||
| 20 | export { constants, isIP, isIPv4, isIPv6 } | |
| 21 | ||
| 22 | export interface NetOptions { | |
| 23 | allowHalfOpen?: boolean | |
| 24 | eagerOpen?: boolean | |
| 25 | readBufferSize?: number | |
| 26 | } | |
| 27 | ||
| 28 | export interface NetSocketEvents extends DuplexEvents { | |
| 29 | connect: [] | |
| 30 | } | |
| 31 | ||
| 32 | interface NetSocket<M extends NetSocketEvents = NetSocketEvents> extends Duplex<M> { | |
| 33 | readonly connecting: boolean | |
| 34 | readonly pending: boolean | |
| 35 | readonly timeout?: number | |
| 36 | readonly readyState: 'open' | 'readOnly' | 'writeOnly' | 'opening' | |
| 37 | readonly localAddress?: string | |
| 38 | readonly localPort?: number | |
| 39 | readonly localFamily?: string | |
| 40 | readonly remoteAddress?: string | |
| 41 | readonly remotePort?: number | |
| 42 | readonly remoteFamily?: string | |
| 43 | ||
| 44 | connect: Pipe['connect'] & TCPSocket['connect'] | |
| 45 | ||
| 46 | setKeepAlive(enable?: boolean, delay?: number): this | |
| 47 | setKeepAlive(delay: number): this | |
| 48 | ||
| 49 | setNoDelay(enable?: boolean): this | |
| 50 | ||
| 51 | setTimeout(ms: number, ontimeout?: () => void): this | |
| 52 | ||
| 53 | ref(): this | |
| 54 | unref(): this | |
| 55 | } | |
| 56 | ||
| 57 | declare class NetSocket { | |
| 58 | constructor(opts?: NetOptions) | |
| 59 | } | |
| 60 | ||
| 61 | export { type NetSocket, NetSocket as Socket } | |
| 62 | ||
| 63 | export interface NetServerEvents extends EventMap { | |
| 64 | close: [] | |
| 65 | connection: [socket: NetSocket] | |
| 66 | error: [err: Error] | |
| 67 | listening: [] | |
| 68 | } | |
| 69 | ||
| 70 | interface NetServer<M extends NetServerEvents = NetServerEvents> extends EventEmitter<M> { | |
| 71 | readonly listening: boolean | |
| 72 | ||
| 73 | address(): (PipeServer['address'] & TCPServer['address']) | null | |
| 74 | ||
| 75 | listen: PipeServer['listen'] & TCPServer['listen'] | |
| 76 | ||
| 77 | close(onclose: (err?: Error) => void): this | |
| 78 | ||
| 79 | ref(): this | |
| 80 | unref(): this | |
| 81 | } | |
| 82 | ||
| 83 | declare class NetServer { | |
| 84 | constructor(opts?: NetOptions, onconnection?: (socket: NetSocket) => void) | |
| 85 | constructor(onconnection: (socket: NetSocket) => void) | |
| 86 | } | |
| 87 | ||
| 88 | export { type NetServer, NetServer as Server } | |
| 89 | ||
| 90 | export function createConnection( | |
| 91 | ...args: Parameters<typeof createPipeConnection & typeof createTCPConnection> | |
| 92 | ): NetSocket | |
| 93 | ||
| 94 | export { createConnection as connect } | |
| 95 | ||
| 96 | export function createServer( | |
| 97 | ...args: Parameters<typeof createPipeServer & typeof createTCPServer> | |
| 98 | ): NetServer |
| … | … | @@ -0,0 +1,414 @@ |
| 1 | const EventEmitter = require('bare-events') | |
| 2 | const { Duplex } = require('bare-stream') | |
| 3 | const tcp = require('bare-tcp') | |
| 4 | const pipe = require('bare-pipe') | |
| 5 | const constants = require('./lib/constants') | |
| 6 | ||
| 7 | const defaultReadBufferSize = 65536 | |
| 8 | ||
| 9 | exports.Socket = class NetSocket extends Duplex { | |
| 10 | constructor(opts = {}) { | |
| 11 | const { | |
| 12 | readBufferSize = defaultReadBufferSize, | |
| 13 | allowHalfOpen = false, | |
| 14 | eagerOpen = false | |
| 15 | } = opts | |
| 16 | ||
| 17 | super({ eagerOpen, allowHalfOpen }) | |
| 18 | ||
| 19 | this._type = 0 | |
| 20 | this._state = 0 | |
| 21 | this._socket = null | |
| 22 | ||
| 23 | this._opts = { readBufferSize, allowHalfOpen, eagerOpen } | |
| 24 | ||
| 25 | this._pendingOpen = null | |
| 26 | this._pendingWrite = null | |
| 27 | this._pendingFinal = null | |
| 28 | this._pendingDestroy = null | |
| 29 | } | |
| 30 | ||
| 31 | get connecting() { | |
| 32 | return this._socket !== null && this._socket.connecting | |
| 33 | } | |
| 34 | ||
| 35 | get pending() { | |
| 36 | return this._socket === null || this._socket.pending | |
| 37 | } | |
| 38 | ||
| 39 | get timeout() { | |
| 40 | return this._socket === null ? undefined : this._socket.timeout | |
| 41 | } | |
| 42 | ||
| 43 | get readyState() { | |
| 44 | return this._socket === null ? 'opening' : this._socket.readyState | |
| 45 | } | |
| 46 | ||
| 47 | get localAddress() { | |
| 48 | return this._socket === null ? undefined : this._socket.localAddress | |
| 49 | } | |
| 50 | ||
| 51 | get localPort() { | |
| 52 | return this._socket === null ? undefined : this._socket.localPort | |
| 53 | } | |
| 54 | ||
| 55 | get localFamily() { | |
| 56 | return this._socket === null ? undefined : this._socket.localFamily | |
| 57 | } | |
| 58 | ||
| 59 | get remoteAddress() { | |
| 60 | return this._socket === null ? undefined : this._socket.remoteAddress | |
| 61 | } | |
| 62 | ||
| 63 | get remotePort() { | |
| 64 | return this._socket === null ? undefined : this._socket.remotePort | |
| 65 | } | |
| 66 | ||
| 67 | get remoteFamily() { | |
| 68 | return this._socket === null ? undefined : this._socket.remoteFamily | |
| 69 | } | |
| 70 | ||
| 71 | connect(...args) { | |
| 72 | let opts = {} | |
| 73 | let onconnect | |
| 74 | ||
| 75 | if (typeof args[0] === 'string') { | |
| 76 | // connect(path[, onconnect]) | |
| 77 | opts.path = args[0] | |
| 78 | onconnect = args[1] | |
| 79 | } else if (typeof args[0] === 'number') { | |
| 80 | // connect(port[, host][, onconnect]) | |
| 81 | opts.port = args[0] | |
| 82 | ||
| 83 | if (typeof args[1] === 'function') { | |
| 84 | onconnect = args[1] | |
| 85 | } else { | |
| 86 | opts.host = args[1] | |
| 87 | onconnect = args[2] | |
| 88 | } | |
| 89 | } else { | |
| 90 | // connect(opts[, onconnect]) | |
| 91 | opts = args[0] || {} | |
| 92 | onconnect = args[1] | |
| 93 | } | |
| 94 | ||
| 95 | opts = { ...opts, ...this._opts } | |
| 96 | ||
| 97 | if (opts.path) { | |
| 98 | this._attach(constants.type.IPC, pipe.createConnection(opts)) | |
| 99 | } else { | |
| 100 | this._attach(constants.type.TCP, tcp.createConnection(opts)) | |
| 101 | } | |
| 102 | ||
| 103 | if (onconnect) this.once('connect', onconnect) | |
| 104 | ||
| 105 | return this | |
| 106 | } | |
| 107 | ||
| 108 | setKeepAlive(...args) { | |
| 109 | if (this._socket !== null) this._socket.setKeepAlive(...args) | |
| 110 | return this | |
| 111 | } | |
| 112 | ||
| 113 | setNoDelay(...args) { | |
| 114 | if (this._socket !== null) this._socket.setNoDelay(...args) | |
| 115 | return this | |
| 116 | } | |
| 117 | ||
| 118 | setTimeout(...args) { | |
| 119 | if (this._socket !== null) this._socket.setTimeout(...args) | |
| 120 | return this | |
| 121 | } | |
| 122 | ||
| 123 | ref() { | |
| 124 | this._state &= ~constants.state.UNREFED | |
| 125 | if (this._socket !== null) this._socket.ref() | |
| 126 | return this | |
| 127 | } | |
| 128 | ||
| 129 | unref() { | |
| 130 | this._state |= constants.state.UNREFED | |
| 131 | if (this._socket !== null) this._socket.unref() | |
| 132 | return this | |
| 133 | } | |
| 134 | ||
| 135 | _attach(type, socket) { | |
| 136 | this._type = type | |
| 137 | this._socket = socket | |
| 138 | ||
| 139 | this._socket | |
| 140 | .on('connect', this._onconnect.bind(this)) | |
| 141 | .on('timeout', this._ontimeout.bind(this)) | |
| 142 | .on('error', this._onerror.bind(this)) | |
| 143 | .on('data', this._ondata.bind(this)) | |
| 144 | .on('end', this._onend.bind(this)) | |
| 145 | .on('finish', this._onfinish.bind(this)) | |
| 146 | .on('drain', this._ondrain.bind(this)) | |
| 147 | .on('close', this._onclose.bind(this)) | |
| 148 | ||
| 149 | if (this._state & constants.state.UNREFED) this._socket.unref() | |
| 150 | ||
| 151 | this._continueOpen() | |
| 152 | ||
| 153 | return this | |
| 154 | } | |
| 155 | ||
| 156 | _open(cb) { | |
| 157 | if (this._socket !== null) return cb(null) | |
| 158 | this._pendingOpen = cb | |
| 159 | } | |
| 160 | ||
| 161 | _write(data, encoding, cb) { | |
| 162 | if (this._socket.write(data)) return cb(null) | |
| 163 | this._pendingWrite = cb | |
| 164 | } | |
| 165 | ||
| 166 | _final(cb) { | |
| 167 | this._socket.end() | |
| 168 | this._pendingFinal = cb | |
| 169 | } | |
| 170 | ||
| 171 | _destroy(err, cb) { | |
| 172 | if (this._socket === null || this._socket.destroyed) return cb(null) | |
| 173 | ||
| 174 | this._socket.destroy(err) | |
| 175 | this._pendingDestroy = cb | |
| 176 | } | |
| 177 | ||
| 178 | _onconnect() { | |
| 179 | this.emit('connect') | |
| 180 | } | |
| 181 | ||
| 182 | _ontimeout() { | |
| 183 | this.emit('timeout') | |
| 184 | } | |
| 185 | ||
| 186 | _onerror(err) { | |
| 187 | this.destroy(err) | |
| 188 | } | |
| 189 | ||
| 190 | _ondata(data) { | |
| 191 | this.push(data) | |
| 192 | } | |
| 193 | ||
| 194 | _onend() { | |
| 195 | this.push(null) | |
| 196 | } | |
| 197 | ||
| 198 | _onfinish() { | |
| 199 | this._continueFinal() | |
| 200 | } | |
| 201 | ||
| 202 | _ondrain() { | |
| 203 | this._continueWrite() | |
| 204 | } | |
| 205 | ||
| 206 | _onclose() { | |
| 207 | this._continueWrite() | |
| 208 | this._continueFinal() | |
| 209 | ||
| 210 | if (this._pendingDestroy) this._continueDestroy() | |
| 211 | else this.destroy() | |
| 212 | } | |
| 213 | ||
| 214 | _continueOpen() { | |
| 215 | if (this._pendingOpen === null) return | |
| 216 | const cb = this._pendingOpen | |
| 217 | this._pendingOpen = null | |
| 218 | cb(null) | |
| 219 | } | |
| 220 | ||
| 221 | _continueWrite() { | |
| 222 | if (this._pendingWrite === null) return | |
| 223 | const cb = this._pendingWrite | |
| 224 | this._pendingWrite = null | |
| 225 | cb(null) | |
| 226 | } | |
| 227 | ||
| 228 | _continueFinal() { | |
| 229 | if (this._pendingFinal === null) return | |
| 230 | const cb = this._pendingFinal | |
| 231 | this._pendingFinal = null | |
| 232 | cb(null) | |
| 233 | } | |
| 234 | ||
| 235 | _continueDestroy() { | |
| 236 | if (this._pendingDestroy === null) return | |
| 237 | const cb = this._pendingDestroy | |
| 238 | this._pendingDestroy = null | |
| 239 | cb(null) | |
| 240 | } | |
| 241 | } | |
| 242 | ||
| 243 | exports.Server = class NetServer extends EventEmitter { | |
| 244 | constructor(opts = {}, onconnection) { | |
| 245 | if (typeof opts === 'function') { | |
| 246 | onconnection = opts | |
| 247 | opts = {} | |
| 248 | } | |
| 249 | ||
| 250 | super() | |
| 251 | ||
| 252 | const { | |
| 253 | readBufferSize = defaultReadBufferSize, | |
| 254 | allowHalfOpen = false, | |
| 255 | pauseOnConnect = false | |
| 256 | } = opts | |
| 257 | ||
| 258 | this._type = 0 | |
| 259 | this._state = 0 | |
| 260 | this._server = null | |
| 261 | ||
| 262 | this._opts = { readBufferSize, allowHalfOpen, pauseOnConnect } | |
| 263 | ||
| 264 | if (onconnection) this.on('connection', onconnection) | |
| 265 | } | |
| 266 | ||
| 267 | get listening() { | |
| 268 | return this._server !== null && this._server.listening | |
| 269 | } | |
| 270 | ||
| 271 | address() { | |
| 272 | return this._server === null ? null : this._server.address() | |
| 273 | } | |
| 274 | ||
| 275 | listen(...args) { | |
| 276 | let opts = {} | |
| 277 | let onlistening | |
| 278 | ||
| 279 | if (typeof args[0] === 'string') { | |
| 280 | // listen(path[, backlog][, onlistening]) | |
| 281 | opts.path = args[0] | |
| 282 | ||
| 283 | if (typeof args[1] === 'function') { | |
| 284 | onlistening = args[1] | |
| 285 | } else { | |
| 286 | opts.backlog = args[1] | |
| 287 | onlistening = args[2] | |
| 288 | } | |
| 289 | } else { | |
| 290 | // listen([port[, host[, backlog]]][, onlistening]) | |
| 291 | if (typeof args[0] === 'function') { | |
| 292 | onlistening = args[0] | |
| 293 | } else { | |
| 294 | opts.port = args[0] | |
| 295 | ||
| 296 | if (typeof args[1] === 'function') { | |
| 297 | onlistening = args[1] | |
| 298 | } else { | |
| 299 | opts.host = args[1] | |
| 300 | ||
| 301 | if (typeof args[2] === 'function') { | |
| 302 | onlistening = args[2] | |
| 303 | } else { | |
| 304 | opts.backlog = args[2] | |
| 305 | onlistening = args[3] | |
| 306 | } | |
| 307 | } | |
| 308 | } | |
| 309 | } | |
| 310 | ||
| 311 | opts = { ...opts, ...this._opts } | |
| 312 | ||
| 313 | if (opts.path) { | |
| 314 | this._attach(constants.type.IPC, pipe.createServer(opts)) | |
| 315 | } else { | |
| 316 | this._attach(constants.type.TCP, tcp.createServer(opts)) | |
| 317 | } | |
| 318 | ||
| 319 | this._server.listen(opts) | |
| 320 | ||
| 321 | if (onlistening) this.once('listening', onlistening) | |
| 322 | ||
| 323 | return this | |
| 324 | } | |
| 325 | ||
| 326 | close(onclose) { | |
| 327 | if (onclose) this.once('close', onclose) | |
| 328 | this._server.close() | |
| 329 | return this | |
| 330 | } | |
| 331 | ||
| 332 | ref() { | |
| 333 | this._state &= ~constants.state.UNREFED | |
| 334 | if (this._server !== null) this._server.ref() | |
| 335 | return this | |
| 336 | } | |
| 337 | ||
| 338 | unref() { | |
| 339 | this._state |= constants.state.UNREFED | |
| 340 | if (this._server !== null) this._server.unref() | |
| 341 | return this | |
| 342 | } | |
| 343 | ||
| 344 | _attach(type, server) { | |
| 345 | this._type = type | |
| 346 | this._server = server | |
| 347 | ||
| 348 | this._server | |
| 349 | .on('listening', this._onlistening.bind(this)) | |
| 350 | .on('connection', this._onconnection.bind(this)) | |
| 351 | .on('error', this._onerror.bind(this)) | |
| 352 | .on('close', this._onclose.bind(this)) | |
| 353 | ||
| 354 | if (this._state & constants.state.UNREFED) this._server.unref() | |
| 355 | ||
| 356 | return this | |
| 357 | } | |
| 358 | ||
| 359 | _onlistening() { | |
| 360 | this.emit('listening') | |
| 361 | } | |
| 362 | ||
| 363 | _onconnection(socket) { | |
| 364 | this.emit('connection', new exports.Socket(this._opts)._attach(this._type, socket)) | |
| 365 | } | |
| 366 | ||
| 367 | _onerror(err) { | |
| 368 | this.emit('error', err) | |
| 369 | } | |
| 370 | ||
| 371 | _onclose() { | |
| 372 | this.emit('close') | |
| 373 | } | |
| 374 | } | |
| 375 | ||
| 376 | exports.constants = constants | |
| 377 | ||
| 378 | exports.isIP = tcp.isIP | |
| 379 | exports.isIPv4 = tcp.isIPv4 | |
| 380 | exports.isIPv6 = tcp.isIPv6 | |
| 381 | ||
| 382 | exports.createConnection = function createConnection(...args) { | |
| 383 | let opts = {} | |
| 384 | let onconnect | |
| 385 | ||
| 386 | if (typeof args[0] === 'string') { | |
| 387 | // createConnection(path[, onconnect]) | |
| 388 | opts.path = args[0] | |
| 389 | onconnect = args[1] | |
| 390 | } else if (typeof args[0] === 'number') { | |
| 391 | // createConnection(port[, host][, onconnect]) | |
| 392 | opts.port = args[0] | |
| 393 | ||
| 394 | if (typeof args[1] === 'function') { | |
| 395 | onconnect = args[1] | |
| 396 | } else { | |
| 397 | opts.host = args[1] | |
| 398 | onconnect = args[2] | |
| 399 | } | |
| 400 | } else { | |
| 401 | // createConnection(opts[, onconnect]) | |
| 402 | opts = args[0] || {} | |
| 403 | onconnect = args[1] | |
| 404 | } | |
| 405 | ||
| 406 | return new exports.Socket(opts).connect(opts, onconnect) | |
| 407 | } | |
| 408 | ||
| 409 | // For Node.js compatibility | |
| 410 | exports.connect = exports.createConnection | |
| 411 | ||
| 412 | exports.createServer = function createServer(opts, onconnection) { | |
| 413 | return new exports.Server(opts, onconnection) | |
| 414 | } |
| … | … | @@ -0,0 +1,6 @@ |
| 1 | declare const constants: { | |
| 2 | type: { TCP: 1; IPC: 2 } | |
| 3 | state: { UNREFED: number } | |
| 4 | } | |
| 5 | ||
| 6 | export = constants |
| … | … | @@ -0,0 +1,9 @@ |
| 1 | module.exports = { | |
| 2 | type: { | |
| 3 | TCP: 1, | |
| 4 | IPC: 2 | |
| 5 | }, | |
| 6 | state: { | |
| 7 | UNREFED: 0x1 | |
| 8 | } | |
| 9 | } |
| … | … | @@ -0,0 +1,45 @@ |
| 1 | { | |
| 2 | "name": "bare-net", | |
| 3 | "version": "2.3.1", | |
| 4 | "description": "TCP and IPC servers and clients for JavaScript", | |
| 5 | "exports": { | |
| 6 | "./package": "./package.json", | |
| 7 | ".": { | |
| 8 | "types": "./index.d.ts", | |
| 9 | "default": "./index.js" | |
| 10 | }, | |
| 11 | "./constants": { | |
| 12 | "types": "./lib/constants.d.ts", | |
| 13 | "default": "./lib/constants.js" | |
| 14 | } | |
| 15 | }, | |
| 16 | "files": [ | |
| 17 | "index.js", | |
| 18 | "index.d.ts", | |
| 19 | "lib" | |
| 20 | ], | |
| 21 | "scripts": { | |
| 22 | "test": "prettier . --check && bare test.js" | |
| 23 | }, | |
| 24 | "repository": { | |
| 25 | "type": "git", | |
| 26 | "url": "git+https://github.com/holepunchto/bare-net.git" | |
| 27 | }, | |
| 28 | "author": "Holepunch", | |
| 29 | "license": "Apache-2.0", | |
| 30 | "bugs": { | |
| 31 | "url": "https://github.com/holepunchto/bare-net/issues" | |
| 32 | }, | |
| 33 | "homepage": "https://github.com/holepunchto/bare-net#readme", | |
| 34 | "dependencies": { | |
| 35 | "bare-events": "^2.2.2", | |
| 36 | "bare-pipe": "^4.0.0", | |
| 37 | "bare-stream": "^2.0.0", | |
| 38 | "bare-tcp": "^2.0.0" | |
| 39 | }, | |
| 40 | "devDependencies": { | |
| 41 | "brittle": "^3.3.0", | |
| 42 | "prettier": "^3.4.1", | |
| 43 | "prettier-config-holepunch": "^2.0.0" | |
| 44 | } | |
| 45 | } |
| … | … | @@ -0,0 +1,201 @@ |
| 1 | Apache License | |
| 2 | Version 2.0, January 2004 | |
| 3 | http://www.apache.org/licenses/ | |
| 4 | ||
| 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | |
| 6 | ||
| 7 | 1. Definitions. | |
| 8 | ||
| 9 | "License" shall mean the terms and conditions for use, reproduction, | |
| 10 | and distribution as defined by Sections 1 through 9 of this document. | |
| 11 | ||
| 12 | "Licensor" shall mean the copyright owner or entity authorized by | |
| 13 | the copyright owner that is granting the License. | |
| 14 | ||
| 15 | "Legal Entity" shall mean the union of the acting entity and all | |
| 16 | other entities that control, are controlled by, or are under common | |
| 17 | control with that entity. For the purposes of this definition, | |
| 18 | "control" means (i) the power, direct or indirect, to cause the | |
| 19 | direction or management of such entity, whether by contract or | |
| 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the | |
| 21 | outstanding shares, or (iii) beneficial ownership of such entity. | |
| 22 | ||
| 23 | "You" (or "Your") shall mean an individual or Legal Entity | |
| 24 | exercising permissions granted by this License. | |
| 25 | ||
| 26 | "Source" form shall mean the preferred form for making modifications, | |
| 27 | including but not limited to software source code, documentation | |
| 28 | source, and configuration files. | |
| 29 | ||
| 30 | "Object" form shall mean any form resulting from mechanical | |
| 31 | transformation or translation of a Source form, including but | |
| 32 | not limited to compiled object code, generated documentation, | |
| 33 | and conversions to other media types. | |
| 34 | ||
| 35 | "Work" shall mean the work of authorship, whether in Source or | |
| 36 | Object form, made available under the License, as indicated by a | |
| 37 | copyright notice that is included in or attached to the work | |
| 38 | (an example is provided in the Appendix below). | |
| 39 | ||
| 40 | "Derivative Works" shall mean any work, whether in Source or Object | |
| 41 | form, that is based on (or derived from) the Work and for which the | |
| 42 | editorial revisions, annotations, elaborations, or other modifications | |
| 43 | represent, as a whole, an original work of authorship. For the purposes | |
| 44 | of this License, Derivative Works shall not include works that remain | |
| 45 | separable from, or merely link (or bind by name) to the interfaces of, | |
| 46 | the Work and Derivative Works thereof. | |
| 47 | ||
| 48 | "Contribution" shall mean any work of authorship, including | |
| 49 | the original version of the Work and any modifications or additions | |
| 50 | to that Work or Derivative Works thereof, that is intentionally | |
| 51 | submitted to Licensor for inclusion in the Work by the copyright owner | |
| 52 | or by an individual or Legal Entity authorized to submit on behalf of | |
| 53 | the copyright owner. For the purposes of this definition, "submitted" | |
| 54 | means any form of electronic, verbal, or written communication sent | |
| 55 | to the Licensor or its representatives, including but not limited to | |
| 56 | communication on electronic mailing lists, source code control systems, | |
| 57 | and issue tracking systems that are managed by, or on behalf of, the | |
| 58 | Licensor for the purpose of discussing and improving the Work, but | |
| 59 | excluding communication that is conspicuously marked or otherwise | |
| 60 | designated in writing by the copyright owner as "Not a Contribution." | |
| 61 | ||
| 62 | "Contributor" shall mean Licensor and any individual or Legal Entity | |
| 63 | on behalf of whom a Contribution has been received by Licensor and | |
| 64 | subsequently incorporated within the Work. | |
| 65 | ||
| 66 | 2. Grant of Copyright License. Subject to the terms and conditions of | |
| 67 | this License, each Contributor hereby grants to You a perpetual, | |
| 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 69 | copyright license to reproduce, prepare Derivative Works of, | |
| 70 | publicly display, publicly perform, sublicense, and distribute the | |
| 71 | Work and such Derivative Works in Source or Object form. | |
| 72 | ||
| 73 | 3. Grant of Patent License. Subject to the terms and conditions of | |
| 74 | this License, each Contributor hereby grants to You a perpetual, | |
| 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 76 | (except as stated in this section) patent license to make, have made, | |
| 77 | use, offer to sell, sell, import, and otherwise transfer the Work, | |
| 78 | where such license applies only to those patent claims licensable | |
| 79 | by such Contributor that are necessarily infringed by their | |
| 80 | Contribution(s) alone or by combination of their Contribution(s) | |
| 81 | with the Work to which such Contribution(s) was submitted. If You | |
| 82 | institute patent litigation against any entity (including a | |
| 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work | |
| 84 | or a Contribution incorporated within the Work constitutes direct | |
| 85 | or contributory patent infringement, then any patent licenses | |
| 86 | granted to You under this License for that Work shall terminate | |
| 87 | as of the date such litigation is filed. | |
| 88 | ||
| 89 | 4. Redistribution. You may reproduce and distribute copies of the | |
| 90 | Work or Derivative Works thereof in any medium, with or without | |
| 91 | modifications, and in Source or Object form, provided that You | |
| 92 | meet the following conditions: | |
| 93 | ||
| 94 | (a) You must give any other recipients of the Work or | |
| 95 | Derivative Works a copy of this License; and | |
| 96 | ||
| 97 | (b) You must cause any modified files to carry prominent notices | |
| 98 | stating that You changed the files; and | |
| 99 | ||
| 100 | (c) You must retain, in the Source form of any Derivative Works | |
| 101 | that You distribute, all copyright, patent, trademark, and | |
| 102 | attribution notices from the Source form of the Work, | |
| 103 | excluding those notices that do not pertain to any part of | |
| 104 | the Derivative Works; and | |
| 105 | ||
| 106 | (d) If the Work includes a "NOTICE" text file as part of its | |
| 107 | distribution, then any Derivative Works that You distribute must | |
| 108 | include a readable copy of the attribution notices contained | |
| 109 | within such NOTICE file, excluding those notices that do not | |
| 110 | pertain to any part of the Derivative Works, in at least one | |
| 111 | of the following places: within a NOTICE text file distributed | |
| 112 | as part of the Derivative Works; within the Source form or | |
| 113 | documentation, if provided along with the Derivative Works; or, | |
| 114 | within a display generated by the Derivative Works, if and | |
| 115 | wherever such third-party notices normally appear. The contents | |
| 116 | of the NOTICE file are for informational purposes only and | |
| 117 | do not modify the License. You may add Your own attribution | |
| 118 | notices within Derivative Works that You distribute, alongside | |
| 119 | or as an addendum to the NOTICE text from the Work, provided | |
| 120 | that such additional attribution notices cannot be construed | |
| 121 | as modifying the License. | |
| 122 | ||
| 123 | You may add Your own copyright statement to Your modifications and | |
| 124 | may provide additional or different license terms and conditions | |
| 125 | for use, reproduction, or distribution of Your modifications, or | |
| 126 | for any such Derivative Works as a whole, provided Your use, | |
| 127 | reproduction, and distribution of the Work otherwise complies with | |
| 128 | the conditions stated in this License. | |
| 129 | ||
| 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, | |
| 131 | any Contribution intentionally submitted for inclusion in the Work | |
| 132 | by You to the Licensor shall be under the terms and conditions of | |
| 133 | this License, without any additional terms or conditions. | |
| 134 | Notwithstanding the above, nothing herein shall supersede or modify | |
| 135 | the terms of any separate license agreement you may have executed | |
| 136 | with Licensor regarding such Contributions. | |
| 137 | ||
| 138 | 6. Trademarks. This License does not grant permission to use the trade | |
| 139 | names, trademarks, service marks, or product names of the Licensor, | |
| 140 | except as required for reasonable and customary use in describing the | |
| 141 | origin of the Work and reproducing the content of the NOTICE file. | |
| 142 | ||
| 143 | 7. Disclaimer of Warranty. Unless required by applicable law or | |
| 144 | agreed to in writing, Licensor provides the Work (and each | |
| 145 | Contributor provides its Contributions) on an "AS IS" BASIS, | |
| 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |
| 147 | implied, including, without limitation, any warranties or conditions | |
| 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | |
| 149 | PARTICULAR PURPOSE. You are solely responsible for determining the | |
| 150 | appropriateness of using or redistributing the Work and assume any | |
| 151 | risks associated with Your exercise of permissions under this License. | |
| 152 | ||
| 153 | 8. Limitation of Liability. In no event and under no legal theory, | |
| 154 | whether in tort (including negligence), contract, or otherwise, | |
| 155 | unless required by applicable law (such as deliberate and grossly | |
| 156 | negligent acts) or agreed to in writing, shall any Contributor be | |
| 157 | liable to You for damages, including any direct, indirect, special, | |
| 158 | incidental, or consequential damages of any character arising as a | |
| 159 | result of this License or out of the use or inability to use the | |
| 160 | Work (including but not limited to damages for loss of goodwill, | |
| 161 | work stoppage, computer failure or malfunction, or any and all | |
| 162 | other commercial damages or losses), even if such Contributor | |
| 163 | has been advised of the possibility of such damages. | |
| 164 | ||
| 165 | 9. Accepting Warranty or Additional Liability. While redistributing | |
| 166 | the Work or Derivative Works thereof, You may choose to offer, | |
| 167 | and charge a fee for, acceptance of support, warranty, indemnity, | |
| 168 | or other liability obligations and/or rights consistent with this | |
| 169 | License. However, in accepting such obligations, You may act only | |
| 170 | on Your own behalf and on Your sole responsibility, not on behalf | |
| 171 | of any other Contributor, and only if You agree to indemnify, | |
| 172 | defend, and hold each Contributor harmless for any liability | |
| 173 | incurred by, or claims asserted against, such Contributor by reason | |
| 174 | of your accepting any such warranty or additional liability. | |
| 175 | ||
| 176 | END OF TERMS AND CONDITIONS | |
| 177 | ||
| 178 | APPENDIX: How to apply the Apache License to your work. | |
| 179 | ||
| 180 | To apply the Apache License to your work, attach the following | |
| 181 | boilerplate notice, with the fields enclosed by brackets "[]" | |
| 182 | replaced with your own identifying information. (Don't include | |
| 183 | the brackets!) The text should be enclosed in the appropriate | |
| 184 | comment syntax for the file format. We also recommend that a | |
| 185 | file or class name and description of purpose be included on the | |
| 186 | same "printed page" as the copyright notice for easier | |
| 187 | identification within third-party archives. | |
| 188 | ||
| 189 | Copyright [yyyy] [name of copyright owner] | |
| 190 | ||
| 191 | Licensed under the Apache License, Version 2.0 (the "License"); | |
| 192 | you may not use this file except in compliance with the License. | |
| 193 | You may obtain a copy of the License at | |
| 194 | ||
| 195 | http://www.apache.org/licenses/LICENSE-2.0 | |
| 196 | ||
| 197 | Unless required by applicable law or agreed to in writing, software | |
| 198 | distributed under the License is distributed on an "AS IS" BASIS, | |
| 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 200 | See the License for the specific language governing permissions and | |
| 201 | limitations under the License. |
| … | … | @@ -0,0 +1,20 @@ |
| 1 | # bare-stylize | |
| 2 | ||
| 3 | Terminal string styling for JavaScript. | |
| 4 | ||
| 5 | ``` | |
| 6 | npm i bare-stylize | |
| 7 | ``` | |
| 8 | ||
| 9 | ## Usage | |
| 10 | ||
| 11 | ```js | |
| 12 | const stylize = require('bare-stylize') | |
| 13 | ||
| 14 | console.log(stylize('green', 'ok')) | |
| 15 | console.log(stylize(['bold', 'red'], 'error')) | |
| 16 | ``` | |
| 17 | ||
| 18 | ## License | |
| 19 | ||
| 20 | Apache-2.0 |
| … | … | @@ -0,0 +1,36 @@ |
| 1 | const escapes = require('bare-ansi-escapes') | |
| 2 | const process = require('bare-process') | |
| 3 | ||
| 4 | function getStylesMap() { | |
| 5 | const stylesMap = {} | |
| 6 | ||
| 7 | let style | |
| 8 | for (const escape of Object.getOwnPropertyNames(escapes)) { | |
| 9 | if (escape.startsWith('color')) { | |
| 10 | style = escape[5].toLowerCase() + escape.slice(6) | |
| 11 | } else if (escape.startsWith('modifier')) { | |
| 12 | style = escape[8].toLowerCase() + escape.slice(9) | |
| 13 | } else { | |
| 14 | continue | |
| 15 | } | |
| 16 | ||
| 17 | stylesMap[style] = escapes[escape] | |
| 18 | } | |
| 19 | ||
| 20 | return stylesMap | |
| 21 | } | |
| 22 | ||
| 23 | const stylesMap = getStylesMap() | |
| 24 | ||
| 25 | module.exports = function stylize(styles, text, opts = {}) { | |
| 26 | const validateStream = opts.validateStream ?? true | |
| 27 | const stream = opts.stream ?? process.stdout | |
| 28 | ||
| 29 | if (!validateStream || !stream.isTTY) return text | |
| 30 | ||
| 31 | if (typeof styles === 'string') styles = [styles] | |
| 32 | ||
| 33 | const code = styles.map((style) => stylesMap[style]).join('') | |
| 34 | ||
| 35 | return code + text + escapes.modifierReset | |
| 36 | } |
| … | … | @@ -0,0 +1,36 @@ |
| 1 | { | |
| 2 | "name": "bare-stylize", | |
| 3 | "version": "0.0.1", | |
| 4 | "description": "Terminal string styling for JavaScript", | |
| 5 | "exports": { | |
| 6 | "./package": "./package.json", | |
| 7 | ".": { | |
| 8 | "default": "./index.js" | |
| 9 | } | |
| 10 | }, | |
| 11 | "files": [ | |
| 12 | "index.js" | |
| 13 | ], | |
| 14 | "scripts": { | |
| 15 | "test": "prettier . --check && bare test.js" | |
| 16 | }, | |
| 17 | "repository": { | |
| 18 | "type": "git", | |
| 19 | "url": "git+https://github.com/holepunchto/bare-stylize.git" | |
| 20 | }, | |
| 21 | "author": "Holepunch", | |
| 22 | "license": "Apache-2.0", | |
| 23 | "bugs": { | |
| 24 | "url": "https://github.com/holepunchto/bare-stylize/issues" | |
| 25 | }, | |
| 26 | "homepage": "https://github.com/holepunchto/bare-stylize#readme", | |
| 27 | "dependencies": { | |
| 28 | "bare-ansi-escapes": "^2.2.3", | |
| 29 | "bare-process": "^4.2.1" | |
| 30 | }, | |
| 31 | "devDependencies": { | |
| 32 | "brittle": "^3.3.2", | |
| 33 | "prettier": "^3.4.2", | |
| 34 | "prettier-config-holepunch": "^2.0.0" | |
| 35 | } | |
| 36 | } |
| … | … | @@ -0,0 +1,13 @@ |
| 1 | cmake_minimum_required(VERSION 3.25) | |
| 2 | ||
| 3 | find_package(cmake-bare REQUIRED PATHS node_modules/cmake-bare) | |
| 4 | ||
| 5 | project(bare_subprocess C) | |
| 6 | ||
| 7 | add_bare_module(bare_subprocess) | |
| 8 | ||
| 9 | target_sources( | |
| 10 | ${bare_subprocess} | |
| 11 | PRIVATE | |
| 12 | binding.c | |
| 13 | ) |
| … | … | @@ -0,0 +1,201 @@ |
| 1 | Apache License | |
| 2 | Version 2.0, January 2004 | |
| 3 | http://www.apache.org/licenses/ | |
| 4 | ||
| 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | |
| 6 | ||
| 7 | 1. Definitions. | |
| 8 | ||
| 9 | "License" shall mean the terms and conditions for use, reproduction, | |
| 10 | and distribution as defined by Sections 1 through 9 of this document. | |
| 11 | ||
| 12 | "Licensor" shall mean the copyright owner or entity authorized by | |
| 13 | the copyright owner that is granting the License. | |
| 14 | ||
| 15 | "Legal Entity" shall mean the union of the acting entity and all | |
| 16 | other entities that control, are controlled by, or are under common | |
| 17 | control with that entity. For the purposes of this definition, | |
| 18 | "control" means (i) the power, direct or indirect, to cause the | |
| 19 | direction or management of such entity, whether by contract or | |
| 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the | |
| 21 | outstanding shares, or (iii) beneficial ownership of such entity. | |
| 22 | ||
| 23 | "You" (or "Your") shall mean an individual or Legal Entity | |
| 24 | exercising permissions granted by this License. | |
| 25 | ||
| 26 | "Source" form shall mean the preferred form for making modifications, | |
| 27 | including but not limited to software source code, documentation | |
| 28 | source, and configuration files. | |
| 29 | ||
| 30 | "Object" form shall mean any form resulting from mechanical | |
| 31 | transformation or translation of a Source form, including but | |
| 32 | not limited to compiled object code, generated documentation, | |
| 33 | and conversions to other media types. | |
| 34 | ||
| 35 | "Work" shall mean the work of authorship, whether in Source or | |
| 36 | Object form, made available under the License, as indicated by a | |
| 37 | copyright notice that is included in or attached to the work | |
| 38 | (an example is provided in the Appendix below). | |
| 39 | ||
| 40 | "Derivative Works" shall mean any work, whether in Source or Object | |
| 41 | form, that is based on (or derived from) the Work and for which the | |
| 42 | editorial revisions, annotations, elaborations, or other modifications | |
| 43 | represent, as a whole, an original work of authorship. For the purposes | |
| 44 | of this License, Derivative Works shall not include works that remain | |
| 45 | separable from, or merely link (or bind by name) to the interfaces of, | |
| 46 | the Work and Derivative Works thereof. | |
| 47 | ||
| 48 | "Contribution" shall mean any work of authorship, including | |
| 49 | the original version of the Work and any modifications or additions | |
| 50 | to that Work or Derivative Works thereof, that is intentionally | |
| 51 | submitted to Licensor for inclusion in the Work by the copyright owner | |
| 52 | or by an individual or Legal Entity authorized to submit on behalf of | |
| 53 | the copyright owner. For the purposes of this definition, "submitted" | |
| 54 | means any form of electronic, verbal, or written communication sent | |
| 55 | to the Licensor or its representatives, including but not limited to | |
| 56 | communication on electronic mailing lists, source code control systems, | |
| 57 | and issue tracking systems that are managed by, or on behalf of, the | |
| 58 | Licensor for the purpose of discussing and improving the Work, but | |
| 59 | excluding communication that is conspicuously marked or otherwise | |
| 60 | designated in writing by the copyright owner as "Not a Contribution." | |
| 61 | ||
| 62 | "Contributor" shall mean Licensor and any individual or Legal Entity | |
| 63 | on behalf of whom a Contribution has been received by Licensor and | |
| 64 | subsequently incorporated within the Work. | |
| 65 | ||
| 66 | 2. Grant of Copyright License. Subject to the terms and conditions of | |
| 67 | this License, each Contributor hereby grants to You a perpetual, | |
| 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 69 | copyright license to reproduce, prepare Derivative Works of, | |
| 70 | publicly display, publicly perform, sublicense, and distribute the | |
| 71 | Work and such Derivative Works in Source or Object form. | |
| 72 | ||
| 73 | 3. Grant of Patent License. Subject to the terms and conditions of | |
| 74 | this License, each Contributor hereby grants to You a perpetual, | |
| 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 76 | (except as stated in this section) patent license to make, have made, | |
| 77 | use, offer to sell, sell, import, and otherwise transfer the Work, | |
| 78 | where such license applies only to those patent claims licensable | |
| 79 | by such Contributor that are necessarily infringed by their | |
| 80 | Contribution(s) alone or by combination of their Contribution(s) | |
| 81 | with the Work to which such Contribution(s) was submitted. If You | |
| 82 | institute patent litigation against any entity (including a | |
| 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work | |
| 84 | or a Contribution incorporated within the Work constitutes direct | |
| 85 | or contributory patent infringement, then any patent licenses | |
| 86 | granted to You under this License for that Work shall terminate | |
| 87 | as of the date such litigation is filed. | |
| 88 | ||
| 89 | 4. Redistribution. You may reproduce and distribute copies of the | |
| 90 | Work or Derivative Works thereof in any medium, with or without | |
| 91 | modifications, and in Source or Object form, provided that You | |
| 92 | meet the following conditions: | |
| 93 | ||
| 94 | (a) You must give any other recipients of the Work or | |
| 95 | Derivative Works a copy of this License; and | |
| 96 | ||
| 97 | (b) You must cause any modified files to carry prominent notices | |
| 98 | stating that You changed the files; and | |
| 99 | ||
| 100 | (c) You must retain, in the Source form of any Derivative Works | |
| 101 | that You distribute, all copyright, patent, trademark, and | |
| 102 | attribution notices from the Source form of the Work, | |
| 103 | excluding those notices that do not pertain to any part of | |
| 104 | the Derivative Works; and | |
| 105 | ||
| 106 | (d) If the Work includes a "NOTICE" text file as part of its | |
| 107 | distribution, then any Derivative Works that You distribute must | |
| 108 | include a readable copy of the attribution notices contained | |
| 109 | within such NOTICE file, excluding those notices that do not | |
| 110 | pertain to any part of the Derivative Works, in at least one | |
| 111 | of the following places: within a NOTICE text file distributed | |
| 112 | as part of the Derivative Works; within the Source form or | |
| 113 | documentation, if provided along with the Derivative Works; or, | |
| 114 | within a display generated by the Derivative Works, if and | |
| 115 | wherever such third-party notices normally appear. The contents | |
| 116 | of the NOTICE file are for informational purposes only and | |
| 117 | do not modify the License. You may add Your own attribution | |
| 118 | notices within Derivative Works that You distribute, alongside | |
| 119 | or as an addendum to the NOTICE text from the Work, provided | |
| 120 | that such additional attribution notices cannot be construed | |
| 121 | as modifying the License. | |
| 122 | ||
| 123 | You may add Your own copyright statement to Your modifications and | |
| 124 | may provide additional or different license terms and conditions | |
| 125 | for use, reproduction, or distribution of Your modifications, or | |
| 126 | for any such Derivative Works as a whole, provided Your use, | |
| 127 | reproduction, and distribution of the Work otherwise complies with | |
| 128 | the conditions stated in this License. | |
| 129 | ||
| 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, | |
| 131 | any Contribution intentionally submitted for inclusion in the Work | |
| 132 | by You to the Licensor shall be under the terms and conditions of | |
| 133 | this License, without any additional terms or conditions. | |
| 134 | Notwithstanding the above, nothing herein shall supersede or modify | |
| 135 | the terms of any separate license agreement you may have executed | |
| 136 | with Licensor regarding such Contributions. | |
| 137 | ||
| 138 | 6. Trademarks. This License does not grant permission to use the trade | |
| 139 | names, trademarks, service marks, or product names of the Licensor, | |
| 140 | except as required for reasonable and customary use in describing the | |
| 141 | origin of the Work and reproducing the content of the NOTICE file. | |
| 142 | ||
| 143 | 7. Disclaimer of Warranty. Unless required by applicable law or | |
| 144 | agreed to in writing, Licensor provides the Work (and each | |
| 145 | Contributor provides its Contributions) on an "AS IS" BASIS, | |
| 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |
| 147 | implied, including, without limitation, any warranties or conditions | |
| 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | |
| 149 | PARTICULAR PURPOSE. You are solely responsible for determining the | |
| 150 | appropriateness of using or redistributing the Work and assume any | |
| 151 | risks associated with Your exercise of permissions under this License. | |
| 152 | ||
| 153 | 8. Limitation of Liability. In no event and under no legal theory, | |
| 154 | whether in tort (including negligence), contract, or otherwise, | |
| 155 | unless required by applicable law (such as deliberate and grossly | |
| 156 | negligent acts) or agreed to in writing, shall any Contributor be | |
| 157 | liable to You for damages, including any direct, indirect, special, | |
| 158 | incidental, or consequential damages of any character arising as a | |
| 159 | result of this License or out of the use or inability to use the | |
| 160 | Work (including but not limited to damages for loss of goodwill, | |
| 161 | work stoppage, computer failure or malfunction, or any and all | |
| 162 | other commercial damages or losses), even if such Contributor | |
| 163 | has been advised of the possibility of such damages. | |
| 164 | ||
| 165 | 9. Accepting Warranty or Additional Liability. While redistributing | |
| 166 | the Work or Derivative Works thereof, You may choose to offer, | |
| 167 | and charge a fee for, acceptance of support, warranty, indemnity, | |
| 168 | or other liability obligations and/or rights consistent with this | |
| 169 | License. However, in accepting such obligations, You may act only | |
| 170 | on Your own behalf and on Your sole responsibility, not on behalf | |
| 171 | of any other Contributor, and only if You agree to indemnify, | |
| 172 | defend, and hold each Contributor harmless for any liability | |
| 173 | incurred by, or claims asserted against, such Contributor by reason | |
| 174 | of your accepting any such warranty or additional liability. | |
| 175 | ||
| 176 | END OF TERMS AND CONDITIONS | |
| 177 | ||
| 178 | APPENDIX: How to apply the Apache License to your work. | |
| 179 | ||
| 180 | To apply the Apache License to your work, attach the following | |
| 181 | boilerplate notice, with the fields enclosed by brackets "[]" | |
| 182 | replaced with your own identifying information. (Don't include | |
| 183 | the brackets!) The text should be enclosed in the appropriate | |
| 184 | comment syntax for the file format. We also recommend that a | |
| 185 | file or class name and description of purpose be included on the | |
| 186 | same "printed page" as the copyright notice for easier | |
| 187 | identification within third-party archives. | |
| 188 | ||
| 189 | Copyright [yyyy] [name of copyright owner] | |
| 190 | ||
| 191 | Licensed under the Apache License, Version 2.0 (the "License"); | |
| 192 | you may not use this file except in compliance with the License. | |
| 193 | You may obtain a copy of the License at | |
| 194 | ||
| 195 | http://www.apache.org/licenses/LICENSE-2.0 | |
| 196 | ||
| 197 | Unless required by applicable law or agreed to in writing, software | |
| 198 | distributed under the License is distributed on an "AS IS" BASIS, | |
| 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 200 | See the License for the specific language governing permissions and | |
| 201 | limitations under the License. |
| … | … | @@ -0,0 +1,23 @@ |
| 1 | # bare-subprocess | |
| 2 | ||
| 3 | Native process spawning for JavaScript. | |
| 4 | ||
| 5 | ``` | |
| 6 | npm i bare-subprocess | |
| 7 | ``` | |
| 8 | ||
| 9 | ## Usage | |
| 10 | ||
| 11 | ```js | |
| 12 | const { spawn } = require('bare-subprocess') | |
| 13 | ||
| 14 | const subprocess = spawn('echo', ['hello', 'world'], { | |
| 15 | stdio: 'inherit' | |
| 16 | }) | |
| 17 | ||
| 18 | subprocess.on('exit', () => console.log('done')) | |
| 19 | ``` | |
| 20 | ||
| 21 | ## License | |
| 22 | ||
| 23 | Apache-2.0 |
| … | … | @@ -0,0 +1,725 @@ |
| 1 | #include <assert.h> | |
| 2 | #include <bare.h> | |
| 3 | #include <js.h> | |
| 4 | #include <stdlib.h> | |
| 5 | #include <utf.h> | |
| 6 | #include <uv.h> | |
| 7 | ||
| 8 | typedef struct { | |
| 9 | uv_process_t handle; | |
| 10 | ||
| 11 | js_env_t *env; | |
| 12 | js_ref_t *ctx; | |
| 13 | js_ref_t *on_exit; | |
| 14 | ||
| 15 | bool killed; | |
| 16 | bool exiting; | |
| 17 | ||
| 18 | js_deferred_teardown_t *teardown; | |
| 19 | } bare_subprocess_t; | |
| 20 | ||
| 21 | typedef struct { | |
| 22 | uv_pipe_t pipe; | |
| 23 | uv_buf_t read; | |
| 24 | size_t written; | |
| 25 | } bare_subprocess_buffered_pipe_t; | |
| 26 | ||
| 27 | typedef utf8_t bare_subprocess_path_t[4096 + 1 /* NULL */]; | |
| 28 | ||
| 29 | static void | |
| 30 | bare_subprocess__on_close(uv_handle_t *handle) { | |
| 31 | int err; | |
| 32 | ||
| 33 | bare_subprocess_t *subprocess = (bare_subprocess_t *) handle; | |
| 34 | ||
| 35 | js_env_t *env = subprocess->env; | |
| 36 | ||
| 37 | js_deferred_teardown_t *teardown = subprocess->teardown; | |
| 38 | ||
| 39 | err = js_delete_reference(env, subprocess->on_exit); | |
| 40 | assert(err == 0); | |
| 41 | ||
| 42 | err = js_delete_reference(env, subprocess->ctx); | |
| 43 | assert(err == 0); | |
| 44 | ||
| 45 | err = js_finish_deferred_teardown_callback(teardown); | |
| 46 | assert(err == 0); | |
| 47 | } | |
| 48 | ||
| 49 | static void | |
| 50 | bare_subprocess__on_exit(uv_process_t *handle, int64_t exit_status, int term_signal) { | |
| 51 | int err; | |
| 52 | ||
| 53 | bare_subprocess_t *subprocess = (bare_subprocess_t *) handle; | |
| 54 | ||
| 55 | subprocess->killed = true; | |
| 56 | ||
| 57 | if (subprocess->exiting) goto close; | |
| 58 | ||
| 59 | js_env_t *env = subprocess->env; | |
| 60 | ||
| 61 | js_handle_scope_t *scope; | |
| 62 | err = js_open_handle_scope(env, &scope); | |
| 63 | assert(err == 0); | |
| 64 | ||
| 65 | js_value_t *ctx; | |
| 66 | err = js_get_reference_value(env, subprocess->ctx, &ctx); | |
| 67 | assert(err == 0); | |
| 68 | ||
| 69 | js_value_t *on_exit; | |
| 70 | err = js_get_reference_value(env, subprocess->on_exit, &on_exit); | |
| 71 | assert(err == 0); | |
| 72 | ||
| 73 | js_value_t *argv[2]; | |
| 74 | ||
| 75 | err = js_create_int64(env, exit_status, &argv[0]); | |
| 76 | assert(err == 0); | |
| 77 | ||
| 78 | err = js_create_int32(env, term_signal, &argv[1]); | |
| 79 | assert(err == 0); | |
| 80 | ||
| 81 | js_call_function(env, ctx, on_exit, 2, argv, NULL); | |
| 82 | ||
| 83 | err = js_close_handle_scope(env, scope); | |
| 84 | assert(err == 0); | |
| 85 | ||
| 86 | close: | |
| 87 | uv_close((uv_handle_t *) &subprocess->handle, bare_subprocess__on_close); | |
| 88 | } | |
| 89 | ||
| 90 | static void | |
| 91 | bare_subprocess__on_teardown(js_deferred_teardown_t *handle, void *data) { | |
| 92 | int err; | |
| 93 | ||
| 94 | bare_subprocess_t *subprocess = (bare_subprocess_t *) data; | |
| 95 | ||
| 96 | subprocess->exiting = true; | |
| 97 | ||
| 98 | if (subprocess->killed) return; | |
| 99 | ||
| 100 | err = uv_process_kill(&subprocess->handle, SIGTERM); | |
| 101 | assert(err == 0); | |
| 102 | } | |
| 103 | ||
| 104 | static void | |
| 105 | bare_subprocess__on_alloc(uv_handle_t *handle, size_t suggested_size, uv_buf_t *buf) { | |
| 106 | bare_subprocess_buffered_pipe_t *pipe = (bare_subprocess_buffered_pipe_t *) handle; | |
| 107 | ||
| 108 | *buf = pipe->read; | |
| 109 | } | |
| 110 | ||
| 111 | static void | |
| 112 | bare_subprocess__on_read(uv_stream_t *stream, ssize_t nread, const uv_buf_t *buf) { | |
| 113 | if (nread == 0) return; | |
| 114 | ||
| 115 | if (nread == UV_EOF) { | |
| 116 | uv_close((uv_handle_t *) stream, NULL); | |
| 117 | } else { | |
| 118 | int err; | |
| 119 | ||
| 120 | bare_subprocess_buffered_pipe_t *pipe = (bare_subprocess_buffered_pipe_t *) stream; | |
| 121 | ||
| 122 | pipe->read.base += nread; | |
| 123 | pipe->read.len -= nread; | |
| 124 | ||
| 125 | pipe->written += nread; | |
| 126 | } | |
| 127 | } | |
| 128 | ||
| 129 | static js_value_t * | |
| 130 | bare_subprocess_init(js_env_t *env, js_callback_info_t *info) { | |
| 131 | int err; | |
| 132 | ||
| 133 | size_t argc = 2; | |
| 134 | js_value_t *argv[2]; | |
| 135 | ||
| 136 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 137 | assert(err == 0); | |
| 138 | ||
| 139 | assert(argc == 2); | |
| 140 | ||
| 141 | js_value_t *handle; | |
| 142 | ||
| 143 | bare_subprocess_t *subprocess; | |
| 144 | err = js_create_arraybuffer(env, sizeof(bare_subprocess_t), (void **) &subprocess, &handle); | |
| 145 | assert(err == 0); | |
| 146 | ||
| 147 | subprocess->env = env; | |
| 148 | subprocess->killed = false; | |
| 149 | subprocess->exiting = false; | |
| 150 | ||
| 151 | err = js_create_reference(env, argv[0], 1, &subprocess->ctx); | |
| 152 | assert(err == 0); | |
| 153 | ||
| 154 | err = js_create_reference(env, argv[1], 1, &subprocess->on_exit); | |
| 155 | assert(err == 0); | |
| 156 | ||
| 157 | err = js_add_deferred_teardown_callback(env, bare_subprocess__on_teardown, (void *) subprocess, &subprocess->teardown); | |
| 158 | assert(err == 0); | |
| 159 | ||
| 160 | return handle; | |
| 161 | } | |
| 162 | ||
| 163 | static js_value_t * | |
| 164 | bare_subprocess_spawn(js_env_t *env, js_callback_info_t *info) { | |
| 165 | int err; | |
| 166 | ||
| 167 | size_t argc = 11; | |
| 168 | js_value_t *argv[11]; | |
| 169 | ||
| 170 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 171 | assert(err == 0); | |
| 172 | ||
| 173 | assert(argc == 11); | |
| 174 | ||
| 175 | uv_loop_t *loop; | |
| 176 | err = js_get_env_loop(env, &loop); | |
| 177 | assert(err == 0); | |
| 178 | ||
| 179 | bare_subprocess_t *subprocess; | |
| 180 | err = js_get_arraybuffer_info(env, argv[0], (void **) &subprocess, NULL); | |
| 181 | assert(err == 0); | |
| 182 | ||
| 183 | bare_subprocess_path_t file; | |
| 184 | err = js_get_value_string_utf8(env, argv[1], file, sizeof(bare_subprocess_path_t), NULL); | |
| 185 | assert(err == 0); | |
| 186 | ||
| 187 | uint32_t args_len; | |
| 188 | err = js_get_array_length(env, argv[2], &args_len); | |
| 189 | assert(err == 0); | |
| 190 | ||
| 191 | utf8_t **args = calloc(1 /* file */ + args_len + 1 /* NULL */, sizeof(utf8_t *)); | |
| 192 | ||
| 193 | args[0] = file; | |
| 194 | ||
| 195 | for (uint32_t i = 0; i < args_len; i++) { | |
| 196 | js_value_t *value; | |
| 197 | err = js_get_element(env, argv[2], i, &value); | |
| 198 | assert(err == 0); | |
| 199 | ||
| 200 | size_t arg_len; | |
| 201 | err = js_get_value_string_utf8(env, value, NULL, 0, &arg_len); | |
| 202 | assert(err == 0); | |
| 203 | ||
| 204 | arg_len += 1 /* NULL */; | |
| 205 | ||
| 206 | utf8_t *arg = malloc(arg_len); | |
| 207 | err = js_get_value_string_utf8(env, value, arg, arg_len, NULL); | |
| 208 | assert(err == 0); | |
| 209 | ||
| 210 | args[i + 1] = arg; | |
| 211 | } | |
| 212 | ||
| 213 | bare_subprocess_path_t cwd; | |
| 214 | err = js_get_value_string_utf8(env, argv[3], cwd, sizeof(bare_subprocess_path_t), NULL); | |
| 215 | assert(err == 0); | |
| 216 | ||
| 217 | uint32_t pairs_len; | |
| 218 | err = js_get_array_length(env, argv[4], &pairs_len); | |
| 219 | assert(err == 0); | |
| 220 | ||
| 221 | utf8_t **pairs = calloc(pairs_len + 1 /* NULL */, sizeof(utf8_t *)); | |
| 222 | ||
| 223 | for (uint32_t i = 0; i < pairs_len; i++) { | |
| 224 | js_value_t *value; | |
| 225 | err = js_get_element(env, argv[4], i, &value); | |
| 226 | assert(err == 0); | |
| 227 | ||
| 228 | size_t pair_len; | |
| 229 | err = js_get_value_string_utf8(env, value, NULL, 0, &pair_len); | |
| 230 | assert(err == 0); | |
| 231 | ||
| 232 | pair_len += 1 /* NULL */; | |
| 233 | ||
| 234 | utf8_t *pair = malloc(pair_len); | |
| 235 | err = js_get_value_string_utf8(env, value, pair, pair_len, NULL); | |
| 236 | assert(err == 0); | |
| 237 | ||
| 238 | pairs[i] = pair; | |
| 239 | } | |
| 240 | ||
| 241 | uint32_t stdio_len; | |
| 242 | err = js_get_array_length(env, argv[5], &stdio_len); | |
| 243 | assert(err == 0); | |
| 244 | ||
| 245 | uv_stdio_container_t *stdio = malloc(stdio_len * sizeof(uv_stdio_container_t)); | |
| 246 | ||
| 247 | for (uint32_t i = 0; i < stdio_len; i++) { | |
| 248 | js_value_t *value; | |
| 249 | err = js_get_element(env, argv[5], i, &value); | |
| 250 | assert(err == 0); | |
| 251 | ||
| 252 | js_value_t *property; | |
| 253 | ||
| 254 | err = js_get_named_property(env, value, "flags", &property); | |
| 255 | assert(err == 0); | |
| 256 | ||
| 257 | uint32_t flags; | |
| 258 | err = js_get_value_uint32(env, property, &flags); | |
| 259 | assert(err == 0); | |
| 260 | ||
| 261 | stdio[i] = (uv_stdio_container_t) { | |
| 262 | .flags = flags, | |
| 263 | }; | |
| 264 | ||
| 265 | if (flags & UV_INHERIT_FD) { | |
| 266 | err = js_get_named_property(env, value, "fd", &property); | |
| 267 | assert(err == 0); | |
| 268 | ||
| 269 | uint32_t fd; | |
| 270 | err = js_get_value_uint32(env, property, &fd); | |
| 271 | assert(err == 0); | |
| 272 | ||
| 273 | stdio[i].data.fd = fd; | |
| 274 | } | |
| 275 | ||
| 276 | if (flags & UV_CREATE_PIPE) { | |
| 277 | err = js_get_named_property(env, value, "pipe", &property); | |
| 278 | assert(err == 0); | |
| 279 | ||
| 280 | uv_stream_t *pipe; | |
| 281 | err = js_get_arraybuffer_info(env, property, (void **) &pipe, NULL); | |
| 282 | assert(err == 0); | |
| 283 | ||
| 284 | stdio[i].data.stream = pipe; | |
| 285 | } | |
| 286 | } | |
| 287 | ||
| 288 | bool detached; | |
| 289 | err = js_get_value_bool(env, argv[6], &detached); | |
| 290 | assert(err == 0); | |
| 291 | ||
| 292 | int32_t uid; | |
| 293 | err = js_get_value_int32(env, argv[7], &uid); | |
| 294 | assert(err == 0); | |
| 295 | ||
| 296 | int32_t gid; | |
| 297 | err = js_get_value_int32(env, argv[8], &gid); | |
| 298 | assert(err == 0); | |
| 299 | ||
| 300 | bool windows_hide; | |
| 301 | err = js_get_value_bool(env, argv[9], &windows_hide); | |
| 302 | assert(err == 0); | |
| 303 | ||
| 304 | bool windows_verbatim_arguments; | |
| 305 | err = js_get_value_bool(env, argv[10], &windows_verbatim_arguments); | |
| 306 | assert(err == 0); | |
| 307 | ||
| 308 | int flags = UV_PROCESS_WINDOWS_HIDE_CONSOLE; | |
| 309 | ||
| 310 | if (detached) flags |= UV_PROCESS_DETACHED; | |
| 311 | if (uid != -1) flags |= UV_PROCESS_SETUID; | |
| 312 | if (gid != -1) flags |= UV_PROCESS_SETGID; | |
| 313 | if (windows_hide) flags |= UV_PROCESS_WINDOWS_HIDE; | |
| 314 | if (windows_verbatim_arguments) flags |= UV_PROCESS_WINDOWS_VERBATIM_ARGUMENTS; | |
| 315 | ||
| 316 | uv_process_options_t opts = { | |
| 317 | .exit_cb = bare_subprocess__on_exit, | |
| 318 | .file = (char *) file, | |
| 319 | .args = (char **) args, | |
| 320 | .env = (char **) pairs, | |
| 321 | .cwd = (char *) cwd, | |
| 322 | .flags = flags, | |
| 323 | .stdio_count = stdio_len, | |
| 324 | .stdio = stdio, | |
| 325 | .uid = uid, | |
| 326 | .gid = gid, | |
| 327 | }; | |
| 328 | ||
| 329 | err = uv_spawn(loop, &subprocess->handle, &opts); | |
| 330 | ||
| 331 | js_value_t *pid = NULL; | |
| 332 | ||
| 333 | if (err < 0) { | |
| 334 | err = js_throw_error(env, uv_err_name(err), uv_strerror(err)); | |
| 335 | assert(err == 0); | |
| 336 | } else { | |
| 337 | err = js_create_uint32(env, subprocess->handle.pid, &pid); | |
| 338 | assert(err == 0); | |
| 339 | } | |
| 340 | ||
| 341 | for (uint32_t i = 0; i < args_len; i++) { | |
| 342 | free(args[i + 1]); | |
| 343 | } | |
| 344 | ||
| 345 | for (uint32_t i = 0; i < pairs_len; i++) { | |
| 346 | free(pairs[i]); | |
| 347 | } | |
| 348 | ||
| 349 | free(args); | |
| 350 | free(pairs); | |
| 351 | free(stdio); | |
| 352 | ||
| 353 | return pid; | |
| 354 | } | |
| 355 | ||
| 356 | static js_value_t * | |
| 357 | bare_subprocess_spawn_sync(js_env_t *env, js_callback_info_t *info) { | |
| 358 | int err; | |
| 359 | ||
| 360 | size_t argc = 11; | |
| 361 | js_value_t *argv[11]; | |
| 362 | ||
| 363 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 364 | assert(err == 0); | |
| 365 | ||
| 366 | assert(argc == 11); | |
| 367 | ||
| 368 | uv_loop_t loop; | |
| 369 | err = uv_loop_init(&loop); | |
| 370 | assert(err == 0); | |
| 371 | ||
| 372 | bare_subprocess_t *subprocess; | |
| 373 | err = js_get_arraybuffer_info(env, argv[0], (void **) &subprocess, NULL); | |
| 374 | assert(err == 0); | |
| 375 | ||
| 376 | bare_subprocess_path_t file; | |
| 377 | err = js_get_value_string_utf8(env, argv[1], file, sizeof(bare_subprocess_path_t), NULL); | |
| 378 | assert(err == 0); | |
| 379 | ||
| 380 | uint32_t args_len; | |
| 381 | err = js_get_array_length(env, argv[2], &args_len); | |
| 382 | assert(err == 0); | |
| 383 | ||
| 384 | utf8_t **args = calloc(1 /* file */ + args_len + 1 /* NULL */, sizeof(utf8_t *)); | |
| 385 | ||
| 386 | args[0] = file; | |
| 387 | ||
| 388 | for (uint32_t i = 0; i < args_len; i++) { | |
| 389 | js_value_t *value; | |
| 390 | err = js_get_element(env, argv[2], i, &value); | |
| 391 | assert(err == 0); | |
| 392 | ||
| 393 | size_t arg_len; | |
| 394 | err = js_get_value_string_utf8(env, value, NULL, 0, &arg_len); | |
| 395 | assert(err == 0); | |
| 396 | ||
| 397 | arg_len += 1 /* NULL */; | |
| 398 | ||
| 399 | utf8_t *arg = malloc(arg_len); | |
| 400 | err = js_get_value_string_utf8(env, value, arg, arg_len, NULL); | |
| 401 | assert(err == 0); | |
| 402 | ||
| 403 | args[i + 1] = arg; | |
| 404 | } | |
| 405 | ||
| 406 | bare_subprocess_path_t cwd; | |
| 407 | err = js_get_value_string_utf8(env, argv[3], cwd, sizeof(bare_subprocess_path_t), NULL); | |
| 408 | assert(err == 0); | |
| 409 | ||
| 410 | uint32_t pairs_len; | |
| 411 | err = js_get_array_length(env, argv[4], &pairs_len); | |
| 412 | assert(err == 0); | |
| 413 | ||
| 414 | utf8_t **pairs = calloc(pairs_len + 1 /* NULL */, sizeof(utf8_t *)); | |
| 415 | ||
| 416 | for (uint32_t i = 0; i < pairs_len; i++) { | |
| 417 | js_value_t *value; | |
| 418 | err = js_get_element(env, argv[4], i, &value); | |
| 419 | assert(err == 0); | |
| 420 | ||
| 421 | size_t pair_len; | |
| 422 | err = js_get_value_string_utf8(env, value, NULL, 0, &pair_len); | |
| 423 | assert(err == 0); | |
| 424 | ||
| 425 | pair_len += 1 /* NULL */; | |
| 426 | ||
| 427 | utf8_t *pair = malloc(pair_len); | |
| 428 | err = js_get_value_string_utf8(env, value, pair, pair_len, NULL); | |
| 429 | assert(err == 0); | |
| 430 | ||
| 431 | pairs[i] = pair; | |
| 432 | } | |
| 433 | ||
| 434 | uint32_t stdio_len; | |
| 435 | err = js_get_array_length(env, argv[5], &stdio_len); | |
| 436 | assert(err == 0); | |
| 437 | ||
| 438 | uv_stdio_container_t *stdio = malloc(stdio_len * sizeof(uv_stdio_container_t)); | |
| 439 | ||
| 440 | for (uint32_t i = 0; i < stdio_len; i++) { | |
| 441 | js_value_t *value; | |
| 442 | err = js_get_element(env, argv[5], i, &value); | |
| 443 | assert(err == 0); | |
| 444 | ||
| 445 | js_value_t *property; | |
| 446 | ||
| 447 | err = js_get_named_property(env, value, "flags", &property); | |
| 448 | assert(err == 0); | |
| 449 | ||
| 450 | uint32_t flags; | |
| 451 | err = js_get_value_uint32(env, property, &flags); | |
| 452 | assert(err == 0); | |
| 453 | ||
| 454 | stdio[i] = (uv_stdio_container_t) { | |
| 455 | .flags = flags, | |
| 456 | }; | |
| 457 | ||
| 458 | if (flags & UV_INHERIT_FD) { | |
| 459 | err = js_get_named_property(env, value, "fd", &property); | |
| 460 | assert(err == 0); | |
| 461 | ||
| 462 | uint32_t fd; | |
| 463 | err = js_get_value_uint32(env, property, &fd); | |
| 464 | assert(err == 0); | |
| 465 | ||
| 466 | stdio[i].data.fd = fd; | |
| 467 | } | |
| 468 | ||
| 469 | if (flags & UV_CREATE_PIPE) { | |
| 470 | err = js_get_named_property(env, value, "buffer", &property); | |
| 471 | assert(err == 0); | |
| 472 | ||
| 473 | bare_subprocess_buffered_pipe_t *pipe = malloc(sizeof(bare_subprocess_buffered_pipe_t)); | |
| 474 | ||
| 475 | pipe->written = 0; | |
| 476 | ||
| 477 | err = uv_pipe_init(&loop, (uv_pipe_t *) pipe, false); | |
| 478 | assert(err == 0); | |
| 479 | ||
| 480 | err = js_get_typedarray_info(env, property, NULL, (void **) &pipe->read.base, (size_t *) &pipe->read.len, NULL, NULL); | |
| 481 | assert(err == 0); | |
| 482 | ||
| 483 | stdio[i].data.stream = (uv_stream_t *) pipe; | |
| 484 | } | |
| 485 | } | |
| 486 | ||
| 487 | bool detached; | |
| 488 | err = js_get_value_bool(env, argv[6], &detached); | |
| 489 | assert(err == 0); | |
| 490 | ||
| 491 | int32_t uid; | |
| 492 | err = js_get_value_int32(env, argv[7], &uid); | |
| 493 | assert(err == 0); | |
| 494 | ||
| 495 | int32_t gid; | |
| 496 | err = js_get_value_int32(env, argv[8], &gid); | |
| 497 | assert(err == 0); | |
| 498 | ||
| 499 | bool windows_hide; | |
| 500 | err = js_get_value_bool(env, argv[9], &windows_hide); | |
| 501 | assert(err == 0); | |
| 502 | ||
| 503 | bool windows_verbatim_arguments; | |
| 504 | err = js_get_value_bool(env, argv[10], &windows_verbatim_arguments); | |
| 505 | assert(err == 0); | |
| 506 | ||
| 507 | int flags = UV_PROCESS_WINDOWS_HIDE_CONSOLE; | |
| 508 | ||
| 509 | if (detached) flags |= UV_PROCESS_DETACHED; | |
| 510 | if (uid != -1) flags |= UV_PROCESS_SETUID; | |
| 511 | if (gid != -1) flags |= UV_PROCESS_SETGID; | |
| 512 | if (windows_hide) flags |= UV_PROCESS_WINDOWS_HIDE; | |
| 513 | if (windows_verbatim_arguments) flags |= UV_PROCESS_WINDOWS_VERBATIM_ARGUMENTS; | |
| 514 | ||
| 515 | uv_process_options_t opts = { | |
| 516 | .exit_cb = bare_subprocess__on_exit, | |
| 517 | .file = (char *) file, | |
| 518 | .args = (char **) args, | |
| 519 | .env = (char **) pairs, | |
| 520 | .cwd = (char *) cwd, | |
| 521 | .flags = flags, | |
| 522 | .stdio_count = stdio_len, | |
| 523 | .stdio = stdio, | |
| 524 | .uid = uid, | |
| 525 | .gid = gid, | |
| 526 | }; | |
| 527 | ||
| 528 | err = uv_spawn(&loop, &subprocess->handle, &opts); | |
| 529 | ||
| 530 | js_value_t *pid = NULL; | |
| 531 | ||
| 532 | int throw = err; | |
| 533 | ||
| 534 | if (throw < 0) { | |
| 535 | uv_close((uv_handle_t *) &subprocess->handle, bare_subprocess__on_close); | |
| 536 | ||
| 537 | for (uint32_t i = 0; i < stdio_len; i++) { | |
| 538 | if (stdio[i].flags & UV_CREATE_PIPE) { | |
| 539 | bare_subprocess_buffered_pipe_t *pipe = (bare_subprocess_buffered_pipe_t *) stdio[i].data.stream; | |
| 540 | ||
| 541 | uv_close((uv_handle_t *) pipe, NULL); | |
| 542 | } | |
| 543 | } | |
| 544 | } else { | |
| 545 | for (uint32_t i = 0; i < stdio_len; i++) { | |
| 546 | if (stdio[i].flags & UV_CREATE_PIPE) { | |
| 547 | bare_subprocess_buffered_pipe_t *pipe = (bare_subprocess_buffered_pipe_t *) stdio[i].data.stream; | |
| 548 | ||
| 549 | err = uv_read_start((uv_stream_t *) pipe, bare_subprocess__on_alloc, bare_subprocess__on_read); | |
| 550 | assert(err == 0); | |
| 551 | } | |
| 552 | } | |
| 553 | ||
| 554 | err = js_create_uint32(env, subprocess->handle.pid, &pid); | |
| 555 | assert(err == 0); | |
| 556 | } | |
| 557 | ||
| 558 | err = uv_run(&loop, UV_RUN_DEFAULT); | |
| 559 | assert(err == 0); | |
| 560 | ||
| 561 | for (uint32_t i = 0; i < stdio_len; i++) { | |
| 562 | js_value_t *value; | |
| 563 | err = js_get_element(env, argv[5], i, &value); | |
| 564 | assert(err == 0); | |
| 565 | ||
| 566 | js_value_t *property; | |
| 567 | ||
| 568 | if (stdio[i].flags & UV_CREATE_PIPE) { | |
| 569 | bare_subprocess_buffered_pipe_t *pipe = (bare_subprocess_buffered_pipe_t *) stdio[i].data.stream; | |
| 570 | ||
| 571 | js_value_t *written; | |
| 572 | err = js_create_int64(env, pipe->written, &written); | |
| 573 | assert(err == 0); | |
| 574 | ||
| 575 | err = js_set_named_property(env, value, "written", written); | |
| 576 | assert(err == 0); | |
| 577 | ||
| 578 | free(pipe); | |
| 579 | } | |
| 580 | } | |
| 581 | ||
| 582 | err = uv_loop_close(&loop); | |
| 583 | assert(err == 0); | |
| 584 | ||
| 585 | for (uint32_t i = 0; i < args_len; i++) { | |
| 586 | free(args[i + 1]); | |
| 587 | } | |
| 588 | ||
| 589 | for (uint32_t i = 0; i < pairs_len; i++) { | |
| 590 | free(pairs[i]); | |
| 591 | } | |
| 592 | ||
| 593 | free(args); | |
| 594 | free(pairs); | |
| 595 | free(stdio); | |
| 596 | ||
| 597 | if (throw < 0) { | |
| 598 | err = js_throw_error(env, uv_err_name(throw), uv_strerror(throw)); | |
| 599 | assert(err == 0); | |
| 600 | ||
| 601 | return NULL; | |
| 602 | } | |
| 603 | ||
| 604 | return pid; | |
| 605 | } | |
| 606 | ||
| 607 | static js_value_t * | |
| 608 | bare_subprocess_kill(js_env_t *env, js_callback_info_t *info) { | |
| 609 | int err; | |
| 610 | ||
| 611 | size_t argc = 2; | |
| 612 | js_value_t *argv[2]; | |
| 613 | ||
| 614 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 615 | assert(err == 0); | |
| 616 | ||
| 617 | assert(argc == 2); | |
| 618 | ||
| 619 | bare_subprocess_t *subprocess; | |
| 620 | err = js_get_arraybuffer_info(env, argv[0], (void **) &subprocess, NULL); | |
| 621 | assert(err == 0); | |
| 622 | ||
| 623 | if (subprocess->killed) return NULL; | |
| 624 | ||
| 625 | uint32_t signum; | |
| 626 | err = js_get_value_uint32(env, argv[1], &signum); | |
| 627 | assert(err == 0); | |
| 628 | ||
| 629 | subprocess->killed = true; | |
| 630 | ||
| 631 | err = uv_process_kill(&subprocess->handle, signum); | |
| 632 | ||
| 633 | if (err < 0) { | |
| 634 | err = js_throw_error(env, uv_err_name(err), uv_strerror(err)); | |
| 635 | assert(err == 0); | |
| 636 | } | |
| 637 | ||
| 638 | return NULL; | |
| 639 | } | |
| 640 | ||
| 641 | static js_value_t * | |
| 642 | bare_subprocess_ref(js_env_t *env, js_callback_info_t *info) { | |
| 643 | int err; | |
| 644 | ||
| 645 | size_t argc = 1; | |
| 646 | js_value_t *argv[1]; | |
| 647 | ||
| 648 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 649 | assert(err == 0); | |
| 650 | ||
| 651 | assert(argc == 1); | |
| 652 | ||
| 653 | bare_subprocess_t *subprocess; | |
| 654 | err = js_get_arraybuffer_info(env, argv[0], (void **) &subprocess, NULL); | |
| 655 | assert(err == 0); | |
| 656 | ||
| 657 | uv_ref((uv_handle_t *) &subprocess->handle); | |
| 658 | ||
| 659 | return NULL; | |
| 660 | } | |
| 661 | ||
| 662 | static js_value_t * | |
| 663 | bare_subprocess_unref(js_env_t *env, js_callback_info_t *info) { | |
| 664 | int err; | |
| 665 | ||
| 666 | size_t argc = 1; | |
| 667 | js_value_t *argv[1]; | |
| 668 | ||
| 669 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 670 | assert(err == 0); | |
| 671 | ||
| 672 | assert(argc == 1); | |
| 673 | ||
| 674 | bare_subprocess_t *subprocess; | |
| 675 | err = js_get_arraybuffer_info(env, argv[0], (void **) &subprocess, NULL); | |
| 676 | assert(err == 0); | |
| 677 | ||
| 678 | uv_unref((uv_handle_t *) &subprocess->handle); | |
| 679 | ||
| 680 | return NULL; | |
| 681 | } | |
| 682 | ||
| 683 | static js_value_t * | |
| 684 | bare_subprocess_exports(js_env_t *env, js_value_t *exports) { | |
| 685 | int err; | |
| 686 | ||
| 687 | #define V(name, fn) \ | |
| 688 | { \ | |
| 689 | js_value_t *val; \ | |
| 690 | err = js_create_function(env, name, -1, fn, NULL, &val); \ | |
| 691 | assert(err == 0); \ | |
| 692 | err = js_set_named_property(env, exports, name, val); \ | |
| 693 | assert(err == 0); \ | |
| 694 | } | |
| 695 | ||
| 696 | V("init", bare_subprocess_init) | |
| 697 | V("spawn", bare_subprocess_spawn) | |
| 698 | V("spawnSync", bare_subprocess_spawn_sync) | |
| 699 | V("kill", bare_subprocess_kill) | |
| 700 | V("ref", bare_subprocess_ref) | |
| 701 | V("unref", bare_subprocess_unref) | |
| 702 | #undef V | |
| 703 | ||
| 704 | #define V(name) \ | |
| 705 | { \ | |
| 706 | js_value_t *val; \ | |
| 707 | err = js_create_uint32(env, name, &val); \ | |
| 708 | assert(err == 0); \ | |
| 709 | err = js_set_named_property(env, exports, #name, val); \ | |
| 710 | assert(err == 0); \ | |
| 711 | } | |
| 712 | ||
| 713 | V(UV_IGNORE) | |
| 714 | V(UV_CREATE_PIPE) | |
| 715 | V(UV_INHERIT_FD) | |
| 716 | V(UV_INHERIT_STREAM) | |
| 717 | V(UV_READABLE_PIPE) | |
| 718 | V(UV_WRITABLE_PIPE) | |
| 719 | V(UV_NONBLOCK_PIPE) | |
| 720 | #undef V | |
| 721 | ||
| 722 | return exports; | |
| 723 | } | |
| 724 | ||
| 725 | BARE_MODULE(bare_subprocess, bare_subprocess_exports) |
| … | … | @@ -0,0 +1 @@ |
| 1 | module.exports = require.addon() |
| … | … | @@ -0,0 +1,79 @@ |
| 1 | import EventEmitter, { EventMap } from 'bare-events' | |
| 2 | import Buffer from 'bare-buffer' | |
| 3 | import Pipe from 'bare-pipe' | |
| 4 | import constants from './lib/constants' | |
| 5 | import errors from './lib/errors' | |
| 6 | ||
| 7 | export { constants, errors } | |
| 8 | ||
| 9 | export interface SubprocessEvents extends EventMap { | |
| 10 | exit: [code: number, signalCode: string] | |
| 11 | } | |
| 12 | ||
| 13 | export type IO = 'inherit' | 'pipe' | 'overlapped' | 'ignore' | |
| 14 | ||
| 15 | export interface Subprocess<M extends SubprocessEvents = SubprocessEvents> extends EventEmitter<M> { | |
| 16 | readonly exitCode: number | null | |
| 17 | readonly killed: boolean | |
| 18 | readonly pid: number | |
| 19 | readonly signalCode: string | null | |
| 20 | readonly spawnargs: string[] | |
| 21 | readonly spawnfile: string | |
| 22 | readonly stdio: (Pipe | null)[] | |
| 23 | readonly stdin: Pipe | null | |
| 24 | readonly stdout: Pipe | null | |
| 25 | readonly stderr: Pipe | null | |
| 26 | ||
| 27 | ref(): void | |
| 28 | unref(): void | |
| 29 | ||
| 30 | kill(signum?: number): void | |
| 31 | } | |
| 32 | ||
| 33 | export class Subprocess {} | |
| 34 | ||
| 35 | export interface SpawnOptions { | |
| 36 | cwd?: string | |
| 37 | stdio?: [stdin?: IO, stdout?: IO, stderr?: IO, ...fds: IO[]] | IO | null | |
| 38 | shell?: boolean | string | |
| 39 | detached?: boolean | |
| 40 | uid?: number | |
| 41 | gid?: number | |
| 42 | env?: Record<string, string> | |
| 43 | windowsHide?: boolean | |
| 44 | windowsVerbatimArguments?: boolean | |
| 45 | } | |
| 46 | ||
| 47 | export function spawn(file: string, args?: string[] | null, opts?: SpawnOptions): Subprocess | |
| 48 | ||
| 49 | export function spawn(file: string, opts?: SpawnOptions): Subprocess | |
| 50 | ||
| 51 | export interface SpawnSyncOptions extends SpawnOptions { | |
| 52 | input?: string | Buffer | |
| 53 | maxBuffer?: number | |
| 54 | } | |
| 55 | ||
| 56 | export function spawnSync( | |
| 57 | file: string, | |
| 58 | args?: string[] | null, | |
| 59 | opts?: SpawnSyncOptions | |
| 60 | ): { | |
| 61 | output: (Buffer | null)[] | |
| 62 | pid: number | |
| 63 | signal: number | |
| 64 | status: number | |
| 65 | stderr: Buffer | null | |
| 66 | stdout: Buffer | null | |
| 67 | } | |
| 68 | ||
| 69 | export function spawnSync( | |
| 70 | file: string, | |
| 71 | opts?: SpawnSyncOptions | |
| 72 | ): { | |
| 73 | output: (Buffer | null)[] | |
| 74 | pid: number | |
| 75 | signal: number | |
| 76 | status: number | |
| 77 | stderr: Buffer | null | |
| 78 | stdout: Buffer | null | |
| 79 | } |
| … | … | @@ -0,0 +1,369 @@ |
| 1 | const EventEmitter = require('bare-events') | |
| 2 | const Pipe = require('bare-pipe') | |
| 3 | const os = require('bare-os') | |
| 4 | const env = require('bare-env') | |
| 5 | const { isURL, fileURLToPath } = require('bare-url') | |
| 6 | const binding = require('./binding') | |
| 7 | const constants = require('./lib/constants') | |
| 8 | const errors = require('./lib/errors') | |
| 9 | ||
| 10 | exports.Subprocess = class Subprocess extends EventEmitter { | |
| 11 | constructor() { | |
| 12 | super() | |
| 13 | ||
| 14 | this.spawnfile = null | |
| 15 | this.spawnargs = [] | |
| 16 | this.pid = null | |
| 17 | this.stdio = [] | |
| 18 | this.exitCode = null | |
| 19 | this.signalCode = null | |
| 20 | this.killed = false | |
| 21 | ||
| 22 | this._closing = [] | |
| 23 | this._handle = binding.init(this, this._onexit) | |
| 24 | } | |
| 25 | ||
| 26 | _flush() { | |
| 27 | for (const pipe of this.stdio) { | |
| 28 | if (pipe && typeof pipe.resume === 'function') pipe.resume() | |
| 29 | } | |
| 30 | } | |
| 31 | ||
| 32 | async _onexit(code, signal) { | |
| 33 | this.exitCode = code | |
| 34 | this.signalCode = signal | |
| 35 | ||
| 36 | this.emit('exit', code, signal) | |
| 37 | ||
| 38 | queueMicrotask(this._flush.bind(this)) // Defer to provide a last chance to consume | |
| 39 | ||
| 40 | await Promise.all(this._closing) | |
| 41 | ||
| 42 | this.emit('close', code, signal) | |
| 43 | } | |
| 44 | ||
| 45 | get stdin() { | |
| 46 | return this.stdio[0] || null | |
| 47 | } | |
| 48 | ||
| 49 | get stdout() { | |
| 50 | return this.stdio[1] || null | |
| 51 | } | |
| 52 | ||
| 53 | get stderr() { | |
| 54 | return this.stdio[2] || null | |
| 55 | } | |
| 56 | ||
| 57 | ref() { | |
| 58 | for (const pipe of this.stdio) { | |
| 59 | if (pipe && typeof pipe.ref === 'function') pipe.ref() | |
| 60 | } | |
| 61 | ||
| 62 | binding.ref(this._handle) | |
| 63 | } | |
| 64 | ||
| 65 | unref() { | |
| 66 | for (const pipe of this.stdio) { | |
| 67 | if (pipe && typeof pipe.unref === 'function') pipe.unref() | |
| 68 | } | |
| 69 | ||
| 70 | binding.unref(this._handle) | |
| 71 | } | |
| 72 | ||
| 73 | kill(signum = constants.SIGTERM) { | |
| 74 | if (typeof signum === 'string') { | |
| 75 | if (signum in constants === false) { | |
| 76 | throw errors.UNKNOWN_SIGNAL('Unknown signal: ' + signum) | |
| 77 | } | |
| 78 | ||
| 79 | signum = constants[signum] | |
| 80 | } | |
| 81 | ||
| 82 | binding.kill(this._handle, signum) | |
| 83 | ||
| 84 | this.killed = true | |
| 85 | } | |
| 86 | } | |
| 87 | ||
| 88 | exports.ChildProcess = exports.Subprocess // For Node.js compatibility | |
| 89 | ||
| 90 | exports.constants = constants | |
| 91 | exports.errors = errors | |
| 92 | ||
| 93 | exports.spawn = function spawn(file, args, opts) { | |
| 94 | if (Array.isArray(args)) { | |
| 95 | args = [...args] | |
| 96 | } else if (args === null) { | |
| 97 | args = [] | |
| 98 | } else { | |
| 99 | opts = args | |
| 100 | args = [] | |
| 101 | } | |
| 102 | ||
| 103 | if (!opts) opts = {} | |
| 104 | ||
| 105 | args = args.map(String) | |
| 106 | ||
| 107 | let { | |
| 108 | cwd = defaultCwd(), | |
| 109 | env = defaultEnv(), | |
| 110 | stdio = [], | |
| 111 | detached = false, | |
| 112 | shell = false, | |
| 113 | uid = -1, | |
| 114 | gid = -1, | |
| 115 | windowsHide = false, | |
| 116 | windowsVerbatimArguments = false | |
| 117 | } = opts | |
| 118 | ||
| 119 | file = toPath(file) | |
| 120 | cwd = toPath(cwd) | |
| 121 | ||
| 122 | if (shell) { | |
| 123 | const command = [file, ...args].join(' ') | |
| 124 | ||
| 125 | if (Bare.platform === 'win32') { | |
| 126 | if (typeof shell === 'string') { | |
| 127 | file = shell | |
| 128 | } else { | |
| 129 | file = env.comspec || 'cmd.exe' | |
| 130 | } | |
| 131 | ||
| 132 | if (/^(.*\\)?cmd(\.exe)?$/i.test(file)) { | |
| 133 | args = ['/d', '/s', '/c', `"${command}"`] | |
| 134 | windowsVerbatimArguments = true | |
| 135 | } else { | |
| 136 | args = ['-c', command] | |
| 137 | } | |
| 138 | } else { | |
| 139 | if (typeof shell === 'string') { | |
| 140 | file = shell | |
| 141 | } else if (Bare.platform === 'android') { | |
| 142 | file = '/system/bin/sh' | |
| 143 | } else { | |
| 144 | file = '/bin/sh' | |
| 145 | } | |
| 146 | ||
| 147 | args = ['-c', command] | |
| 148 | } | |
| 149 | } | |
| 150 | ||
| 151 | const pairs = [] | |
| 152 | ||
| 153 | for (const [key, value] of Object.entries(env)) { | |
| 154 | pairs.push(`${key}=${value}`) | |
| 155 | } | |
| 156 | ||
| 157 | if (Array.isArray(stdio)) { | |
| 158 | stdio = [...stdio] | |
| 159 | } else if (typeof stdio === 'string') { | |
| 160 | stdio = [stdio, stdio, stdio] | |
| 161 | } else { | |
| 162 | stdio = [] | |
| 163 | } | |
| 164 | ||
| 165 | const subprocess = new exports.Subprocess() | |
| 166 | ||
| 167 | subprocess.spawnfile = file | |
| 168 | subprocess.spawnargs = args | |
| 169 | ||
| 170 | for (let i = 0, n = Math.max(3, stdio.length); i < n; i++) { | |
| 171 | subprocess.stdio[i] = null | |
| 172 | ||
| 173 | let fd = stdio[i] || 'pipe' | |
| 174 | ||
| 175 | if (fd === 'inherit') fd = i < 3 ? i : 'ignore' | |
| 176 | ||
| 177 | if (fd === 'ignore') { | |
| 178 | stdio[i] = { flags: binding.UV_IGNORE } | |
| 179 | } else if (fd === 'pipe' || fd === 'overlapped') { | |
| 180 | const pipe = new Pipe() | |
| 181 | ||
| 182 | pipe._onspawn(i !== 0 /* Readable */, i === 0 || i > 2 /* Writable */) | |
| 183 | ||
| 184 | if (i > 0) { | |
| 185 | subprocess._closing.push(new Promise((resolve) => pipe.once('close', resolve))) | |
| 186 | } | |
| 187 | ||
| 188 | let flags = binding.UV_CREATE_PIPE | binding.UV_READABLE_PIPE | binding.UV_WRITABLE_PIPE | |
| 189 | ||
| 190 | if (fd === 'overlapped') flags |= binding.UV_NONBLOCK_PIPE | |
| 191 | ||
| 192 | stdio[i] = { flags, pipe: pipe._handle } | |
| 193 | ||
| 194 | subprocess.stdio[i] = pipe | |
| 195 | } else { | |
| 196 | stdio[i] = { flags: binding.UV_INHERIT_FD, fd } | |
| 197 | } | |
| 198 | } | |
| 199 | ||
| 200 | subprocess.pid = binding.spawn( | |
| 201 | subprocess._handle, | |
| 202 | file, | |
| 203 | args, | |
| 204 | cwd, | |
| 205 | pairs, | |
| 206 | stdio, | |
| 207 | detached, | |
| 208 | uid, | |
| 209 | gid, | |
| 210 | windowsHide, | |
| 211 | windowsVerbatimArguments | |
| 212 | ) | |
| 213 | ||
| 214 | return subprocess | |
| 215 | } | |
| 216 | ||
| 217 | exports.spawnSync = function spawn(file, args, opts) { | |
| 218 | if (Array.isArray(args)) { | |
| 219 | args = [...args] | |
| 220 | } else if (args === null) { | |
| 221 | args = [] | |
| 222 | } else { | |
| 223 | opts = args | |
| 224 | args = [] | |
| 225 | } | |
| 226 | ||
| 227 | if (!opts) opts = {} | |
| 228 | ||
| 229 | let { | |
| 230 | cwd = defaultCwd(), | |
| 231 | env = defaultEnv(), | |
| 232 | input = null, | |
| 233 | stdio = [], | |
| 234 | detached = false, | |
| 235 | shell = false, | |
| 236 | uid = -1, | |
| 237 | gid = -1, | |
| 238 | windowsHide = false, | |
| 239 | windowsVerbatimArguments = false, | |
| 240 | maxBuffer = 1024 * 1024 | |
| 241 | } = opts | |
| 242 | ||
| 243 | file = toPath(file) | |
| 244 | cwd = toPath(cwd) | |
| 245 | ||
| 246 | if (shell) { | |
| 247 | const command = [file, ...args].join(' ') | |
| 248 | ||
| 249 | if (Bare.platform === 'win32') { | |
| 250 | if (typeof shell === 'string') { | |
| 251 | file = shell | |
| 252 | } else { | |
| 253 | file = env.comspec || 'cmd.exe' | |
| 254 | } | |
| 255 | ||
| 256 | if (/^(.*\\)?cmd(\.exe)?$/i.test(file)) { | |
| 257 | args = ['/d', '/s', '/c', `"${command}"`] | |
| 258 | windowsVerbatimArguments = true | |
| 259 | } else { | |
| 260 | args = ['-c', command] | |
| 261 | } | |
| 262 | } else { | |
| 263 | if (typeof shell === 'string') { | |
| 264 | file = shell | |
| 265 | } else if (Bare.platform === 'android') { | |
| 266 | file = '/system/bin/sh' | |
| 267 | } else { | |
| 268 | file = '/bin/sh' | |
| 269 | } | |
| 270 | ||
| 271 | args = ['-c', command] | |
| 272 | } | |
| 273 | } | |
| 274 | ||
| 275 | const pairs = [] | |
| 276 | ||
| 277 | for (const [key, value] of Object.entries(env)) { | |
| 278 | pairs.push(`${key}=${value}`) | |
| 279 | } | |
| 280 | ||
| 281 | if (Array.isArray(stdio)) { | |
| 282 | stdio = [...stdio] | |
| 283 | } else if (typeof stdio === 'string') { | |
| 284 | stdio = [stdio, stdio, stdio] | |
| 285 | } else { | |
| 286 | stdio = [] | |
| 287 | } | |
| 288 | ||
| 289 | const subprocess = new exports.Subprocess() | |
| 290 | ||
| 291 | if (input) { | |
| 292 | stdio[0] = { | |
| 293 | flags: binding.UV_CREATE_PIPE | binding.UV_READABLE_PIPE, | |
| 294 | buffer: input | |
| 295 | } | |
| 296 | ||
| 297 | subprocess.stdio[0] = null | |
| 298 | } | |
| 299 | ||
| 300 | for (let i = input ? 1 : 0, n = Math.max(3, stdio.length); i < n; i++) { | |
| 301 | subprocess.stdio[i] = null | |
| 302 | ||
| 303 | let fd = stdio[i] || 'pipe' | |
| 304 | ||
| 305 | if (fd === 'inherit') fd = i < 3 ? i : 'ignore' | |
| 306 | ||
| 307 | if (fd === 'ignore') { | |
| 308 | stdio[i] = { flags: binding.UV_IGNORE } | |
| 309 | } else if (fd === 'pipe') { | |
| 310 | const buffer = Buffer.alloc(maxBuffer) | |
| 311 | ||
| 312 | stdio[i] = { | |
| 313 | flags: binding.UV_CREATE_PIPE | binding.UV_WRITABLE_PIPE, | |
| 314 | buffer, | |
| 315 | written: 0 | |
| 316 | } | |
| 317 | ||
| 318 | if (i > 0) subprocess.stdio[i] = buffer | |
| 319 | } else if (typeof fd === 'number') { | |
| 320 | stdio[i] = { flags: binding.UV_INHERIT_FD, fd } | |
| 321 | } | |
| 322 | } | |
| 323 | ||
| 324 | subprocess.pid = binding.spawnSync( | |
| 325 | subprocess._handle, | |
| 326 | file, | |
| 327 | args, | |
| 328 | cwd, | |
| 329 | pairs, | |
| 330 | stdio, | |
| 331 | detached, | |
| 332 | uid, | |
| 333 | gid, | |
| 334 | windowsHide, | |
| 335 | windowsVerbatimArguments | |
| 336 | ) | |
| 337 | ||
| 338 | for (let i = 1, n = stdio.length; i < n; i++) { | |
| 339 | if (stdio[i].flags & binding.UV_WRITABLE_PIPE) { | |
| 340 | subprocess.stdio[i] = subprocess.stdio[i].subarray(0, stdio[i].written) | |
| 341 | } | |
| 342 | } | |
| 343 | ||
| 344 | return { | |
| 345 | status: subprocess.exitCode, | |
| 346 | signal: subprocess.signalCode, | |
| 347 | output: subprocess.stdio, | |
| 348 | pid: subprocess.pid, | |
| 349 | stdout: subprocess.stdout, | |
| 350 | stderr: subprocess.stderr | |
| 351 | } | |
| 352 | } | |
| 353 | ||
| 354 | function defaultCwd() { | |
| 355 | return os.cwd() | |
| 356 | } | |
| 357 | ||
| 358 | function defaultEnv() { | |
| 359 | return env | |
| 360 | } | |
| 361 | ||
| 362 | function toPath(filepath) { | |
| 363 | if (typeof filepath !== 'string') { | |
| 364 | if (isURL(filepath)) filepath = fileURLToPath(filepath) | |
| 365 | else filepath = filepath.toString() | |
| 366 | } | |
| 367 | ||
| 368 | return filepath | |
| 369 | } |
| … | … | @@ -0,0 +1,3 @@ |
| 1 | import os from 'bare-os' | |
| 2 | ||
| 3 | export = os.constants.signals |
| … | … | @@ -0,0 +1,3 @@ |
| 1 | const os = require('bare-os') | |
| 2 | ||
| 3 | module.exports = os.constants.signals |
| … | … | @@ -0,0 +1,7 @@ |
| 1 | declare class SubprocessError extends Error { | |
| 2 | readonly code: string | |
| 3 | ||
| 4 | static UNKNOWN_SIGNAL(msg: string): SubprocessError | |
| 5 | } | |
| 6 | ||
| 7 | export = SubprocessError |
| … | … | @@ -0,0 +1,18 @@ |
| 1 | module.exports = class SubprocessError extends Error { | |
| 2 | constructor(msg, code, fn = SubprocessError) { | |
| 3 | super(`${code}: ${msg}`) | |
| 4 | this.code = code | |
| 5 | ||
| 6 | if (Error.captureStackTrace) { | |
| 7 | Error.captureStackTrace(this, fn) | |
| 8 | } | |
| 9 | } | |
| 10 | ||
| 11 | get name() { | |
| 12 | return 'SubprocessError' | |
| 13 | } | |
| 14 | ||
| 15 | static UNKNOWN_SIGNAL(msg) { | |
| 16 | return new SubprocessError(msg, 'UNKNOWN_SIGNAL', SubprocessError.UNKNOWN_SIGNAL) | |
| 17 | } | |
| 18 | } |
| … | … | @@ -0,0 +1,72 @@ |
| 1 | { | |
| 2 | "name": "bare-subprocess", | |
| 3 | "version": "5.2.3", | |
| 4 | "description": "Native process spawning for JavaScript", | |
| 5 | "exports": { | |
| 6 | ".": { | |
| 7 | "types": "./index.d.ts", | |
| 8 | "default": "./index.js" | |
| 9 | }, | |
| 10 | "./package": "./package.json", | |
| 11 | "./constants": { | |
| 12 | "types": "./lib/constants.d.ts", | |
| 13 | "default": "./lib/constants.js" | |
| 14 | }, | |
| 15 | "./errors": { | |
| 16 | "types": "./lib/errors.d.ts", | |
| 17 | "default": "./lib/errors.js" | |
| 18 | } | |
| 19 | }, | |
| 20 | "files": [ | |
| 21 | "index.js", | |
| 22 | "index.d.ts", | |
| 23 | "binding.c", | |
| 24 | "binding.js", | |
| 25 | "CMakeLists.txt", | |
| 26 | "lib", | |
| 27 | "prebuilds" | |
| 28 | ], | |
| 29 | "addon": true, | |
| 30 | "scripts": { | |
| 31 | "test": "prettier . --check && bare test.js" | |
| 32 | }, | |
| 33 | "repository": { | |
| 34 | "type": "git", | |
| 35 | "url": "git+https://github.com/holepunchto/bare-subprocess.git" | |
| 36 | }, | |
| 37 | "author": "Holepunch", | |
| 38 | "license": "Apache-2.0", | |
| 39 | "bugs": { | |
| 40 | "url": "https://github.com/holepunchto/bare-subprocess/issues" | |
| 41 | }, | |
| 42 | "homepage": "https://github.com/holepunchto/bare-subprocess#readme", | |
| 43 | "engines": { | |
| 44 | "bare": ">=1.7.0" | |
| 45 | }, | |
| 46 | "dependencies": { | |
| 47 | "bare-env": "^3.0.0", | |
| 48 | "bare-events": "^2.5.4", | |
| 49 | "bare-os": "^3.0.1", | |
| 50 | "bare-pipe": "^4.0.0", | |
| 51 | "bare-url": "^2.2.2" | |
| 52 | }, | |
| 53 | "devDependencies": { | |
| 54 | "bare-abort": "^2.0.13", | |
| 55 | "bare-assert": "^1.0.2", | |
| 56 | "bare-buffer": "^3.0.2", | |
| 57 | "bare-fs": "^4.1.2", | |
| 58 | "bare-path": "^3.0.0", | |
| 59 | "brittle": "^3.2.1", | |
| 60 | "cmake-bare": "^1.1.7", | |
| 61 | "prettier": "^3.4.1", | |
| 62 | "prettier-config-holepunch": "^2.0.0" | |
| 63 | }, | |
| 64 | "peerDependencies": { | |
| 65 | "bare-buffer": "*" | |
| 66 | }, | |
| 67 | "peerDependenciesMeta": { | |
| 68 | "bare-buffer": { | |
| 69 | "optional": true | |
| 70 | } | |
| 71 | } | |
| 72 | } |
| … | … | @@ -0,0 +1,23 @@ |
| 1 | cmake_minimum_required(VERSION 4.0) | |
| 2 | ||
| 3 | find_package(cmake-bare REQUIRED PATHS node_modules/cmake-bare) | |
| 4 | ||
| 5 | project(bare_tcp C) | |
| 6 | ||
| 7 | bare_target(target) | |
| 8 | ||
| 9 | add_bare_module(bare_tcp) | |
| 10 | ||
| 11 | target_sources( | |
| 12 | ${bare_tcp} | |
| 13 | PRIVATE | |
| 14 | binding.c | |
| 15 | ) | |
| 16 | ||
| 17 | if(target MATCHES "win32") | |
| 18 | target_link_libraries( | |
| 19 | ${bare_tcp} | |
| 20 | PUBLIC | |
| 21 | ws2_32 | |
| 22 | ) | |
| 23 | endif() |
| … | … | @@ -0,0 +1,201 @@ |
| 1 | Apache License | |
| 2 | Version 2.0, January 2004 | |
| 3 | http://www.apache.org/licenses/ | |
| 4 | ||
| 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | |
| 6 | ||
| 7 | 1. Definitions. | |
| 8 | ||
| 9 | "License" shall mean the terms and conditions for use, reproduction, | |
| 10 | and distribution as defined by Sections 1 through 9 of this document. | |
| 11 | ||
| 12 | "Licensor" shall mean the copyright owner or entity authorized by | |
| 13 | the copyright owner that is granting the License. | |
| 14 | ||
| 15 | "Legal Entity" shall mean the union of the acting entity and all | |
| 16 | other entities that control, are controlled by, or are under common | |
| 17 | control with that entity. For the purposes of this definition, | |
| 18 | "control" means (i) the power, direct or indirect, to cause the | |
| 19 | direction or management of such entity, whether by contract or | |
| 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the | |
| 21 | outstanding shares, or (iii) beneficial ownership of such entity. | |
| 22 | ||
| 23 | "You" (or "Your") shall mean an individual or Legal Entity | |
| 24 | exercising permissions granted by this License. | |
| 25 | ||
| 26 | "Source" form shall mean the preferred form for making modifications, | |
| 27 | including but not limited to software source code, documentation | |
| 28 | source, and configuration files. | |
| 29 | ||
| 30 | "Object" form shall mean any form resulting from mechanical | |
| 31 | transformation or translation of a Source form, including but | |
| 32 | not limited to compiled object code, generated documentation, | |
| 33 | and conversions to other media types. | |
| 34 | ||
| 35 | "Work" shall mean the work of authorship, whether in Source or | |
| 36 | Object form, made available under the License, as indicated by a | |
| 37 | copyright notice that is included in or attached to the work | |
| 38 | (an example is provided in the Appendix below). | |
| 39 | ||
| 40 | "Derivative Works" shall mean any work, whether in Source or Object | |
| 41 | form, that is based on (or derived from) the Work and for which the | |
| 42 | editorial revisions, annotations, elaborations, or other modifications | |
| 43 | represent, as a whole, an original work of authorship. For the purposes | |
| 44 | of this License, Derivative Works shall not include works that remain | |
| 45 | separable from, or merely link (or bind by name) to the interfaces of, | |
| 46 | the Work and Derivative Works thereof. | |
| 47 | ||
| 48 | "Contribution" shall mean any work of authorship, including | |
| 49 | the original version of the Work and any modifications or additions | |
| 50 | to that Work or Derivative Works thereof, that is intentionally | |
| 51 | submitted to Licensor for inclusion in the Work by the copyright owner | |
| 52 | or by an individual or Legal Entity authorized to submit on behalf of | |
| 53 | the copyright owner. For the purposes of this definition, "submitted" | |
| 54 | means any form of electronic, verbal, or written communication sent | |
| 55 | to the Licensor or its representatives, including but not limited to | |
| 56 | communication on electronic mailing lists, source code control systems, | |
| 57 | and issue tracking systems that are managed by, or on behalf of, the | |
| 58 | Licensor for the purpose of discussing and improving the Work, but | |
| 59 | excluding communication that is conspicuously marked or otherwise | |
| 60 | designated in writing by the copyright owner as "Not a Contribution." | |
| 61 | ||
| 62 | "Contributor" shall mean Licensor and any individual or Legal Entity | |
| 63 | on behalf of whom a Contribution has been received by Licensor and | |
| 64 | subsequently incorporated within the Work. | |
| 65 | ||
| 66 | 2. Grant of Copyright License. Subject to the terms and conditions of | |
| 67 | this License, each Contributor hereby grants to You a perpetual, | |
| 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 69 | copyright license to reproduce, prepare Derivative Works of, | |
| 70 | publicly display, publicly perform, sublicense, and distribute the | |
| 71 | Work and such Derivative Works in Source or Object form. | |
| 72 | ||
| 73 | 3. Grant of Patent License. Subject to the terms and conditions of | |
| 74 | this License, each Contributor hereby grants to You a perpetual, | |
| 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 76 | (except as stated in this section) patent license to make, have made, | |
| 77 | use, offer to sell, sell, import, and otherwise transfer the Work, | |
| 78 | where such license applies only to those patent claims licensable | |
| 79 | by such Contributor that are necessarily infringed by their | |
| 80 | Contribution(s) alone or by combination of their Contribution(s) | |
| 81 | with the Work to which such Contribution(s) was submitted. If You | |
| 82 | institute patent litigation against any entity (including a | |
| 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work | |
| 84 | or a Contribution incorporated within the Work constitutes direct | |
| 85 | or contributory patent infringement, then any patent licenses | |
| 86 | granted to You under this License for that Work shall terminate | |
| 87 | as of the date such litigation is filed. | |
| 88 | ||
| 89 | 4. Redistribution. You may reproduce and distribute copies of the | |
| 90 | Work or Derivative Works thereof in any medium, with or without | |
| 91 | modifications, and in Source or Object form, provided that You | |
| 92 | meet the following conditions: | |
| 93 | ||
| 94 | (a) You must give any other recipients of the Work or | |
| 95 | Derivative Works a copy of this License; and | |
| 96 | ||
| 97 | (b) You must cause any modified files to carry prominent notices | |
| 98 | stating that You changed the files; and | |
| 99 | ||
| 100 | (c) You must retain, in the Source form of any Derivative Works | |
| 101 | that You distribute, all copyright, patent, trademark, and | |
| 102 | attribution notices from the Source form of the Work, | |
| 103 | excluding those notices that do not pertain to any part of | |
| 104 | the Derivative Works; and | |
| 105 | ||
| 106 | (d) If the Work includes a "NOTICE" text file as part of its | |
| 107 | distribution, then any Derivative Works that You distribute must | |
| 108 | include a readable copy of the attribution notices contained | |
| 109 | within such NOTICE file, excluding those notices that do not | |
| 110 | pertain to any part of the Derivative Works, in at least one | |
| 111 | of the following places: within a NOTICE text file distributed | |
| 112 | as part of the Derivative Works; within the Source form or | |
| 113 | documentation, if provided along with the Derivative Works; or, | |
| 114 | within a display generated by the Derivative Works, if and | |
| 115 | wherever such third-party notices normally appear. The contents | |
| 116 | of the NOTICE file are for informational purposes only and | |
| 117 | do not modify the License. You may add Your own attribution | |
| 118 | notices within Derivative Works that You distribute, alongside | |
| 119 | or as an addendum to the NOTICE text from the Work, provided | |
| 120 | that such additional attribution notices cannot be construed | |
| 121 | as modifying the License. | |
| 122 | ||
| 123 | You may add Your own copyright statement to Your modifications and | |
| 124 | may provide additional or different license terms and conditions | |
| 125 | for use, reproduction, or distribution of Your modifications, or | |
| 126 | for any such Derivative Works as a whole, provided Your use, | |
| 127 | reproduction, and distribution of the Work otherwise complies with | |
| 128 | the conditions stated in this License. | |
| 129 | ||
| 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, | |
| 131 | any Contribution intentionally submitted for inclusion in the Work | |
| 132 | by You to the Licensor shall be under the terms and conditions of | |
| 133 | this License, without any additional terms or conditions. | |
| 134 | Notwithstanding the above, nothing herein shall supersede or modify | |
| 135 | the terms of any separate license agreement you may have executed | |
| 136 | with Licensor regarding such Contributions. | |
| 137 | ||
| 138 | 6. Trademarks. This License does not grant permission to use the trade | |
| 139 | names, trademarks, service marks, or product names of the Licensor, | |
| 140 | except as required for reasonable and customary use in describing the | |
| 141 | origin of the Work and reproducing the content of the NOTICE file. | |
| 142 | ||
| 143 | 7. Disclaimer of Warranty. Unless required by applicable law or | |
| 144 | agreed to in writing, Licensor provides the Work (and each | |
| 145 | Contributor provides its Contributions) on an "AS IS" BASIS, | |
| 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |
| 147 | implied, including, without limitation, any warranties or conditions | |
| 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | |
| 149 | PARTICULAR PURPOSE. You are solely responsible for determining the | |
| 150 | appropriateness of using or redistributing the Work and assume any | |
| 151 | risks associated with Your exercise of permissions under this License. | |
| 152 | ||
| 153 | 8. Limitation of Liability. In no event and under no legal theory, | |
| 154 | whether in tort (including negligence), contract, or otherwise, | |
| 155 | unless required by applicable law (such as deliberate and grossly | |
| 156 | negligent acts) or agreed to in writing, shall any Contributor be | |
| 157 | liable to You for damages, including any direct, indirect, special, | |
| 158 | incidental, or consequential damages of any character arising as a | |
| 159 | result of this License or out of the use or inability to use the | |
| 160 | Work (including but not limited to damages for loss of goodwill, | |
| 161 | work stoppage, computer failure or malfunction, or any and all | |
| 162 | other commercial damages or losses), even if such Contributor | |
| 163 | has been advised of the possibility of such damages. | |
| 164 | ||
| 165 | 9. Accepting Warranty or Additional Liability. While redistributing | |
| 166 | the Work or Derivative Works thereof, You may choose to offer, | |
| 167 | and charge a fee for, acceptance of support, warranty, indemnity, | |
| 168 | or other liability obligations and/or rights consistent with this | |
| 169 | License. However, in accepting such obligations, You may act only | |
| 170 | on Your own behalf and on Your sole responsibility, not on behalf | |
| 171 | of any other Contributor, and only if You agree to indemnify, | |
| 172 | defend, and hold each Contributor harmless for any liability | |
| 173 | incurred by, or claims asserted against, such Contributor by reason | |
| 174 | of your accepting any such warranty or additional liability. | |
| 175 | ||
| 176 | END OF TERMS AND CONDITIONS | |
| 177 | ||
| 178 | APPENDIX: How to apply the Apache License to your work. | |
| 179 | ||
| 180 | To apply the Apache License to your work, attach the following | |
| 181 | boilerplate notice, with the fields enclosed by brackets "[]" | |
| 182 | replaced with your own identifying information. (Don't include | |
| 183 | the brackets!) The text should be enclosed in the appropriate | |
| 184 | comment syntax for the file format. We also recommend that a | |
| 185 | file or class name and description of purpose be included on the | |
| 186 | same "printed page" as the copyright notice for easier | |
| 187 | identification within third-party archives. | |
| 188 | ||
| 189 | Copyright [yyyy] [name of copyright owner] | |
| 190 | ||
| 191 | Licensed under the Apache License, Version 2.0 (the "License"); | |
| 192 | you may not use this file except in compliance with the License. | |
| 193 | You may obtain a copy of the License at | |
| 194 | ||
| 195 | http://www.apache.org/licenses/LICENSE-2.0 | |
| 196 | ||
| 197 | Unless required by applicable law or agreed to in writing, software | |
| 198 | distributed under the License is distributed on an "AS IS" BASIS, | |
| 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 200 | See the License for the specific language governing permissions and | |
| 201 | limitations under the License. |
| … | … | @@ -0,0 +1,25 @@ |
| 1 | # bare-tcp | |
| 2 | ||
| 3 | Native TCP sockets for JavaScript. | |
| 4 | ||
| 5 | ``` | |
| 6 | npm i bare-tcp | |
| 7 | ``` | |
| 8 | ||
| 9 | ## Usage | |
| 10 | ||
| 11 | ```js | |
| 12 | const tcp = require('bare-tcp') | |
| 13 | ||
| 14 | const server = tcp.createServer() | |
| 15 | server.on('connection', (socket) => socket.on('data', console.log)) | |
| 16 | server.listen(() => console.log('server is up')) | |
| 17 | ||
| 18 | const { port } = server.address() | |
| 19 | const socket = tcp.createConnection(port) | |
| 20 | socket.write('hello world') | |
| 21 | ``` | |
| 22 | ||
| 23 | ## License | |
| 24 | ||
| 25 | Apache-2.0 |
| … | … | @@ -0,0 +1,1022 @@ |
| 1 | #include <assert.h> | |
| 2 | #include <bare.h> | |
| 3 | #include <js.h> | |
| 4 | #include <stdlib.h> | |
| 5 | #include <uv.h> | |
| 6 | ||
| 7 | typedef struct { | |
| 8 | uv_tcp_t handle; | |
| 9 | ||
| 10 | struct { | |
| 11 | uv_connect_t connect; | |
| 12 | uv_write_t write; | |
| 13 | uv_shutdown_t shutdown; | |
| 14 | } requests; | |
| 15 | ||
| 16 | uv_buf_t read; | |
| 17 | ||
| 18 | js_env_t *env; | |
| 19 | js_ref_t *ctx; | |
| 20 | js_ref_t *on_connection; | |
| 21 | js_ref_t *on_connect; | |
| 22 | js_ref_t *on_reset; | |
| 23 | js_ref_t *on_read; | |
| 24 | js_ref_t *on_write; | |
| 25 | js_ref_t *on_end; | |
| 26 | js_ref_t *on_close; | |
| 27 | ||
| 28 | bool resetting; | |
| 29 | bool closing; | |
| 30 | bool exiting; | |
| 31 | ||
| 32 | js_deferred_teardown_t *teardown; | |
| 33 | } bare_tcp_t; | |
| 34 | ||
| 35 | static void | |
| 36 | bare_tcp__on_connection(uv_stream_t *server, int status) { | |
| 37 | int err; | |
| 38 | ||
| 39 | bare_tcp_t *tcp = (bare_tcp_t *) server; | |
| 40 | ||
| 41 | if (tcp->closing || tcp->exiting) return; | |
| 42 | ||
| 43 | js_env_t *env = tcp->env; | |
| 44 | ||
| 45 | js_handle_scope_t *scope; | |
| 46 | err = js_open_handle_scope(env, &scope); | |
| 47 | assert(err == 0); | |
| 48 | ||
| 49 | js_value_t *ctx; | |
| 50 | err = js_get_reference_value(env, tcp->ctx, &ctx); | |
| 51 | assert(err == 0); | |
| 52 | ||
| 53 | js_value_t *on_connection; | |
| 54 | err = js_get_reference_value(env, tcp->on_connection, &on_connection); | |
| 55 | assert(err == 0); | |
| 56 | ||
| 57 | js_value_t *argv[1]; | |
| 58 | ||
| 59 | if (status < 0) { | |
| 60 | js_value_t *code; | |
| 61 | err = js_create_string_utf8(env, (utf8_t *) uv_err_name(status), -1, &code); | |
| 62 | assert(err == 0); | |
| 63 | ||
| 64 | js_value_t *message; | |
| 65 | err = js_create_string_utf8(env, (utf8_t *) uv_strerror(status), -1, &message); | |
| 66 | assert(err == 0); | |
| 67 | ||
| 68 | err = js_create_error(env, code, message, &argv[0]); | |
| 69 | assert(err == 0); | |
| 70 | } else { | |
| 71 | err = js_get_null(env, &argv[0]); | |
| 72 | assert(err == 0); | |
| 73 | } | |
| 74 | ||
| 75 | js_call_function(env, ctx, on_connection, 1, argv, NULL); | |
| 76 | ||
| 77 | err = js_close_handle_scope(env, scope); | |
| 78 | assert(err == 0); | |
| 79 | } | |
| 80 | ||
| 81 | static void | |
| 82 | bare_tcp__on_connect(uv_connect_t *req, int status) { | |
| 83 | int err; | |
| 84 | ||
| 85 | bare_tcp_t *tcp = (bare_tcp_t *) req->data; | |
| 86 | ||
| 87 | if (tcp->exiting) return; | |
| 88 | ||
| 89 | js_env_t *env = tcp->env; | |
| 90 | ||
| 91 | js_handle_scope_t *scope; | |
| 92 | err = js_open_handle_scope(env, &scope); | |
| 93 | assert(err == 0); | |
| 94 | ||
| 95 | js_value_t *ctx; | |
| 96 | err = js_get_reference_value(env, tcp->ctx, &ctx); | |
| 97 | assert(err == 0); | |
| 98 | ||
| 99 | js_value_t *on_connect; | |
| 100 | err = js_get_reference_value(env, tcp->on_connect, &on_connect); | |
| 101 | assert(err == 0); | |
| 102 | ||
| 103 | js_value_t *argv[1]; | |
| 104 | ||
| 105 | if (status < 0) { | |
| 106 | js_value_t *code; | |
| 107 | err = js_create_string_utf8(env, (utf8_t *) uv_err_name(status), -1, &code); | |
| 108 | assert(err == 0); | |
| 109 | ||
| 110 | js_value_t *message; | |
| 111 | err = js_create_string_utf8(env, (utf8_t *) uv_strerror(status), -1, &message); | |
| 112 | assert(err == 0); | |
| 113 | ||
| 114 | err = js_create_error(env, code, message, &argv[0]); | |
| 115 | assert(err == 0); | |
| 116 | } else { | |
| 117 | err = js_get_null(env, &argv[0]); | |
| 118 | assert(err == 0); | |
| 119 | } | |
| 120 | ||
| 121 | js_call_function(env, ctx, on_connect, 1, argv, NULL); | |
| 122 | ||
| 123 | err = js_close_handle_scope(env, scope); | |
| 124 | assert(err == 0); | |
| 125 | } | |
| 126 | ||
| 127 | static void | |
| 128 | bare_tcp__on_read(uv_stream_t *stream, ssize_t nread, const uv_buf_t *buf) { | |
| 129 | if (nread == UV_EOF) nread = 0; | |
| 130 | else if (nread == 0) return; | |
| 131 | ||
| 132 | int err; | |
| 133 | ||
| 134 | bare_tcp_t *tcp = (bare_tcp_t *) stream; | |
| 135 | ||
| 136 | if (tcp->exiting) return; | |
| 137 | ||
| 138 | js_env_t *env = tcp->env; | |
| 139 | ||
| 140 | js_handle_scope_t *scope; | |
| 141 | err = js_open_handle_scope(env, &scope); | |
| 142 | assert(err == 0); | |
| 143 | ||
| 144 | js_value_t *ctx; | |
| 145 | err = js_get_reference_value(env, tcp->ctx, &ctx); | |
| 146 | assert(err == 0); | |
| 147 | ||
| 148 | js_value_t *on_read; | |
| 149 | err = js_get_reference_value(env, tcp->on_read, &on_read); | |
| 150 | assert(err == 0); | |
| 151 | ||
| 152 | js_value_t *argv[2]; | |
| 153 | ||
| 154 | if (nread < 0) { | |
| 155 | js_value_t *code; | |
| 156 | err = js_create_string_utf8(env, (utf8_t *) uv_err_name(nread), -1, &code); | |
| 157 | assert(err == 0); | |
| 158 | ||
| 159 | js_value_t *message; | |
| 160 | err = js_create_string_utf8(env, (utf8_t *) uv_strerror(nread), -1, &message); | |
| 161 | assert(err == 0); | |
| 162 | ||
| 163 | err = js_create_error(env, code, message, &argv[0]); | |
| 164 | assert(err == 0); | |
| 165 | ||
| 166 | err = js_create_int32(env, 0, &argv[1]); | |
| 167 | assert(err == 0); | |
| 168 | } else { | |
| 169 | err = js_get_null(env, &argv[0]); | |
| 170 | assert(err == 0); | |
| 171 | ||
| 172 | err = js_create_int32(env, nread, &argv[1]); | |
| 173 | assert(err == 0); | |
| 174 | } | |
| 175 | ||
| 176 | js_call_function(env, ctx, on_read, 2, argv, NULL); | |
| 177 | ||
| 178 | err = js_close_handle_scope(env, scope); | |
| 179 | assert(err == 0); | |
| 180 | } | |
| 181 | ||
| 182 | static void | |
| 183 | bare_tcp__on_write(uv_write_t *req, int status) { | |
| 184 | int err; | |
| 185 | ||
| 186 | bare_tcp_t *tcp = (bare_tcp_t *) req->data; | |
| 187 | ||
| 188 | if (tcp->exiting) return; | |
| 189 | ||
| 190 | js_env_t *env = tcp->env; | |
| 191 | ||
| 192 | js_handle_scope_t *scope; | |
| 193 | err = js_open_handle_scope(env, &scope); | |
| 194 | assert(err == 0); | |
| 195 | ||
| 196 | js_value_t *ctx; | |
| 197 | err = js_get_reference_value(env, tcp->ctx, &ctx); | |
| 198 | assert(err == 0); | |
| 199 | ||
| 200 | js_value_t *on_write; | |
| 201 | err = js_get_reference_value(env, tcp->on_write, &on_write); | |
| 202 | assert(err == 0); | |
| 203 | ||
| 204 | js_value_t *argv[1]; | |
| 205 | ||
| 206 | if (status < 0) { | |
| 207 | js_value_t *code; | |
| 208 | err = js_create_string_utf8(env, (utf8_t *) uv_err_name(status), -1, &code); | |
| 209 | assert(err == 0); | |
| 210 | ||
| 211 | js_value_t *message; | |
| 212 | err = js_create_string_utf8(env, (utf8_t *) uv_strerror(status), -1, &message); | |
| 213 | assert(err == 0); | |
| 214 | ||
| 215 | err = js_create_error(env, code, message, &argv[0]); | |
| 216 | assert(err == 0); | |
| 217 | } else { | |
| 218 | err = js_get_null(env, &argv[0]); | |
| 219 | assert(err == 0); | |
| 220 | } | |
| 221 | ||
| 222 | js_call_function(env, ctx, on_write, 1, argv, NULL); | |
| 223 | ||
| 224 | err = js_close_handle_scope(env, scope); | |
| 225 | assert(err == 0); | |
| 226 | } | |
| 227 | ||
| 228 | static void | |
| 229 | bare_tcp__on_shutdown(uv_shutdown_t *req, int status) { | |
| 230 | int err; | |
| 231 | ||
| 232 | bare_tcp_t *tcp = (bare_tcp_t *) req->data; | |
| 233 | ||
| 234 | if (tcp->exiting) return; | |
| 235 | ||
| 236 | js_env_t *env = tcp->env; | |
| 237 | ||
| 238 | js_handle_scope_t *scope; | |
| 239 | err = js_open_handle_scope(env, &scope); | |
| 240 | assert(err == 0); | |
| 241 | ||
| 242 | js_value_t *ctx; | |
| 243 | err = js_get_reference_value(env, tcp->ctx, &ctx); | |
| 244 | assert(err == 0); | |
| 245 | ||
| 246 | js_value_t *on_end; | |
| 247 | err = js_get_reference_value(env, tcp->on_end, &on_end); | |
| 248 | assert(err == 0); | |
| 249 | ||
| 250 | js_value_t *argv[1]; | |
| 251 | ||
| 252 | if (status < 0) { | |
| 253 | js_value_t *code; | |
| 254 | err = js_create_string_utf8(env, (utf8_t *) uv_err_name(status), -1, &code); | |
| 255 | assert(err == 0); | |
| 256 | ||
| 257 | js_value_t *message; | |
| 258 | err = js_create_string_utf8(env, (utf8_t *) uv_strerror(status), -1, &message); | |
| 259 | assert(err == 0); | |
| 260 | ||
| 261 | err = js_create_error(env, code, message, &argv[0]); | |
| 262 | assert(err == 0); | |
| 263 | } else { | |
| 264 | err = js_get_null(env, &argv[0]); | |
| 265 | assert(err == 0); | |
| 266 | } | |
| 267 | ||
| 268 | js_call_function(env, ctx, on_end, 1, argv, NULL); | |
| 269 | ||
| 270 | err = js_close_handle_scope(env, scope); | |
| 271 | assert(err == 0); | |
| 272 | } | |
| 273 | ||
| 274 | static void | |
| 275 | bare_tcp__on_close(uv_handle_t *handle) { | |
| 276 | int err; | |
| 277 | ||
| 278 | bare_tcp_t *tcp = (bare_tcp_t *) handle; | |
| 279 | ||
| 280 | js_env_t *env = tcp->env; | |
| 281 | ||
| 282 | js_handle_scope_t *scope; | |
| 283 | err = js_open_handle_scope(env, &scope); | |
| 284 | assert(err == 0); | |
| 285 | ||
| 286 | js_value_t *ctx; | |
| 287 | err = js_get_reference_value(env, tcp->ctx, &ctx); | |
| 288 | assert(err == 0); | |
| 289 | ||
| 290 | if (tcp->resetting && !tcp->exiting) { | |
| 291 | uv_loop_t *loop; | |
| 292 | err = js_get_env_loop(env, &loop); | |
| 293 | assert(err == 0); | |
| 294 | ||
| 295 | js_value_t *on_reset; | |
| 296 | err = js_get_reference_value(env, tcp->on_reset, &on_reset); | |
| 297 | assert(err == 0); | |
| 298 | ||
| 299 | err = uv_tcp_init(loop, &tcp->handle); | |
| 300 | ||
| 301 | js_value_t *argv[1]; | |
| 302 | ||
| 303 | if (err < 0) { | |
| 304 | js_value_t *code; | |
| 305 | err = js_create_string_utf8(env, (utf8_t *) uv_err_name(err), -1, &code); | |
| 306 | assert(err == 0); | |
| 307 | ||
| 308 | js_value_t *message; | |
| 309 | err = js_create_string_utf8(env, (utf8_t *) uv_strerror(err), -1, &message); | |
| 310 | assert(err == 0); | |
| 311 | ||
| 312 | err = js_create_error(env, code, message, &argv[0]); | |
| 313 | assert(err == 0); | |
| 314 | } else { | |
| 315 | err = js_get_null(env, &argv[0]); | |
| 316 | assert(err == 0); | |
| 317 | } | |
| 318 | ||
| 319 | tcp->resetting = false; | |
| 320 | ||
| 321 | js_call_function(env, ctx, on_reset, 1, argv, NULL); | |
| 322 | ||
| 323 | err = js_close_handle_scope(env, scope); | |
| 324 | assert(err == 0); | |
| 325 | } else { | |
| 326 | js_deferred_teardown_t *teardown = tcp->teardown; | |
| 327 | ||
| 328 | js_value_t *on_close; | |
| 329 | err = js_get_reference_value(env, tcp->on_close, &on_close); | |
| 330 | assert(err == 0); | |
| 331 | ||
| 332 | err = js_delete_reference(env, tcp->on_connection); | |
| 333 | assert(err == 0); | |
| 334 | ||
| 335 | err = js_delete_reference(env, tcp->on_connect); | |
| 336 | assert(err == 0); | |
| 337 | ||
| 338 | err = js_delete_reference(env, tcp->on_reset); | |
| 339 | assert(err == 0); | |
| 340 | ||
| 341 | err = js_delete_reference(env, tcp->on_read); | |
| 342 | assert(err == 0); | |
| 343 | ||
| 344 | err = js_delete_reference(env, tcp->on_write); | |
| 345 | assert(err == 0); | |
| 346 | ||
| 347 | err = js_delete_reference(env, tcp->on_end); | |
| 348 | assert(err == 0); | |
| 349 | ||
| 350 | err = js_delete_reference(env, tcp->on_close); | |
| 351 | assert(err == 0); | |
| 352 | ||
| 353 | err = js_delete_reference(env, tcp->ctx); | |
| 354 | assert(err == 0); | |
| 355 | ||
| 356 | if (!tcp->exiting) js_call_function(env, ctx, on_close, 0, NULL, NULL); | |
| 357 | ||
| 358 | err = js_close_handle_scope(env, scope); | |
| 359 | assert(err == 0); | |
| 360 | ||
| 361 | err = js_finish_deferred_teardown_callback(teardown); | |
| 362 | assert(err == 0); | |
| 363 | } | |
| 364 | } | |
| 365 | ||
| 366 | static void | |
| 367 | bare_tcp__on_teardown(js_deferred_teardown_t *handle, void *data) { | |
| 368 | bare_tcp_t *tcp = (bare_tcp_t *) data; | |
| 369 | ||
| 370 | tcp->exiting = true; | |
| 371 | ||
| 372 | if (tcp->closing) return; | |
| 373 | ||
| 374 | uv_close((uv_handle_t *) &tcp->handle, bare_tcp__on_close); | |
| 375 | } | |
| 376 | ||
| 377 | static void | |
| 378 | bare_tcp__on_alloc(uv_handle_t *handle, size_t suggested_size, uv_buf_t *buf) { | |
| 379 | bare_tcp_t *tcp = (bare_tcp_t *) handle; | |
| 380 | ||
| 381 | *buf = tcp->read; | |
| 382 | } | |
| 383 | ||
| 384 | static js_value_t * | |
| 385 | bare_tcp_init(js_env_t *env, js_callback_info_t *info) { | |
| 386 | int err; | |
| 387 | ||
| 388 | size_t argc = 9; | |
| 389 | js_value_t *argv[9]; | |
| 390 | ||
| 391 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 392 | assert(err == 0); | |
| 393 | ||
| 394 | assert(argc == 9); | |
| 395 | ||
| 396 | uv_loop_t *loop; | |
| 397 | err = js_get_env_loop(env, &loop); | |
| 398 | assert(err == 0); | |
| 399 | ||
| 400 | js_value_t *handle; | |
| 401 | ||
| 402 | bare_tcp_t *tcp; | |
| 403 | err = js_create_arraybuffer(env, sizeof(bare_tcp_t), (void **) &tcp, &handle); | |
| 404 | assert(err == 0); | |
| 405 | ||
| 406 | err = uv_tcp_init(loop, &tcp->handle); | |
| 407 | ||
| 408 | if (err < 0) { | |
| 409 | err = js_throw_error(env, uv_err_name(err), uv_strerror(err)); | |
| 410 | assert(err == 0); | |
| 411 | ||
| 412 | return NULL; | |
| 413 | } | |
| 414 | ||
| 415 | tcp->env = env; | |
| 416 | tcp->resetting = false; | |
| 417 | tcp->closing = false; | |
| 418 | tcp->exiting = false; | |
| 419 | ||
| 420 | err = js_get_typedarray_info(env, argv[0], NULL, (void **) &tcp->read.base, (size_t *) &tcp->read.len, NULL, NULL); | |
| 421 | assert(err == 0); | |
| 422 | ||
| 423 | err = js_create_reference(env, argv[1], 1, &tcp->ctx); | |
| 424 | assert(err == 0); | |
| 425 | ||
| 426 | err = js_create_reference(env, argv[2], 1, &tcp->on_connection); | |
| 427 | assert(err == 0); | |
| 428 | ||
| 429 | err = js_create_reference(env, argv[3], 1, &tcp->on_connect); | |
| 430 | assert(err == 0); | |
| 431 | ||
| 432 | err = js_create_reference(env, argv[4], 1, &tcp->on_reset); | |
| 433 | assert(err == 0); | |
| 434 | ||
| 435 | err = js_create_reference(env, argv[5], 1, &tcp->on_read); | |
| 436 | assert(err == 0); | |
| 437 | ||
| 438 | err = js_create_reference(env, argv[6], 1, &tcp->on_write); | |
| 439 | assert(err == 0); | |
| 440 | ||
| 441 | err = js_create_reference(env, argv[7], 1, &tcp->on_end); | |
| 442 | assert(err == 0); | |
| 443 | ||
| 444 | err = js_create_reference(env, argv[8], 1, &tcp->on_close); | |
| 445 | assert(err == 0); | |
| 446 | ||
| 447 | err = js_add_deferred_teardown_callback(env, bare_tcp__on_teardown, (void *) tcp, &tcp->teardown); | |
| 448 | assert(err == 0); | |
| 449 | ||
| 450 | return handle; | |
| 451 | } | |
| 452 | ||
| 453 | static js_value_t * | |
| 454 | bare_tcp_connect(js_env_t *env, js_callback_info_t *info) { | |
| 455 | int err; | |
| 456 | ||
| 457 | size_t argc = 4; | |
| 458 | js_value_t *argv[4]; | |
| 459 | ||
| 460 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 461 | assert(err == 0); | |
| 462 | ||
| 463 | assert(argc == 4); | |
| 464 | ||
| 465 | bare_tcp_t *tcp; | |
| 466 | err = js_get_arraybuffer_info(env, argv[0], (void **) &tcp, NULL); | |
| 467 | assert(err == 0); | |
| 468 | ||
| 469 | uint32_t port; | |
| 470 | err = js_get_value_uint32(env, argv[1], &port); | |
| 471 | assert(err == 0); | |
| 472 | ||
| 473 | utf8_t ip[INET6_ADDRSTRLEN]; | |
| 474 | err = js_get_value_string_utf8(env, argv[2], ip, INET6_ADDRSTRLEN, NULL); | |
| 475 | assert(err == 0); | |
| 476 | ||
| 477 | uint32_t family; | |
| 478 | err = js_get_value_uint32(env, argv[3], &family); | |
| 479 | assert(err == 0); | |
| 480 | ||
| 481 | struct sockaddr_storage addr; | |
| 482 | ||
| 483 | if (family == 4) { | |
| 484 | err = uv_ip4_addr((char *) ip, port, (struct sockaddr_in *) &addr); | |
| 485 | } else { | |
| 486 | err = uv_ip6_addr((char *) ip, port, (struct sockaddr_in6 *) &addr); | |
| 487 | } | |
| 488 | ||
| 489 | if (err < 0) { | |
| 490 | err = js_throw_error(env, uv_err_name(err), uv_strerror(err)); | |
| 491 | assert(err == 0); | |
| 492 | ||
| 493 | return NULL; | |
| 494 | } | |
| 495 | ||
| 496 | uv_connect_t *req = &tcp->requests.connect; | |
| 497 | ||
| 498 | req->data = tcp; | |
| 499 | ||
| 500 | err = uv_tcp_connect(req, &tcp->handle, (struct sockaddr *) &addr, bare_tcp__on_connect); | |
| 501 | ||
| 502 | if (err < 0) { | |
| 503 | err = js_throw_error(env, uv_err_name(err), uv_strerror(err)); | |
| 504 | assert(err == 0); | |
| 505 | } | |
| 506 | ||
| 507 | return NULL; | |
| 508 | } | |
| 509 | ||
| 510 | static js_value_t * | |
| 511 | bare_tcp_reset(js_env_t *env, js_callback_info_t *info) { | |
| 512 | int err; | |
| 513 | ||
| 514 | size_t argc = 1; | |
| 515 | js_value_t *argv[1]; | |
| 516 | ||
| 517 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 518 | assert(err == 0); | |
| 519 | ||
| 520 | assert(argc == 1); | |
| 521 | ||
| 522 | bare_tcp_t *tcp; | |
| 523 | err = js_get_arraybuffer_info(env, argv[0], (void **) &tcp, NULL); | |
| 524 | assert(err == 0); | |
| 525 | ||
| 526 | tcp->resetting = true; | |
| 527 | ||
| 528 | uv_close((uv_handle_t *) &tcp->handle, bare_tcp__on_close); | |
| 529 | ||
| 530 | return NULL; | |
| 531 | } | |
| 532 | ||
| 533 | static js_value_t * | |
| 534 | bare_tcp_bind(js_env_t *env, js_callback_info_t *info) { | |
| 535 | int err; | |
| 536 | ||
| 537 | size_t argc = 5; | |
| 538 | js_value_t *argv[5]; | |
| 539 | ||
| 540 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 541 | assert(err == 0); | |
| 542 | ||
| 543 | assert(argc == 5); | |
| 544 | ||
| 545 | bare_tcp_t *tcp; | |
| 546 | err = js_get_arraybuffer_info(env, argv[0], (void **) &tcp, NULL); | |
| 547 | assert(err == 0); | |
| 548 | ||
| 549 | uint32_t port; | |
| 550 | err = js_get_value_uint32(env, argv[1], &port); | |
| 551 | assert(err == 0); | |
| 552 | ||
| 553 | utf8_t ip[INET6_ADDRSTRLEN]; | |
| 554 | err = js_get_value_string_utf8(env, argv[2], ip, INET6_ADDRSTRLEN, NULL); | |
| 555 | assert(err == 0); | |
| 556 | ||
| 557 | uint32_t backlog; | |
| 558 | err = js_get_value_uint32(env, argv[3], &backlog); | |
| 559 | assert(err == 0); | |
| 560 | ||
| 561 | uint32_t family; | |
| 562 | err = js_get_value_uint32(env, argv[4], &family); | |
| 563 | assert(err == 0); | |
| 564 | ||
| 565 | struct sockaddr_storage addr; | |
| 566 | int addr_len; | |
| 567 | ||
| 568 | if (family == 4) { | |
| 569 | addr_len = sizeof(struct sockaddr_in); | |
| 570 | err = uv_ip4_addr((char *) ip, port, (struct sockaddr_in *) &addr); | |
| 571 | } else { | |
| 572 | addr_len = sizeof(struct sockaddr_in6); | |
| 573 | err = uv_ip6_addr((char *) ip, port, (struct sockaddr_in6 *) &addr); | |
| 574 | } | |
| 575 | ||
| 576 | if (err < 0) { | |
| 577 | err = js_throw_error(env, uv_err_name(err), uv_strerror(err)); | |
| 578 | assert(err == 0); | |
| 579 | ||
| 580 | return NULL; | |
| 581 | } | |
| 582 | ||
| 583 | err = uv_tcp_bind(&tcp->handle, (struct sockaddr *) &addr, 0); | |
| 584 | ||
| 585 | if (err < 0) { | |
| 586 | err = js_throw_error(env, uv_err_name(err), uv_strerror(err)); | |
| 587 | assert(err == 0); | |
| 588 | ||
| 589 | return NULL; | |
| 590 | } | |
| 591 | ||
| 592 | err = uv_listen((uv_stream_t *) &tcp->handle, backlog, bare_tcp__on_connection); | |
| 593 | ||
| 594 | if (err < 0) { | |
| 595 | err = js_throw_error(env, uv_err_name(err), uv_strerror(err)); | |
| 596 | assert(err == 0); | |
| 597 | ||
| 598 | return NULL; | |
| 599 | } | |
| 600 | ||
| 601 | return NULL; | |
| 602 | } | |
| 603 | ||
| 604 | static js_value_t * | |
| 605 | bare_tcp_accept(js_env_t *env, js_callback_info_t *info) { | |
| 606 | int err; | |
| 607 | ||
| 608 | size_t argc = 2; | |
| 609 | js_value_t *argv[2]; | |
| 610 | ||
| 611 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 612 | assert(err == 0); | |
| 613 | ||
| 614 | assert(argc == 2); | |
| 615 | ||
| 616 | bare_tcp_t *server; | |
| 617 | err = js_get_arraybuffer_info(env, argv[0], (void **) &server, NULL); | |
| 618 | assert(err == 0); | |
| 619 | ||
| 620 | bare_tcp_t *client; | |
| 621 | err = js_get_arraybuffer_info(env, argv[1], (void **) &client, NULL); | |
| 622 | assert(err == 0); | |
| 623 | ||
| 624 | err = uv_accept((uv_stream_t *) &server->handle, (uv_stream_t *) &client->handle); | |
| 625 | ||
| 626 | if (err < 0) { | |
| 627 | err = js_throw_error(env, uv_err_name(err), uv_strerror(err)); | |
| 628 | assert(err == 0); | |
| 629 | } | |
| 630 | ||
| 631 | return NULL; | |
| 632 | } | |
| 633 | ||
| 634 | static js_value_t * | |
| 635 | bare_tcp_resume(js_env_t *env, js_callback_info_t *info) { | |
| 636 | int err; | |
| 637 | ||
| 638 | size_t argc = 1; | |
| 639 | js_value_t *argv[1]; | |
| 640 | ||
| 641 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 642 | assert(err == 0); | |
| 643 | ||
| 644 | assert(argc == 1); | |
| 645 | ||
| 646 | bare_tcp_t *tcp; | |
| 647 | err = js_get_arraybuffer_info(env, argv[0], (void **) &tcp, NULL); | |
| 648 | assert(err == 0); | |
| 649 | ||
| 650 | if (!uv_is_readable((uv_stream_t *) &tcp->handle)) return NULL; | |
| 651 | ||
| 652 | err = uv_read_start((uv_stream_t *) &tcp->handle, bare_tcp__on_alloc, bare_tcp__on_read); | |
| 653 | ||
| 654 | if (err < 0) { | |
| 655 | err = js_throw_error(env, uv_err_name(err), uv_strerror(err)); | |
| 656 | assert(err == 0); | |
| 657 | } | |
| 658 | ||
| 659 | return NULL; | |
| 660 | } | |
| 661 | ||
| 662 | static js_value_t * | |
| 663 | bare_tcp_pause(js_env_t *env, js_callback_info_t *info) { | |
| 664 | int err; | |
| 665 | ||
| 666 | size_t argc = 1; | |
| 667 | js_value_t *argv[1]; | |
| 668 | ||
| 669 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 670 | assert(err == 0); | |
| 671 | ||
| 672 | assert(argc == 1); | |
| 673 | ||
| 674 | bare_tcp_t *tcp; | |
| 675 | err = js_get_arraybuffer_info(env, argv[0], (void **) &tcp, NULL); | |
| 676 | assert(err == 0); | |
| 677 | ||
| 678 | if (!uv_is_readable((uv_stream_t *) &tcp->handle)) return NULL; | |
| 679 | ||
| 680 | err = uv_read_stop((uv_stream_t *) &tcp->handle); | |
| 681 | ||
| 682 | if (err < 0) { | |
| 683 | err = js_throw_error(env, uv_err_name(err), uv_strerror(err)); | |
| 684 | assert(err == 0); | |
| 685 | } | |
| 686 | ||
| 687 | return NULL; | |
| 688 | } | |
| 689 | ||
| 690 | static js_value_t * | |
| 691 | bare_tcp_writev(js_env_t *env, js_callback_info_t *info) { | |
| 692 | int err; | |
| 693 | ||
| 694 | size_t argc = 2; | |
| 695 | js_value_t *argv[2]; | |
| 696 | ||
| 697 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 698 | assert(err == 0); | |
| 699 | ||
| 700 | assert(argc == 2); | |
| 701 | ||
| 702 | bare_tcp_t *tcp; | |
| 703 | err = js_get_arraybuffer_info(env, argv[0], (void **) &tcp, NULL); | |
| 704 | assert(err == 0); | |
| 705 | ||
| 706 | js_value_t *arr = argv[1]; | |
| 707 | ||
| 708 | uint32_t bufs_len; | |
| 709 | err = js_get_array_length(env, arr, &bufs_len); | |
| 710 | assert(err == 0); | |
| 711 | ||
| 712 | uv_buf_t *bufs = malloc(sizeof(uv_buf_t) * bufs_len); | |
| 713 | ||
| 714 | js_value_t **elements = malloc(sizeof(js_value_t *) * bufs_len); | |
| 715 | err = js_get_array_elements(env, arr, elements, bufs_len, 0, NULL); | |
| 716 | assert(err == 0); | |
| 717 | ||
| 718 | for (uint32_t i = 0; i < bufs_len; i++) { | |
| 719 | js_value_t *item = elements[i]; | |
| 720 | ||
| 721 | uv_buf_t *buf = &bufs[i]; | |
| 722 | err = js_get_typedarray_info(env, item, NULL, (void **) &buf->base, (size_t *) &buf->len, NULL, NULL); | |
| 723 | assert(err == 0); | |
| 724 | } | |
| 725 | ||
| 726 | uv_write_t *req = &tcp->requests.write; | |
| 727 | ||
| 728 | req->data = tcp; | |
| 729 | ||
| 730 | err = uv_write(req, (uv_stream_t *) &tcp->handle, bufs, bufs_len, bare_tcp__on_write); | |
| 731 | ||
| 732 | free(bufs); | |
| 733 | free(elements); | |
| 734 | ||
| 735 | if (err < 0) { | |
| 736 | err = js_throw_error(env, uv_err_name(err), uv_strerror(err)); | |
| 737 | assert(err == 0); | |
| 738 | } | |
| 739 | ||
| 740 | return NULL; | |
| 741 | } | |
| 742 | ||
| 743 | static js_value_t * | |
| 744 | bare_tcp_end(js_env_t *env, js_callback_info_t *info) { | |
| 745 | int err; | |
| 746 | ||
| 747 | size_t argc = 1; | |
| 748 | js_value_t *argv[1]; | |
| 749 | ||
| 750 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 751 | assert(err == 0); | |
| 752 | ||
| 753 | assert(argc == 1); | |
| 754 | ||
| 755 | bare_tcp_t *tcp; | |
| 756 | err = js_get_arraybuffer_info(env, argv[0], (void **) &tcp, NULL); | |
| 757 | assert(err == 0); | |
| 758 | ||
| 759 | uv_shutdown_t *req = &tcp->requests.shutdown; | |
| 760 | ||
| 761 | req->data = tcp; | |
| 762 | ||
| 763 | err = uv_shutdown(req, (uv_stream_t *) &tcp->handle, bare_tcp__on_shutdown); | |
| 764 | ||
| 765 | if (err < 0) { | |
| 766 | err = js_throw_error(env, uv_err_name(err), uv_strerror(err)); | |
| 767 | assert(err == 0); | |
| 768 | } | |
| 769 | ||
| 770 | return NULL; | |
| 771 | } | |
| 772 | ||
| 773 | static js_value_t * | |
| 774 | bare_tcp_close(js_env_t *env, js_callback_info_t *info) { | |
| 775 | int err; | |
| 776 | ||
| 777 | size_t argc = 1; | |
| 778 | js_value_t *argv[1]; | |
| 779 | ||
| 780 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 781 | assert(err == 0); | |
| 782 | ||
| 783 | assert(argc == 1); | |
| 784 | ||
| 785 | bare_tcp_t *tcp; | |
| 786 | err = js_get_arraybuffer_info(env, argv[0], (void **) &tcp, NULL); | |
| 787 | assert(err == 0); | |
| 788 | ||
| 789 | tcp->closing = true; | |
| 790 | ||
| 791 | uv_close((uv_handle_t *) &tcp->handle, bare_tcp__on_close); | |
| 792 | ||
| 793 | return NULL; | |
| 794 | } | |
| 795 | ||
| 796 | static js_value_t * | |
| 797 | bare_tcp_keepalive(js_env_t *env, js_callback_info_t *info) { | |
| 798 | int err; | |
| 799 | ||
| 800 | size_t argc = 3; | |
| 801 | js_value_t *argv[3]; | |
| 802 | ||
| 803 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 804 | assert(err == 0); | |
| 805 | ||
| 806 | assert(argc == 3); | |
| 807 | ||
| 808 | bare_tcp_t *tcp; | |
| 809 | err = js_get_arraybuffer_info(env, argv[0], (void **) &tcp, NULL); | |
| 810 | assert(err == 0); | |
| 811 | ||
| 812 | bool enable; | |
| 813 | err = js_get_value_bool(env, argv[1], &enable); | |
| 814 | assert(err == 0); | |
| 815 | ||
| 816 | uint32_t delay; | |
| 817 | err = js_get_value_uint32(env, argv[2], &delay); | |
| 818 | assert(err == 0); | |
| 819 | ||
| 820 | err = uv_tcp_keepalive(&tcp->handle, enable, delay); | |
| 821 | assert(err == 0); | |
| 822 | ||
| 823 | return NULL; | |
| 824 | } | |
| 825 | ||
| 826 | static js_value_t * | |
| 827 | bare_tcp_nodelay(js_env_t *env, js_callback_info_t *info) { | |
| 828 | int err; | |
| 829 | ||
| 830 | size_t argc = 2; | |
| 831 | js_value_t *argv[2]; | |
| 832 | ||
| 833 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 834 | assert(err == 0); | |
| 835 | ||
| 836 | assert(argc == 2); | |
| 837 | ||
| 838 | bare_tcp_t *tcp; | |
| 839 | err = js_get_arraybuffer_info(env, argv[0], (void **) &tcp, NULL); | |
| 840 | assert(err == 0); | |
| 841 | ||
| 842 | bool enable; | |
| 843 | err = js_get_value_bool(env, argv[1], &enable); | |
| 844 | assert(err == 0); | |
| 845 | ||
| 846 | err = uv_tcp_nodelay(&tcp->handle, enable); | |
| 847 | assert(err == 0); | |
| 848 | ||
| 849 | return NULL; | |
| 850 | } | |
| 851 | ||
| 852 | static js_value_t * | |
| 853 | bare_tcp_address(js_env_t *env, js_callback_info_t *info) { | |
| 854 | int err; | |
| 855 | ||
| 856 | size_t argc = 2; | |
| 857 | js_value_t *argv[2]; | |
| 858 | ||
| 859 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 860 | assert(err == 0); | |
| 861 | ||
| 862 | assert(argc == 2); | |
| 863 | ||
| 864 | bare_tcp_t *tcp; | |
| 865 | err = js_get_arraybuffer_info(env, argv[0], (void **) &tcp, NULL); | |
| 866 | assert(err == 0); | |
| 867 | ||
| 868 | bool local; | |
| 869 | err = js_get_value_bool(env, argv[1], &local); | |
| 870 | assert(err == 0); | |
| 871 | ||
| 872 | struct sockaddr_storage addr; | |
| 873 | int len = sizeof(addr); | |
| 874 | ||
| 875 | if (local) { | |
| 876 | err = uv_tcp_getsockname(&tcp->handle, (struct sockaddr *) &addr, &len); | |
| 877 | } else { | |
| 878 | err = uv_tcp_getpeername(&tcp->handle, (struct sockaddr *) &addr, &len); | |
| 879 | } | |
| 880 | ||
| 881 | if (err < 0) { | |
| 882 | err = js_throw_error(env, uv_err_name(err), uv_strerror(err)); | |
| 883 | assert(err == 0); | |
| 884 | ||
| 885 | return NULL; | |
| 886 | } | |
| 887 | ||
| 888 | js_value_t *result; | |
| 889 | err = js_create_object(env, &result); | |
| 890 | assert(err == 0); | |
| 891 | ||
| 892 | js_value_t *result_address; | |
| 893 | js_value_t *result_family; | |
| 894 | js_value_t *result_port; | |
| 895 | ||
| 896 | if (addr.ss_family == AF_INET) { | |
| 897 | struct sockaddr_in *addr_in = (struct sockaddr_in *) &addr; | |
| 898 | ||
| 899 | char address[INET_ADDRSTRLEN]; | |
| 900 | err = uv_inet_ntop(AF_INET, &(addr_in->sin_addr), address, sizeof(address)); | |
| 901 | assert(err == 0); | |
| 902 | ||
| 903 | err = js_create_string_utf8(env, (utf8_t *) address, -1, &result_address); | |
| 904 | assert(err == 0); | |
| 905 | ||
| 906 | err = js_create_int32(env, 4, &result_family); | |
| 907 | assert(err == 0); | |
| 908 | ||
| 909 | err = js_create_int32(env, ntohs(addr_in->sin_port), &result_port); | |
| 910 | assert(err == 0); | |
| 911 | } else if (addr.ss_family == AF_INET6) { | |
| 912 | struct sockaddr_in6 *addr_in6 = (struct sockaddr_in6 *) &addr; | |
| 913 | ||
| 914 | char address[INET6_ADDRSTRLEN]; | |
| 915 | err = uv_inet_ntop(AF_INET6, &(addr_in6->sin6_addr), address, sizeof(address)); | |
| 916 | assert(err == 0); | |
| 917 | ||
| 918 | err = js_create_string_utf8(env, (utf8_t *) address, -1, &result_address); | |
| 919 | assert(err == 0); | |
| 920 | ||
| 921 | err = js_create_int32(env, 6, &result_family); | |
| 922 | assert(err == 0); | |
| 923 | ||
| 924 | err = js_create_int32(env, ntohs(addr_in6->sin6_port), &result_port); | |
| 925 | assert(err == 0); | |
| 926 | } else { | |
| 927 | err = UV_EAI_ADDRFAMILY; | |
| 928 | ||
| 929 | err = js_throw_error(env, uv_err_name(err), uv_strerror(err)); | |
| 930 | assert(err == 0); | |
| 931 | ||
| 932 | return NULL; | |
| 933 | } | |
| 934 | ||
| 935 | err = js_set_named_property(env, result, "address", result_address); | |
| 936 | assert(err == 0); | |
| 937 | ||
| 938 | err = js_set_named_property(env, result, "family", result_family); | |
| 939 | assert(err == 0); | |
| 940 | ||
| 941 | err = js_set_named_property(env, result, "port", result_port); | |
| 942 | assert(err == 0); | |
| 943 | ||
| 944 | return result; | |
| 945 | } | |
| 946 | ||
| 947 | static js_value_t * | |
| 948 | bare_tcp_ref(js_env_t *env, js_callback_info_t *info) { | |
| 949 | int err; | |
| 950 | ||
| 951 | size_t argc = 1; | |
| 952 | js_value_t *argv[1]; | |
| 953 | ||
| 954 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 955 | assert(err == 0); | |
| 956 | ||
| 957 | assert(argc == 1); | |
| 958 | ||
| 959 | bare_tcp_t *tcp; | |
| 960 | err = js_get_arraybuffer_info(env, argv[0], (void **) &tcp, NULL); | |
| 961 | assert(err == 0); | |
| 962 | ||
| 963 | uv_ref((uv_handle_t *) &tcp->handle); | |
| 964 | ||
| 965 | return NULL; | |
| 966 | } | |
| 967 | ||
| 968 | static js_value_t * | |
| 969 | bare_tcp_unref(js_env_t *env, js_callback_info_t *info) { | |
| 970 | int err; | |
| 971 | ||
| 972 | size_t argc = 1; | |
| 973 | js_value_t *argv[1]; | |
| 974 | ||
| 975 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 976 | assert(err == 0); | |
| 977 | ||
| 978 | assert(argc == 1); | |
| 979 | ||
| 980 | bare_tcp_t *tcp; | |
| 981 | err = js_get_arraybuffer_info(env, argv[0], (void **) &tcp, NULL); | |
| 982 | assert(err == 0); | |
| 983 | ||
| 984 | uv_unref((uv_handle_t *) &tcp->handle); | |
| 985 | ||
| 986 | return NULL; | |
| 987 | } | |
| 988 | ||
| 989 | static js_value_t * | |
| 990 | bare_tcp_exports(js_env_t *env, js_value_t *exports) { | |
| 991 | int err; | |
| 992 | ||
| 993 | #define V(name, fn) \ | |
| 994 | { \ | |
| 995 | js_value_t *val; \ | |
| 996 | err = js_create_function(env, name, -1, fn, NULL, &val); \ | |
| 997 | assert(err == 0); \ | |
| 998 | err = js_set_named_property(env, exports, name, val); \ | |
| 999 | assert(err == 0); \ | |
| 1000 | } | |
| 1001 | ||
| 1002 | V("init", bare_tcp_init) | |
| 1003 | V("connect", bare_tcp_connect) | |
| 1004 | V("reset", bare_tcp_reset) | |
| 1005 | V("bind", bare_tcp_bind) | |
| 1006 | V("accept", bare_tcp_accept) | |
| 1007 | V("resume", bare_tcp_resume) | |
| 1008 | V("pause", bare_tcp_pause) | |
| 1009 | V("writev", bare_tcp_writev) | |
| 1010 | V("end", bare_tcp_end) | |
| 1011 | V("close", bare_tcp_close) | |
| 1012 | V("keepalive", bare_tcp_keepalive) | |
| 1013 | V("nodelay", bare_tcp_nodelay) | |
| 1014 | V("address", bare_tcp_address) | |
| 1015 | V("ref", bare_tcp_ref) | |
| 1016 | V("unref", bare_tcp_unref) | |
| 1017 | #undef V | |
| 1018 | ||
| 1019 | return exports; | |
| 1020 | } | |
| 1021 | ||
| 1022 | BARE_MODULE(bare_tcp, bare_tcp_exports) |
| … | … | @@ -0,0 +1 @@ |
| 1 | module.exports = require.addon() |
| … | … | @@ -0,0 +1,179 @@ |
| 1 | import EventEmitter, { EventMap } from 'bare-events' | |
| 2 | import { Duplex, DuplexEvents } from 'bare-stream' | |
| 3 | import { IPFamily, LookupOptions } from 'bare-dns' | |
| 4 | import TCPError from './lib/errors' | |
| 5 | import constants from './lib/constants' | |
| 6 | ||
| 7 | interface DNSLookup { | |
| 8 | ( | |
| 9 | hostname: string, | |
| 10 | opts: LookupOptions, | |
| 11 | cb: (err: Error | null, address: string | null, family: IPFamily | 0) => void | |
| 12 | ): void | |
| 13 | } | |
| 14 | ||
| 15 | interface TCPSocketAddress { | |
| 16 | address: string | |
| 17 | family: `IPv${IPFamily}` | |
| 18 | port: number | |
| 19 | } | |
| 20 | ||
| 21 | interface TCPSocketEvents extends DuplexEvents { | |
| 22 | connect: [] | |
| 23 | lookup: [err: Error | null, address: string | null, family: IPFamily | 0, host: string] | |
| 24 | timeout: [ms: number] | |
| 25 | } | |
| 26 | ||
| 27 | interface TCPSocketOptions { | |
| 28 | allowHalfOpen?: boolean | |
| 29 | eagerOpen?: boolean | |
| 30 | readBufferSize?: number | |
| 31 | } | |
| 32 | ||
| 33 | interface TCPSocketConnectOptions extends LookupOptions { | |
| 34 | lookup?: DNSLookup | |
| 35 | host?: string | |
| 36 | keepAlive?: boolean | |
| 37 | keepAliveInitialDelay?: boolean | |
| 38 | noDelay?: boolean | |
| 39 | port?: number | |
| 40 | timeout?: number | |
| 41 | } | |
| 42 | ||
| 43 | interface TCPSocket<M extends TCPSocketEvents = TCPSocketEvents> extends Duplex<M> { | |
| 44 | readonly connecting: boolean | |
| 45 | readonly pending: boolean | |
| 46 | readonly timeout?: number | |
| 47 | readonly readyState: 'open' | 'opening' | |
| 48 | readonly localAddress?: string | |
| 49 | readonly localFamily?: string | |
| 50 | readonly localPort?: number | |
| 51 | readonly remoteAddress?: string | |
| 52 | readonly remoteFamily?: string | |
| 53 | readonly remotePort?: number | |
| 54 | ||
| 55 | connect(port: number, host?: string, opts?: TCPSocketConnectOptions, onconnect?: () => void): this | |
| 56 | connect(port: number, host: string, onconnect: () => void): this | |
| 57 | connect(port: number, onconnect: () => void): this | |
| 58 | connect(opts: TCPSocketConnectOptions): this | |
| 59 | ||
| 60 | setKeepAlive(enable?: boolean, delay?: number): this | |
| 61 | setKeepAlive(delay: number): this | |
| 62 | ||
| 63 | setNoDelay(enable?: boolean): this | |
| 64 | ||
| 65 | setTimeout(ms: number, ontimeout?: () => void): this | |
| 66 | ||
| 67 | ref(): this | |
| 68 | unref(): this | |
| 69 | } | |
| 70 | ||
| 71 | declare class TCPSocket<M extends TCPSocketEvents = TCPSocketEvents> extends Duplex<M> { | |
| 72 | constructor(opts?: TCPSocketOptions) | |
| 73 | } | |
| 74 | ||
| 75 | interface TCPServerEvents extends EventMap { | |
| 76 | close: [] | |
| 77 | connection: [socket: TCPSocket] | |
| 78 | error: [err: Error] | |
| 79 | listening: [] | |
| 80 | lookup: [err: Error | null, address: string | null, family: IPFamily | 0, host: string] | |
| 81 | } | |
| 82 | ||
| 83 | interface TCPServerOptions { | |
| 84 | allowHalfOpen?: number | |
| 85 | keepAlive?: boolean | |
| 86 | keepAliveInitialDelay?: boolean | |
| 87 | noDelay?: boolean | |
| 88 | pauseOnConnect?: boolean | |
| 89 | readBufferSize?: number | |
| 90 | } | |
| 91 | ||
| 92 | interface TCPServerListenOptions extends LookupOptions { | |
| 93 | lookup?: DNSLookup | |
| 94 | backlog?: number | |
| 95 | host?: string | |
| 96 | port?: number | |
| 97 | } | |
| 98 | ||
| 99 | interface TCPServer<M extends TCPServerEvents = TCPServerEvents> extends EventEmitter<M> { | |
| 100 | readonly listening: boolean | |
| 101 | readonly closing: boolean | |
| 102 | readonly connections: Set<TCPSocket> | |
| 103 | ||
| 104 | address(): TCPSocketAddress | |
| 105 | ||
| 106 | listen( | |
| 107 | port?: number, | |
| 108 | host?: string, | |
| 109 | backlog?: number, | |
| 110 | opts?: TCPServerListenOptions, | |
| 111 | onlistening?: () => void | |
| 112 | ): this | |
| 113 | listen(port: number, host: string, backlog: number, onlistening: () => void): this | |
| 114 | listen(port: number, host: string, onlistening: () => void): this | |
| 115 | listen(port: number, onlistening: () => void): this | |
| 116 | listen(onlistening: () => void): this | |
| 117 | ||
| 118 | close(onclose?: (err?: Error) => void): this | |
| 119 | ||
| 120 | ref(): this | |
| 121 | unref(): this | |
| 122 | } | |
| 123 | ||
| 124 | declare class TCPServer<M extends TCPServerEvents = TCPServerEvents> extends EventEmitter<M> { | |
| 125 | constructor(opts?: TCPServerOptions, onconnection?: () => void) | |
| 126 | constructor(onconnection: () => void) | |
| 127 | } | |
| 128 | ||
| 129 | declare function createConnection( | |
| 130 | port: number, | |
| 131 | host?: string, | |
| 132 | opts?: TCPSocketOptions & TCPSocketConnectOptions, | |
| 133 | onconnect?: () => void | |
| 134 | ): TCPSocket | |
| 135 | ||
| 136 | declare function createConnection(port: number, host: string, onconnect: () => void): TCPSocket | |
| 137 | ||
| 138 | declare function createConnection(port: number, onconnect: () => void): TCPSocket | |
| 139 | ||
| 140 | declare function createConnection( | |
| 141 | opts: TCPSocketOptions & TCPSocketConnectOptions, | |
| 142 | onconnect: () => void | |
| 143 | ): TCPSocket | |
| 144 | ||
| 145 | declare function createConnection(opts: TCPSocketOptions & TCPSocketConnectOptions): TCPSocket | |
| 146 | ||
| 147 | declare function createServer(opts?: TCPServerOptions, onconnection?: () => void): TCPServer | |
| 148 | ||
| 149 | declare function createServer(onconnection: () => void): TCPServer | |
| 150 | ||
| 151 | declare function isIP(host: string): IPFamily | 0 | |
| 152 | ||
| 153 | declare function isIPv4(host: string): boolean | |
| 154 | ||
| 155 | declare function isIPv6(host: string): boolean | |
| 156 | ||
| 157 | export { | |
| 158 | type TCPSocket, | |
| 159 | TCPSocket as Socket, | |
| 160 | type TCPServer, | |
| 161 | TCPServer as Server, | |
| 162 | createConnection, | |
| 163 | createConnection as connect, | |
| 164 | createServer, | |
| 165 | constants, | |
| 166 | type TCPError, | |
| 167 | TCPError as errors, | |
| 168 | type IPFamily, | |
| 169 | isIP, | |
| 170 | isIPv4, | |
| 171 | isIPv6, | |
| 172 | type TCPSocketAddress, | |
| 173 | type TCPSocketEvents, | |
| 174 | type TCPSocketOptions, | |
| 175 | type TCPSocketConnectOptions, | |
| 176 | type TCPServerEvents, | |
| 177 | type TCPServerOptions, | |
| 178 | type TCPServerListenOptions | |
| 179 | } |
| … | … | @@ -0,0 +1,681 @@ |
| 1 | const EventEmitter = require('bare-events') | |
| 2 | const { Duplex } = require('bare-stream') | |
| 3 | const dns = require('bare-dns') | |
| 4 | const binding = require('./binding') | |
| 5 | const constants = require('./lib/constants') | |
| 6 | const errors = require('./lib/errors') | |
| 7 | const ip = require('./lib/ip') | |
| 8 | ||
| 9 | const defaultReadBufferSize = 65536 | |
| 10 | const empty = Buffer.alloc(0) | |
| 11 | ||
| 12 | exports.Socket = class TCPSocket extends Duplex { | |
| 13 | constructor(opts = {}) { | |
| 14 | const { readBufferSize = defaultReadBufferSize, allowHalfOpen = true, eagerOpen = true } = opts | |
| 15 | ||
| 16 | super({ eagerOpen }) | |
| 17 | ||
| 18 | this._state = 0 | |
| 19 | ||
| 20 | this._allowHalfOpen = allowHalfOpen | |
| 21 | ||
| 22 | this._keepAlive = 0 | |
| 23 | this._keepAliveInitialDelay = 0 | |
| 24 | this._noDelay = 0 | |
| 25 | ||
| 26 | this._localAddress = null | |
| 27 | this._remoteAddress = null | |
| 28 | ||
| 29 | this._pendingOpen = null | |
| 30 | this._pendingWrite = null | |
| 31 | this._pendingFinal = null | |
| 32 | this._pendingDestroy = null | |
| 33 | ||
| 34 | this._timer = null | |
| 35 | this._timeout = 0 | |
| 36 | ||
| 37 | this._buffer = Buffer.alloc(readBufferSize) | |
| 38 | ||
| 39 | this._addresses = null | |
| 40 | this._errors = null | |
| 41 | ||
| 42 | this._handle = binding.init( | |
| 43 | this._buffer, | |
| 44 | this, | |
| 45 | noop, | |
| 46 | this._onconnect, | |
| 47 | this._onreset, | |
| 48 | this._onread, | |
| 49 | this._onwrite, | |
| 50 | this._onfinal, | |
| 51 | this._onclose | |
| 52 | ) | |
| 53 | } | |
| 54 | ||
| 55 | get connecting() { | |
| 56 | return (this._state & constants.state.CONNECTING) !== 0 | |
| 57 | } | |
| 58 | ||
| 59 | get pending() { | |
| 60 | return (this._state & constants.state.CONNECTED) === 0 | |
| 61 | } | |
| 62 | ||
| 63 | get timeout() { | |
| 64 | return this._timeout || undefined // For Node.js compatibility | |
| 65 | } | |
| 66 | ||
| 67 | get readyState() { | |
| 68 | if (this._state & constants.state.CONNECTED) { | |
| 69 | return 'open' | |
| 70 | } | |
| 71 | ||
| 72 | return 'opening' | |
| 73 | } | |
| 74 | ||
| 75 | get localAddress() { | |
| 76 | if (this._localAddress) return this._localAddress.address | |
| 77 | } | |
| 78 | ||
| 79 | get localFamily() { | |
| 80 | if (this._localAddress) return `IPv${this._localAddress.family}` | |
| 81 | } | |
| 82 | ||
| 83 | get localPort() { | |
| 84 | if (this._localAddress) return this._localAddress.port | |
| 85 | } | |
| 86 | ||
| 87 | get remoteAddress() { | |
| 88 | if (this._remoteAddress) return this._remoteAddress.address | |
| 89 | } | |
| 90 | ||
| 91 | get remoteFamily() { | |
| 92 | if (this._remoteAddress) return `IPv${this._remoteAddress.family}` | |
| 93 | } | |
| 94 | ||
| 95 | get remotePort() { | |
| 96 | if (this._remoteAddress) return this._remoteAddress.port | |
| 97 | } | |
| 98 | ||
| 99 | connect(port, host = 'localhost', opts = {}, onconnect) { | |
| 100 | if (this._state & constants.state.CONNECTING || this._state & constants.state.CONNECTED) { | |
| 101 | throw errors.SOCKET_ALREADY_CONNECTED('Socket is already connected') | |
| 102 | } | |
| 103 | ||
| 104 | this._state |= constants.state.CONNECTING | |
| 105 | ||
| 106 | if (typeof host === 'function') { | |
| 107 | onconnect = host | |
| 108 | host = 'localhost' | |
| 109 | } else if (typeof opts === 'function') { | |
| 110 | onconnect = opts | |
| 111 | opts = {} | |
| 112 | } | |
| 113 | ||
| 114 | let family = 0 | |
| 115 | ||
| 116 | if (typeof port === 'object' && port !== null) { | |
| 117 | opts = port || {} | |
| 118 | port = opts.port || 0 | |
| 119 | host = opts.host || 'localhost' | |
| 120 | family = opts.family || 0 | |
| 121 | } | |
| 122 | ||
| 123 | if (!host) host = 'localhost' | |
| 124 | ||
| 125 | const { | |
| 126 | lookup = dns.lookup, | |
| 127 | hints, | |
| 128 | keepAlive = false, | |
| 129 | keepAliveInitialDelay = 0, | |
| 130 | noDelay = false, | |
| 131 | timeout | |
| 132 | } = opts | |
| 133 | ||
| 134 | const type = ip.isIP(host) | |
| 135 | ||
| 136 | if (type === 0) { | |
| 137 | lookup(host, { all: true, family, hints }, (err, addresses) => { | |
| 138 | if (this._state & constants.state.CLOSING) return | |
| 139 | ||
| 140 | this._state &= ~constants.state.CONNECTING | |
| 141 | ||
| 142 | if (err || addresses.length === 0) { | |
| 143 | if (!err) { | |
| 144 | err = new Error(`No address found for host "${host}"`) | |
| 145 | err.code = 'ENOTFOUND' | |
| 146 | } | |
| 147 | ||
| 148 | this.emit('lookup', err, null, 0, host) | |
| 149 | ||
| 150 | if (this._pendingOpen) this._continueOpen(err) | |
| 151 | else this.destroy(err) | |
| 152 | return | |
| 153 | } | |
| 154 | ||
| 155 | for (const { address, family } of addresses) { | |
| 156 | this.emit('lookup', null, address, family, host) | |
| 157 | } | |
| 158 | ||
| 159 | const [{ address, family }, ...rest] = addresses | |
| 160 | ||
| 161 | if (rest.length > 0) { | |
| 162 | this._addresses = rest.map(({ address, family }) => [ | |
| 163 | port, | |
| 164 | address, | |
| 165 | { ...opts, family }, | |
| 166 | onconnect | |
| 167 | ]) | |
| 168 | ||
| 169 | this._errors = [] | |
| 170 | } | |
| 171 | ||
| 172 | this.connect(port, address, { ...opts, family }, onconnect) | |
| 173 | }) | |
| 174 | ||
| 175 | return this | |
| 176 | } | |
| 177 | ||
| 178 | family = type | |
| 179 | ||
| 180 | try { | |
| 181 | binding.connect(this._handle, port, host, family) | |
| 182 | ||
| 183 | if (keepAlive) { | |
| 184 | this._keepAlive = keepAlive | |
| 185 | this._keepAliveInitialDelay = keepAliveInitialDelay | |
| 186 | } | |
| 187 | if (noDelay) this._noDelay = noDelay | |
| 188 | if (timeout) this.setTimeout(timeout) | |
| 189 | ||
| 190 | if (onconnect) this.once('connect', onconnect) | |
| 191 | } catch (err) { | |
| 192 | queueMicrotask(() => { | |
| 193 | if (this._pendingOpen) this._pendingOpen(err) | |
| 194 | else this.destroy(err) | |
| 195 | }) | |
| 196 | } | |
| 197 | ||
| 198 | return this | |
| 199 | } | |
| 200 | ||
| 201 | setKeepAlive(enable = false, delay = 0) { | |
| 202 | if (typeof enable === 'number') { | |
| 203 | delay = enable | |
| 204 | enable = false | |
| 205 | } | |
| 206 | ||
| 207 | delay = Math.floor(delay / 1000) | |
| 208 | ||
| 209 | if (delay === 0) enable = false | |
| 210 | ||
| 211 | binding.keepalive(this._handle, enable, delay) | |
| 212 | ||
| 213 | return this | |
| 214 | } | |
| 215 | ||
| 216 | setNoDelay(enable = true) { | |
| 217 | binding.nodelay(this._handle, enable) | |
| 218 | ||
| 219 | return this | |
| 220 | } | |
| 221 | ||
| 222 | setTimeout(ms, ontimeout) { | |
| 223 | if (ms === 0) { | |
| 224 | clearTimeout(this._timer) | |
| 225 | ||
| 226 | this._timer = null | |
| 227 | } else { | |
| 228 | if (ontimeout) this.once('timeout', ontimeout) | |
| 229 | ||
| 230 | this._timer = setTimeout(() => this.emit('timeout'), ms) | |
| 231 | this._timer.unref() | |
| 232 | } | |
| 233 | ||
| 234 | this._timeout = ms | |
| 235 | ||
| 236 | return this | |
| 237 | } | |
| 238 | ||
| 239 | ref() { | |
| 240 | binding.ref(this._handle) | |
| 241 | ||
| 242 | return this | |
| 243 | } | |
| 244 | ||
| 245 | unref() { | |
| 246 | binding.unref(this._handle) | |
| 247 | ||
| 248 | return this | |
| 249 | } | |
| 250 | ||
| 251 | _open(cb) { | |
| 252 | if (this._state & constants.state.CONNECTED) return cb(null) | |
| 253 | ||
| 254 | this._pendingOpen = cb | |
| 255 | } | |
| 256 | ||
| 257 | _read() { | |
| 258 | if ((this._state & constants.state.READING) === 0) { | |
| 259 | this._state |= constants.state.READING | |
| 260 | ||
| 261 | binding.resume(this._handle) | |
| 262 | } | |
| 263 | } | |
| 264 | ||
| 265 | _writev(batch, cb) { | |
| 266 | this._pendingWrite = [cb, batch] | |
| 267 | ||
| 268 | binding.writev( | |
| 269 | this._handle, | |
| 270 | batch.map(({ chunk }) => chunk) | |
| 271 | ) | |
| 272 | } | |
| 273 | ||
| 274 | _final(cb) { | |
| 275 | this._pendingFinal = cb | |
| 276 | ||
| 277 | binding.end(this._handle) | |
| 278 | } | |
| 279 | ||
| 280 | _predestroy() { | |
| 281 | if (this._state & constants.state.CLOSING) return | |
| 282 | this._state |= constants.state.CLOSING | |
| 283 | ||
| 284 | binding.close(this._handle) | |
| 285 | } | |
| 286 | ||
| 287 | _destroy(err, cb) { | |
| 288 | if (this._state & constants.state.CLOSING) return cb(err) | |
| 289 | this._state |= constants.state.CLOSING | |
| 290 | ||
| 291 | this._pendingDestroy = cb | |
| 292 | ||
| 293 | binding.close(this._handle) | |
| 294 | } | |
| 295 | ||
| 296 | _continueOpen(err) { | |
| 297 | if (this._pendingOpen === null) return | |
| 298 | const cb = this._pendingOpen | |
| 299 | this._pendingOpen = null | |
| 300 | cb(err) | |
| 301 | } | |
| 302 | ||
| 303 | _continueWrite(err) { | |
| 304 | if (this._pendingWrite === null) return | |
| 305 | const cb = this._pendingWrite[0] | |
| 306 | this._pendingWrite = null | |
| 307 | cb(err) | |
| 308 | } | |
| 309 | ||
| 310 | _continueFinal(err) { | |
| 311 | if (this._pendingFinal === null) return | |
| 312 | const cb = this._pendingFinal | |
| 313 | this._pendingFinal = null | |
| 314 | cb(err) | |
| 315 | } | |
| 316 | ||
| 317 | _continueDestroy() { | |
| 318 | if (this._pendingDestroy === null) return | |
| 319 | const cb = this._pendingDestroy | |
| 320 | this._pendingDestroy = null | |
| 321 | cb(null) | |
| 322 | } | |
| 323 | ||
| 324 | _reset() { | |
| 325 | this._state = 0 | |
| 326 | ||
| 327 | this._localAddress = null | |
| 328 | this._remoteAddress = null | |
| 329 | ||
| 330 | binding.reset(this._handle) | |
| 331 | } | |
| 332 | ||
| 333 | _onconnect(err) { | |
| 334 | if (err) { | |
| 335 | if (this._addresses !== null) { | |
| 336 | this._errors.push(err) | |
| 337 | ||
| 338 | if (this._addresses.length > 0) return this._reset() | |
| 339 | ||
| 340 | err = this._errors.length === 1 ? this._errors[0] : new AggregateError(this._errors) | |
| 341 | } | |
| 342 | ||
| 343 | if (this._pendingOpen) this._continueOpen(err) | |
| 344 | else this.destroy(err) | |
| 345 | return | |
| 346 | } | |
| 347 | ||
| 348 | if (this._keepAlive) this.setKeepAlive(this._keepAlive, this._keepAliveInitialDelay) | |
| 349 | if (this._noDelay) this.setNoDelay() | |
| 350 | ||
| 351 | this._localAddress = binding.address(this._handle, true) | |
| 352 | this._remoteAddress = binding.address(this._handle, false) | |
| 353 | ||
| 354 | this._state |= constants.state.CONNECTED | |
| 355 | this._state &= ~constants.state.CONNECTING | |
| 356 | this._continueOpen() | |
| 357 | ||
| 358 | this.emit('connect') | |
| 359 | } | |
| 360 | ||
| 361 | _onreset(err) { | |
| 362 | if (err) { | |
| 363 | this._errors.push(err) | |
| 364 | this.destroy(this._errors.length === 1 ? this._errors[0] : new AggregateError(this._errors)) | |
| 365 | return | |
| 366 | } | |
| 367 | ||
| 368 | this.connect(...this._addresses.shift()) | |
| 369 | } | |
| 370 | ||
| 371 | _onread(err, read) { | |
| 372 | if (this._timer) this._timer.refresh() | |
| 373 | ||
| 374 | if (err) { | |
| 375 | this.destroy(err) | |
| 376 | return | |
| 377 | } | |
| 378 | ||
| 379 | if (read === 0) { | |
| 380 | this.push(null) | |
| 381 | if (this._allowHalfOpen === false) this.end() | |
| 382 | return | |
| 383 | } | |
| 384 | ||
| 385 | const copy = Buffer.allocUnsafe(read) | |
| 386 | copy.set(this._buffer.subarray(0, read)) | |
| 387 | ||
| 388 | if (this.push(copy) === false && this.destroying === false) { | |
| 389 | this._state &= ~constants.state.READING | |
| 390 | ||
| 391 | binding.pause(this._handle) | |
| 392 | } | |
| 393 | } | |
| 394 | ||
| 395 | _onwrite(err) { | |
| 396 | if (this._timer) this._timer.refresh() | |
| 397 | ||
| 398 | this._continueWrite(err) | |
| 399 | } | |
| 400 | ||
| 401 | _onfinal(err) { | |
| 402 | this._continueFinal(err) | |
| 403 | } | |
| 404 | ||
| 405 | _onclose() { | |
| 406 | clearTimeout(this._timer) | |
| 407 | ||
| 408 | this._continueDestroy() | |
| 409 | } | |
| 410 | } | |
| 411 | ||
| 412 | exports.Server = class TCPServer extends EventEmitter { | |
| 413 | constructor(opts = {}, onconnection) { | |
| 414 | if (typeof opts === 'function') { | |
| 415 | onconnection = opts | |
| 416 | opts = {} | |
| 417 | } | |
| 418 | ||
| 419 | super() | |
| 420 | ||
| 421 | const { | |
| 422 | readBufferSize = defaultReadBufferSize, | |
| 423 | allowHalfOpen = true, | |
| 424 | keepAlive = false, | |
| 425 | keepAliveInitialDelay = 0, | |
| 426 | noDelay = false, | |
| 427 | pauseOnConnect = false | |
| 428 | } = opts | |
| 429 | ||
| 430 | this._state = 0 | |
| 431 | ||
| 432 | this._readBufferSize = readBufferSize | |
| 433 | this._allowHalfOpen = allowHalfOpen | |
| 434 | this._keepAlive = keepAlive | |
| 435 | this._keepAliveInitialDelay = keepAliveInitialDelay | |
| 436 | this._noDelay = noDelay | |
| 437 | this._pauseOnConnect = pauseOnConnect | |
| 438 | ||
| 439 | this._address = null | |
| 440 | this._connections = new Set() | |
| 441 | ||
| 442 | this._error = null | |
| 443 | this._handle = null | |
| 444 | ||
| 445 | if (onconnection) this.on('connection', onconnection) | |
| 446 | } | |
| 447 | ||
| 448 | get listening() { | |
| 449 | return (this._state & constants.state.BOUND) !== 0 | |
| 450 | } | |
| 451 | ||
| 452 | get closing() { | |
| 453 | return (this._state & constants.state.CLOSING) !== 0 | |
| 454 | } | |
| 455 | ||
| 456 | get connections() { | |
| 457 | return this._connections | |
| 458 | } | |
| 459 | ||
| 460 | address() { | |
| 461 | if ((this._state & constants.state.BOUND) === 0) return null | |
| 462 | ||
| 463 | const { address, family, port } = this._address | |
| 464 | ||
| 465 | return { address, family: `IPv${family}`, port } | |
| 466 | } | |
| 467 | ||
| 468 | listen(port = 0, host = 'localhost', backlog = 511, opts = {}, onlistening) { | |
| 469 | if (this._state & constants.state.BINDING || this._state & constants.state.BOUND) { | |
| 470 | throw errors.SERVER_ALREADY_LISTENING('Server is already listening') | |
| 471 | } | |
| 472 | ||
| 473 | if (this._state & constants.state.CLOSING) { | |
| 474 | throw errors.SERVER_IS_CLOSED('Server is closed') | |
| 475 | } | |
| 476 | ||
| 477 | this._state |= constants.state.BINDING | |
| 478 | ||
| 479 | if (typeof port === 'function') { | |
| 480 | onlistening = port | |
| 481 | port = 0 | |
| 482 | } else if (typeof host === 'function') { | |
| 483 | onlistening = host | |
| 484 | host = 'localhost' | |
| 485 | } else if (typeof backlog === 'function') { | |
| 486 | onlistening = backlog | |
| 487 | backlog = 511 | |
| 488 | } else if (typeof opts === 'function') { | |
| 489 | onlistening = opts | |
| 490 | opts = {} | |
| 491 | } | |
| 492 | ||
| 493 | let family = 0 | |
| 494 | ||
| 495 | if (typeof port === 'object' && port !== null) { | |
| 496 | opts = port || {} | |
| 497 | port = opts.port || 0 | |
| 498 | host = opts.host || 'localhost' | |
| 499 | family = opts.family || 0 | |
| 500 | backlog = opts.backlog || 511 | |
| 501 | } | |
| 502 | ||
| 503 | if (!host) host = 'localhost' | |
| 504 | if (!backlog) backlog = 511 | |
| 505 | ||
| 506 | const { lookup = dns.lookup, hints } = opts | |
| 507 | ||
| 508 | const type = ip.isIP(host) | |
| 509 | ||
| 510 | if (type === 0) { | |
| 511 | lookup(host, { family, hints }, (err, address, family) => { | |
| 512 | if (this._state & constants.state.CLOSING) return | |
| 513 | ||
| 514 | this.emit('lookup', err, address, family, host) | |
| 515 | ||
| 516 | this._state &= ~constants.state.BINDING | |
| 517 | ||
| 518 | if (err) return this.emit('error', err) | |
| 519 | ||
| 520 | this.listen(port, address, backlog, { ...opts, family }, onlistening) | |
| 521 | }) | |
| 522 | ||
| 523 | return this | |
| 524 | } | |
| 525 | ||
| 526 | family = type | |
| 527 | ||
| 528 | this._handle = binding.init( | |
| 529 | empty, | |
| 530 | this, | |
| 531 | this._onconnection, | |
| 532 | noop, | |
| 533 | noop, | |
| 534 | noop, | |
| 535 | noop, | |
| 536 | noop, | |
| 537 | this._onclose | |
| 538 | ) | |
| 539 | ||
| 540 | if (this._state & constants.state.UNREFED) binding.unref(this._handle) | |
| 541 | ||
| 542 | try { | |
| 543 | binding.bind(this._handle, port, host, backlog, family) | |
| 544 | ||
| 545 | this._address = binding.address(this._handle, true) | |
| 546 | ||
| 547 | this._state |= constants.state.BOUND | |
| 548 | this._state &= ~constants.state.BINDING | |
| 549 | ||
| 550 | if (onlistening) this.once('listening', onlistening) | |
| 551 | ||
| 552 | queueMicrotask(() => this.emit('listening')) | |
| 553 | } catch (err) { | |
| 554 | this._error = err | |
| 555 | ||
| 556 | binding.close(this._handle) | |
| 557 | } | |
| 558 | ||
| 559 | return this | |
| 560 | } | |
| 561 | ||
| 562 | close(onclose) { | |
| 563 | if (onclose) this.once('close', onclose) | |
| 564 | ||
| 565 | if (this._state & constants.state.CLOSING) return this | |
| 566 | this._state |= constants.state.CLOSING | |
| 567 | ||
| 568 | this._closeMaybe() | |
| 569 | ||
| 570 | return this | |
| 571 | } | |
| 572 | ||
| 573 | ref() { | |
| 574 | this._state &= ~constants.state.UNREFED | |
| 575 | ||
| 576 | if (this._handle !== null) binding.ref(this._handle) | |
| 577 | ||
| 578 | return this | |
| 579 | } | |
| 580 | ||
| 581 | unref() { | |
| 582 | this._state |= constants.state.UNREFED | |
| 583 | ||
| 584 | if (this._handle !== null) binding.unref(this._handle) | |
| 585 | ||
| 586 | return this | |
| 587 | } | |
| 588 | ||
| 589 | _closeMaybe() { | |
| 590 | if (this._state & constants.state.CLOSING && this._connections.size === 0) { | |
| 591 | if (this._handle !== null) binding.close(this._handle) | |
| 592 | else queueMicrotask(() => this.emit('close')) | |
| 593 | } | |
| 594 | } | |
| 595 | ||
| 596 | _onconnection(err) { | |
| 597 | if (err) { | |
| 598 | this.emit('error', err) | |
| 599 | return | |
| 600 | } | |
| 601 | ||
| 602 | if (this._state & constants.state.CLOSING) return | |
| 603 | ||
| 604 | const socket = new exports.Socket({ | |
| 605 | readBufferSize: this._readBufferSize, | |
| 606 | allowHalfOpen: this._allowHalfOpen, | |
| 607 | eagerOpen: !this._pauseOnConnect | |
| 608 | }) | |
| 609 | ||
| 610 | try { | |
| 611 | binding.accept(this._handle, socket._handle) | |
| 612 | ||
| 613 | socket._localAddress = binding.address(socket._handle, true) | |
| 614 | socket._remoteAddress = binding.address(socket._handle, false) | |
| 615 | ||
| 616 | socket._state |= constants.state.CONNECTED | |
| 617 | ||
| 618 | this._connections.add(socket) | |
| 619 | ||
| 620 | if (this._keepAlive) socket.setKeepAlive(this._keepAlive, this._keepAliveInitialDelay) | |
| 621 | if (this._noDelay) socket.setNoDelay() | |
| 622 | ||
| 623 | socket.on('close', () => { | |
| 624 | this._connections.delete(socket) | |
| 625 | this._closeMaybe() | |
| 626 | }) | |
| 627 | ||
| 628 | this.emit('connection', socket) | |
| 629 | } catch (err) { | |
| 630 | socket.destroy() | |
| 631 | ||
| 632 | this.emit('error', err) | |
| 633 | } | |
| 634 | } | |
| 635 | ||
| 636 | _onclose() { | |
| 637 | const err = this._error | |
| 638 | ||
| 639 | this._state &= ~constants.state.BINDING | |
| 640 | this._error = null | |
| 641 | this._handle = null | |
| 642 | this._address = null | |
| 643 | ||
| 644 | if (err) this.emit('error', err) | |
| 645 | else this.emit('close') | |
| 646 | } | |
| 647 | } | |
| 648 | ||
| 649 | exports.constants = constants | |
| 650 | exports.errors = errors | |
| 651 | ||
| 652 | exports.isIP = ip.isIP | |
| 653 | exports.isIPv4 = ip.isIPv4 | |
| 654 | exports.isIPv6 = ip.isIPv6 | |
| 655 | ||
| 656 | exports.createConnection = function createConnection(port, host, opts, onconnect) { | |
| 657 | if (typeof host === 'function') { | |
| 658 | onconnect = host | |
| 659 | host = 'localhost' | |
| 660 | } else if (typeof opts === 'function') { | |
| 661 | onconnect = opts | |
| 662 | opts = {} | |
| 663 | } | |
| 664 | ||
| 665 | if (typeof port === 'object' && port !== null) { | |
| 666 | opts = port || {} | |
| 667 | port = opts.port || 0 | |
| 668 | host = opts.host || 'localhost' | |
| 669 | } | |
| 670 | ||
| 671 | return new exports.Socket(opts).connect(port, host, opts, onconnect) | |
| 672 | } | |
| 673 | ||
| 674 | // For Node.js compatibility | |
| 675 | exports.connect = exports.createConnection | |
| 676 | ||
| 677 | exports.createServer = function createServer(opts, onconnection) { | |
| 678 | return new exports.Server(opts, onconnection) | |
| 679 | } | |
| 680 | ||
| 681 | function noop() {} |
| … | … | @@ -0,0 +1,13 @@ |
| 1 | declare const constants: { | |
| 2 | state: { | |
| 3 | CONNECTING: number | |
| 4 | CONNECTED: number | |
| 5 | BINDING: number | |
| 6 | BOUND: number | |
| 7 | READING: number | |
| 8 | CLOSING: number | |
| 9 | UNREFED: number | |
| 10 | } | |
| 11 | } | |
| 12 | ||
| 13 | exports = constants |
| … | … | @@ -0,0 +1,11 @@ |
| 1 | module.exports = { | |
| 2 | state: { | |
| 3 | CONNECTING: 0x1, | |
| 4 | CONNECTED: 0x2, | |
| 5 | BINDING: 0x4, | |
| 6 | BOUND: 0x8, | |
| 7 | READING: 0x10, | |
| 8 | CLOSING: 0x20, | |
| 9 | UNREFED: 0x40 | |
| 10 | } | |
| 11 | } |
| … | … | @@ -0,0 +1,5 @@ |
| 1 | declare class TCPError extends Error { | |
| 2 | readonly code: string | |
| 3 | } | |
| 4 | ||
| 5 | export = TCPError |
| … | … | @@ -0,0 +1,30 @@ |
| 1 | module.exports = class TCPError extends Error { | |
| 2 | constructor(msg, fn = TCPError, code = fn.name) { | |
| 3 | super(`${code}: ${msg}`) | |
| 4 | this.code = code | |
| 5 | ||
| 6 | if (Error.captureStackTrace) { | |
| 7 | Error.captureStackTrace(this, fn) | |
| 8 | } | |
| 9 | } | |
| 10 | ||
| 11 | get name() { | |
| 12 | return 'TCPError' | |
| 13 | } | |
| 14 | ||
| 15 | static SOCKET_ALREADY_CONNECTED(msg) { | |
| 16 | return new TCPError(msg, TCPError.SOCKET_ALREADY_CONNECTED) | |
| 17 | } | |
| 18 | ||
| 19 | static SERVER_ALREADY_LISTENING(msg) { | |
| 20 | return new TCPError(msg, TCPError.SERVER_ALREADY_LISTENING) | |
| 21 | } | |
| 22 | ||
| 23 | static SERVER_IS_CLOSED(msg) { | |
| 24 | return new TCPError(msg, TCPError.SERVER_IS_CLOSED) | |
| 25 | } | |
| 26 | ||
| 27 | static INVALID_HOST(msg = 'Unrecognizable host format') { | |
| 28 | return new TCPError(msg, TCPError.INVALID_HOST) | |
| 29 | } | |
| 30 | } |
| … | … | @@ -0,0 +1,52 @@ |
| 1 | // Copyright Joyent, Inc. and other Node contributors. | |
| 2 | // | |
| 3 | // Permission is hereby granted, free of charge, to any person obtaining a | |
| 4 | // copy of this software and associated documentation files (the | |
| 5 | // "Software"), to deal in the Software without restriction, including | |
| 6 | // without limitation the rights to use, copy, modify, merge, publish, | |
| 7 | // distribute, sublicense, and/or sell copies of the Software, and to permit | |
| 8 | // persons to whom the Software is furnished to do so, subject to the | |
| 9 | // following conditions: | |
| 10 | // | |
| 11 | // The above copyright notice and this permission notice shall be included | |
| 12 | // in all copies or substantial portions of the Software. | |
| 13 | // | |
| 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS | |
| 15 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | |
| 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN | |
| 17 | // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, | |
| 18 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR | |
| 19 | // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE | |
| 20 | // USE OR OTHER DEALINGS IN THE SOFTWARE. | |
| 21 | ||
| 22 | const v4Seg = '(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])' | |
| 23 | const v4Str = `(${v4Seg}[.]){3}${v4Seg}` | |
| 24 | const IPv4Pattern = new RegExp(`^${v4Str}$`) | |
| 25 | ||
| 26 | const v6Seg = '(?:[0-9a-fA-F]{1,4})' | |
| 27 | const IPv6Pattern = new RegExp( | |
| 28 | '^(' + | |
| 29 | `(?:${v6Seg}:){7}(?:${v6Seg}|:)|` + | |
| 30 | `(?:${v6Seg}:){6}(?:${v4Str}|:${v6Seg}|:)|` + | |
| 31 | `(?:${v6Seg}:){5}(?::${v4Str}|(:${v6Seg}){1,2}|:)|` + | |
| 32 | `(?:${v6Seg}:){4}(?:(:${v6Seg}){0,1}:${v4Str}|(:${v6Seg}){1,3}|:)|` + | |
| 33 | `(?:${v6Seg}:){3}(?:(:${v6Seg}){0,2}:${v4Str}|(:${v6Seg}){1,4}|:)|` + | |
| 34 | `(?:${v6Seg}:){2}(?:(:${v6Seg}){0,3}:${v4Str}|(:${v6Seg}){1,5}|:)|` + | |
| 35 | `(?:${v6Seg}:){1}(?:(:${v6Seg}){0,4}:${v4Str}|(:${v6Seg}){1,6}|:)|` + | |
| 36 | `(?::((?::${v6Seg}){0,5}:${v4Str}|(?::${v6Seg}){1,7}|:))` + | |
| 37 | ')(%[0-9a-zA-Z-.:]{1,})?$' | |
| 38 | ) | |
| 39 | ||
| 40 | const isIPv4 = (exports.isIPv4 = function isIPv4(host) { | |
| 41 | return IPv4Pattern.test(host) | |
| 42 | }) | |
| 43 | ||
| 44 | const isIPv6 = (exports.isIPv6 = function isIPv6(host) { | |
| 45 | return IPv6Pattern.test(host) | |
| 46 | }) | |
| 47 | ||
| 48 | exports.isIP = function isIP(host) { | |
| 49 | if (isIPv4(host)) return 4 | |
| 50 | if (isIPv6(host)) return 6 | |
| 51 | return 0 | |
| 52 | } |
| … | … | @@ -0,0 +1,51 @@ |
| 1 | { | |
| 2 | "name": "bare-tcp", | |
| 3 | "version": "2.2.7", | |
| 4 | "description": "Native TCP sockets for JavaScript", | |
| 5 | "exports": { | |
| 6 | ".": { | |
| 7 | "types": "./index.d.ts", | |
| 8 | "default": "./index.js" | |
| 9 | }, | |
| 10 | "./package": "./package.json", | |
| 11 | "./constants": "./lib/constants.js", | |
| 12 | "./errors": "./lib/errors.js" | |
| 13 | }, | |
| 14 | "files": [ | |
| 15 | "index.js", | |
| 16 | "index.d.ts", | |
| 17 | "binding.c", | |
| 18 | "binding.js", | |
| 19 | "CMakeLists.txt", | |
| 20 | "lib", | |
| 21 | "prebuilds" | |
| 22 | ], | |
| 23 | "addon": true, | |
| 24 | "scripts": { | |
| 25 | "test": "prettier . --check && bare test.js" | |
| 26 | }, | |
| 27 | "repository": { | |
| 28 | "type": "git", | |
| 29 | "url": "git+https://github.com/holepunchto/bare-tcp.git" | |
| 30 | }, | |
| 31 | "author": "Holepunch", | |
| 32 | "license": "Apache-2.0", | |
| 33 | "bugs": { | |
| 34 | "url": "https://github.com/holepunchto/bare-tcp/issues" | |
| 35 | }, | |
| 36 | "homepage": "https://github.com/holepunchto/bare-tcp#readme", | |
| 37 | "engines": { | |
| 38 | "bare": ">=1.16.0" | |
| 39 | }, | |
| 40 | "dependencies": { | |
| 41 | "bare-dns": "^2.0.4", | |
| 42 | "bare-events": "^2.5.4", | |
| 43 | "bare-stream": "^2.6.4" | |
| 44 | }, | |
| 45 | "devDependencies": { | |
| 46 | "brittle": "^3.5.0", | |
| 47 | "cmake-bare": "^1.1.6", | |
| 48 | "prettier": "^3.4.1", | |
| 49 | "prettier-config-holepunch": "^2.0.0" | |
| 50 | } | |
| 51 | } |
| … | … | @@ -0,0 +1,34 @@ |
| 1 | cmake_minimum_required(VERSION 4.0) | |
| 2 | ||
| 3 | find_package(cmake-bare REQUIRED PATHS node_modules/cmake-bare) | |
| 4 | find_package(cmake-fetch REQUIRED PATHS node_modules/cmake-fetch) | |
| 5 | ||
| 6 | project(bare_tls C CXX) | |
| 7 | ||
| 8 | bare_target(target) | |
| 9 | ||
| 10 | if(target MATCHES "win32") | |
| 11 | add_definitions(-DWIN32_LEAN_AND_MEAN) | |
| 12 | endif() | |
| 13 | ||
| 14 | if(target MATCHES "win32-x64") | |
| 15 | enable_language(ASM_NASM) | |
| 16 | else() | |
| 17 | enable_language(ASM) | |
| 18 | endif() | |
| 19 | ||
| 20 | fetch_package("github:google/boringssl#0.20260211.0") | |
| 21 | ||
| 22 | add_bare_module(bare_tls) | |
| 23 | ||
| 24 | target_sources( | |
| 25 | ${bare_tls} | |
| 26 | PRIVATE | |
| 27 | binding.c | |
| 28 | ) | |
| 29 | ||
| 30 | target_link_libraries( | |
| 31 | ${bare_tls} | |
| 32 | PUBLIC | |
| 33 | ssl | |
| 34 | ) |
| … | … | @@ -0,0 +1,201 @@ |
| 1 | Apache License | |
| 2 | Version 2.0, January 2004 | |
| 3 | http://www.apache.org/licenses/ | |
| 4 | ||
| 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | |
| 6 | ||
| 7 | 1. Definitions. | |
| 8 | ||
| 9 | "License" shall mean the terms and conditions for use, reproduction, | |
| 10 | and distribution as defined by Sections 1 through 9 of this document. | |
| 11 | ||
| 12 | "Licensor" shall mean the copyright owner or entity authorized by | |
| 13 | the copyright owner that is granting the License. | |
| 14 | ||
| 15 | "Legal Entity" shall mean the union of the acting entity and all | |
| 16 | other entities that control, are controlled by, or are under common | |
| 17 | control with that entity. For the purposes of this definition, | |
| 18 | "control" means (i) the power, direct or indirect, to cause the | |
| 19 | direction or management of such entity, whether by contract or | |
| 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the | |
| 21 | outstanding shares, or (iii) beneficial ownership of such entity. | |
| 22 | ||
| 23 | "You" (or "Your") shall mean an individual or Legal Entity | |
| 24 | exercising permissions granted by this License. | |
| 25 | ||
| 26 | "Source" form shall mean the preferred form for making modifications, | |
| 27 | including but not limited to software source code, documentation | |
| 28 | source, and configuration files. | |
| 29 | ||
| 30 | "Object" form shall mean any form resulting from mechanical | |
| 31 | transformation or translation of a Source form, including but | |
| 32 | not limited to compiled object code, generated documentation, | |
| 33 | and conversions to other media types. | |
| 34 | ||
| 35 | "Work" shall mean the work of authorship, whether in Source or | |
| 36 | Object form, made available under the License, as indicated by a | |
| 37 | copyright notice that is included in or attached to the work | |
| 38 | (an example is provided in the Appendix below). | |
| 39 | ||
| 40 | "Derivative Works" shall mean any work, whether in Source or Object | |
| 41 | form, that is based on (or derived from) the Work and for which the | |
| 42 | editorial revisions, annotations, elaborations, or other modifications | |
| 43 | represent, as a whole, an original work of authorship. For the purposes | |
| 44 | of this License, Derivative Works shall not include works that remain | |
| 45 | separable from, or merely link (or bind by name) to the interfaces of, | |
| 46 | the Work and Derivative Works thereof. | |
| 47 | ||
| 48 | "Contribution" shall mean any work of authorship, including | |
| 49 | the original version of the Work and any modifications or additions | |
| 50 | to that Work or Derivative Works thereof, that is intentionally | |
| 51 | submitted to Licensor for inclusion in the Work by the copyright owner | |
| 52 | or by an individual or Legal Entity authorized to submit on behalf of | |
| 53 | the copyright owner. For the purposes of this definition, "submitted" | |
| 54 | means any form of electronic, verbal, or written communication sent | |
| 55 | to the Licensor or its representatives, including but not limited to | |
| 56 | communication on electronic mailing lists, source code control systems, | |
| 57 | and issue tracking systems that are managed by, or on behalf of, the | |
| 58 | Licensor for the purpose of discussing and improving the Work, but | |
| 59 | excluding communication that is conspicuously marked or otherwise | |
| 60 | designated in writing by the copyright owner as "Not a Contribution." | |
| 61 | ||
| 62 | "Contributor" shall mean Licensor and any individual or Legal Entity | |
| 63 | on behalf of whom a Contribution has been received by Licensor and | |
| 64 | subsequently incorporated within the Work. | |
| 65 | ||
| 66 | 2. Grant of Copyright License. Subject to the terms and conditions of | |
| 67 | this License, each Contributor hereby grants to You a perpetual, | |
| 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 69 | copyright license to reproduce, prepare Derivative Works of, | |
| 70 | publicly display, publicly perform, sublicense, and distribute the | |
| 71 | Work and such Derivative Works in Source or Object form. | |
| 72 | ||
| 73 | 3. Grant of Patent License. Subject to the terms and conditions of | |
| 74 | this License, each Contributor hereby grants to You a perpetual, | |
| 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 76 | (except as stated in this section) patent license to make, have made, | |
| 77 | use, offer to sell, sell, import, and otherwise transfer the Work, | |
| 78 | where such license applies only to those patent claims licensable | |
| 79 | by such Contributor that are necessarily infringed by their | |
| 80 | Contribution(s) alone or by combination of their Contribution(s) | |
| 81 | with the Work to which such Contribution(s) was submitted. If You | |
| 82 | institute patent litigation against any entity (including a | |
| 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work | |
| 84 | or a Contribution incorporated within the Work constitutes direct | |
| 85 | or contributory patent infringement, then any patent licenses | |
| 86 | granted to You under this License for that Work shall terminate | |
| 87 | as of the date such litigation is filed. | |
| 88 | ||
| 89 | 4. Redistribution. You may reproduce and distribute copies of the | |
| 90 | Work or Derivative Works thereof in any medium, with or without | |
| 91 | modifications, and in Source or Object form, provided that You | |
| 92 | meet the following conditions: | |
| 93 | ||
| 94 | (a) You must give any other recipients of the Work or | |
| 95 | Derivative Works a copy of this License; and | |
| 96 | ||
| 97 | (b) You must cause any modified files to carry prominent notices | |
| 98 | stating that You changed the files; and | |
| 99 | ||
| 100 | (c) You must retain, in the Source form of any Derivative Works | |
| 101 | that You distribute, all copyright, patent, trademark, and | |
| 102 | attribution notices from the Source form of the Work, | |
| 103 | excluding those notices that do not pertain to any part of | |
| 104 | the Derivative Works; and | |
| 105 | ||
| 106 | (d) If the Work includes a "NOTICE" text file as part of its | |
| 107 | distribution, then any Derivative Works that You distribute must | |
| 108 | include a readable copy of the attribution notices contained | |
| 109 | within such NOTICE file, excluding those notices that do not | |
| 110 | pertain to any part of the Derivative Works, in at least one | |
| 111 | of the following places: within a NOTICE text file distributed | |
| 112 | as part of the Derivative Works; within the Source form or | |
| 113 | documentation, if provided along with the Derivative Works; or, | |
| 114 | within a display generated by the Derivative Works, if and | |
| 115 | wherever such third-party notices normally appear. The contents | |
| 116 | of the NOTICE file are for informational purposes only and | |
| 117 | do not modify the License. You may add Your own attribution | |
| 118 | notices within Derivative Works that You distribute, alongside | |
| 119 | or as an addendum to the NOTICE text from the Work, provided | |
| 120 | that such additional attribution notices cannot be construed | |
| 121 | as modifying the License. | |
| 122 | ||
| 123 | You may add Your own copyright statement to Your modifications and | |
| 124 | may provide additional or different license terms and conditions | |
| 125 | for use, reproduction, or distribution of Your modifications, or | |
| 126 | for any such Derivative Works as a whole, provided Your use, | |
| 127 | reproduction, and distribution of the Work otherwise complies with | |
| 128 | the conditions stated in this License. | |
| 129 | ||
| 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, | |
| 131 | any Contribution intentionally submitted for inclusion in the Work | |
| 132 | by You to the Licensor shall be under the terms and conditions of | |
| 133 | this License, without any additional terms or conditions. | |
| 134 | Notwithstanding the above, nothing herein shall supersede or modify | |
| 135 | the terms of any separate license agreement you may have executed | |
| 136 | with Licensor regarding such Contributions. | |
| 137 | ||
| 138 | 6. Trademarks. This License does not grant permission to use the trade | |
| 139 | names, trademarks, service marks, or product names of the Licensor, | |
| 140 | except as required for reasonable and customary use in describing the | |
| 141 | origin of the Work and reproducing the content of the NOTICE file. | |
| 142 | ||
| 143 | 7. Disclaimer of Warranty. Unless required by applicable law or | |
| 144 | agreed to in writing, Licensor provides the Work (and each | |
| 145 | Contributor provides its Contributions) on an "AS IS" BASIS, | |
| 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |
| 147 | implied, including, without limitation, any warranties or conditions | |
| 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | |
| 149 | PARTICULAR PURPOSE. You are solely responsible for determining the | |
| 150 | appropriateness of using or redistributing the Work and assume any | |
| 151 | risks associated with Your exercise of permissions under this License. | |
| 152 | ||
| 153 | 8. Limitation of Liability. In no event and under no legal theory, | |
| 154 | whether in tort (including negligence), contract, or otherwise, | |
| 155 | unless required by applicable law (such as deliberate and grossly | |
| 156 | negligent acts) or agreed to in writing, shall any Contributor be | |
| 157 | liable to You for damages, including any direct, indirect, special, | |
| 158 | incidental, or consequential damages of any character arising as a | |
| 159 | result of this License or out of the use or inability to use the | |
| 160 | Work (including but not limited to damages for loss of goodwill, | |
| 161 | work stoppage, computer failure or malfunction, or any and all | |
| 162 | other commercial damages or losses), even if such Contributor | |
| 163 | has been advised of the possibility of such damages. | |
| 164 | ||
| 165 | 9. Accepting Warranty or Additional Liability. While redistributing | |
| 166 | the Work or Derivative Works thereof, You may choose to offer, | |
| 167 | and charge a fee for, acceptance of support, warranty, indemnity, | |
| 168 | or other liability obligations and/or rights consistent with this | |
| 169 | License. However, in accepting such obligations, You may act only | |
| 170 | on Your own behalf and on Your sole responsibility, not on behalf | |
| 171 | of any other Contributor, and only if You agree to indemnify, | |
| 172 | defend, and hold each Contributor harmless for any liability | |
| 173 | incurred by, or claims asserted against, such Contributor by reason | |
| 174 | of your accepting any such warranty or additional liability. | |
| 175 | ||
| 176 | END OF TERMS AND CONDITIONS | |
| 177 | ||
| 178 | APPENDIX: How to apply the Apache License to your work. | |
| 179 | ||
| 180 | To apply the Apache License to your work, attach the following | |
| 181 | boilerplate notice, with the fields enclosed by brackets "[]" | |
| 182 | replaced with your own identifying information. (Don't include | |
| 183 | the brackets!) The text should be enclosed in the appropriate | |
| 184 | comment syntax for the file format. We also recommend that a | |
| 185 | file or class name and description of purpose be included on the | |
| 186 | same "printed page" as the copyright notice for easier | |
| 187 | identification within third-party archives. | |
| 188 | ||
| 189 | Copyright [yyyy] [name of copyright owner] | |
| 190 | ||
| 191 | Licensed under the Apache License, Version 2.0 (the "License"); | |
| 192 | you may not use this file except in compliance with the License. | |
| 193 | You may obtain a copy of the License at | |
| 194 | ||
| 195 | http://www.apache.org/licenses/LICENSE-2.0 | |
| 196 | ||
| 197 | Unless required by applicable law or agreed to in writing, software | |
| 198 | distributed under the License is distributed on an "AS IS" BASIS, | |
| 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 200 | See the License for the specific language governing permissions and | |
| 201 | limitations under the License. |
| … | … | @@ -0,0 +1,44 @@ |
| 1 | # bare-tls | |
| 2 | ||
| 3 | Transport Layer Security (TLS) streams for JavaScript. | |
| 4 | ||
| 5 | ``` | |
| 6 | npm i bare-tls | |
| 7 | ``` | |
| 8 | ||
| 9 | ## Usage | |
| 10 | ||
| 11 | On the server side: | |
| 12 | ||
| 13 | ```js | |
| 14 | const tls = require('bare-tls') | |
| 15 | const fs = require('bare-fs') | |
| 16 | ||
| 17 | const socket = new tls.Socket(stream, { | |
| 18 | isServer: true, | |
| 19 | cert: fs.readFileSync('server-cert.pem'), | |
| 20 | key: fs.readFileSync('server-key.pem') | |
| 21 | }) | |
| 22 | ||
| 23 | socket | |
| 24 | .on('connect', () => console.log('server connected')) | |
| 25 | .on('data', (data) => console.log(data)) | |
| 26 | .write('Hello from server') | |
| 27 | ``` | |
| 28 | ||
| 29 | On the client side: | |
| 30 | ||
| 31 | ```js | |
| 32 | const tls = require('bare-tls') | |
| 33 | ||
| 34 | const socket = new tls.Socket(stream) | |
| 35 | ||
| 36 | socket | |
| 37 | .on('connect', () => console.log('client connected')) | |
| 38 | .on('data', (data) => console.log(data)) | |
| 39 | .write('Hello from client') | |
| 40 | ``` | |
| 41 | ||
| 42 | ## License | |
| 43 | ||
| 44 | Apache-2.0 |
| … | … | @@ -0,0 +1,661 @@ |
| 1 | #include <assert.h> | |
| 2 | #include <bare.h> | |
| 3 | #include <js.h> | |
| 4 | #include <openssl/base.h> | |
| 5 | #include <openssl/bio.h> | |
| 6 | #include <openssl/err.h> | |
| 7 | #include <openssl/evp.h> | |
| 8 | #include <openssl/ssl.h> | |
| 9 | #include <stddef.h> | |
| 10 | ||
| 11 | typedef struct { | |
| 12 | SSL_CTX *ssl; | |
| 13 | BIO_METHOD *io; | |
| 14 | ||
| 15 | js_env_t *env; | |
| 16 | js_ref_t *ctx; | |
| 17 | } bare_tls_context_t; | |
| 18 | ||
| 19 | typedef struct { | |
| 20 | SSL *ssl; | |
| 21 | BIO *io; | |
| 22 | X509 *certificate; | |
| 23 | EVP_PKEY *key; | |
| 24 | ||
| 25 | uint8_t *alpn; | |
| 26 | size_t alpn_len; | |
| 27 | ||
| 28 | js_env_t *env; | |
| 29 | js_ref_t *ctx; | |
| 30 | js_ref_t *on_read; | |
| 31 | js_ref_t *on_write; | |
| 32 | } bare_tls_t; | |
| 33 | ||
| 34 | static int | |
| 35 | bare_tls__on_read(BIO *io, char *buffer, int len) { | |
| 36 | if (len == 0) return 0; | |
| 37 | ||
| 38 | int err; | |
| 39 | ||
| 40 | BIO_clear_retry_flags(io); | |
| 41 | ||
| 42 | bare_tls_t *socket = BIO_get_ex_data(io, 0); | |
| 43 | ||
| 44 | js_env_t *env = socket->env; | |
| 45 | ||
| 46 | js_value_t *arraybuffer; | |
| 47 | err = js_create_external_arraybuffer(env, (void *) buffer, len, NULL, NULL, &arraybuffer); | |
| 48 | assert(err == 0); | |
| 49 | ||
| 50 | js_value_t *typedarray; | |
| 51 | err = js_create_typedarray(env, js_uint8array, len, arraybuffer, 0, &typedarray); | |
| 52 | assert(err == 0); | |
| 53 | ||
| 54 | js_value_t *ctx; | |
| 55 | err = js_get_reference_value(env, socket->ctx, &ctx); | |
| 56 | assert(err == 0); | |
| 57 | ||
| 58 | js_value_t *on_read; | |
| 59 | err = js_get_reference_value(env, socket->on_read, &on_read); | |
| 60 | assert(err == 0); | |
| 61 | ||
| 62 | js_value_t *result; | |
| 63 | err = js_call_function(env, ctx, on_read, 1, &typedarray, &result); | |
| 64 | if (err < 0) return -1; | |
| 65 | ||
| 66 | err = js_get_value_int32(env, result, &len); | |
| 67 | assert(err == 0); | |
| 68 | ||
| 69 | err = js_detach_arraybuffer(env, arraybuffer); | |
| 70 | assert(err == 0); | |
| 71 | ||
| 72 | if (len == 0) { | |
| 73 | BIO_set_retry_read(io); | |
| 74 | ||
| 75 | return -1; | |
| 76 | } | |
| 77 | ||
| 78 | return len; | |
| 79 | } | |
| 80 | ||
| 81 | static int | |
| 82 | bare_tls__on_write(BIO *io, const char *buffer, int len) { | |
| 83 | if (len == 0) return 0; | |
| 84 | ||
| 85 | int err; | |
| 86 | ||
| 87 | BIO_clear_retry_flags(io); | |
| 88 | ||
| 89 | bare_tls_t *socket = BIO_get_ex_data(io, 0); | |
| 90 | ||
| 91 | js_env_t *env = socket->env; | |
| 92 | ||
| 93 | js_value_t *arraybuffer; | |
| 94 | err = js_create_external_arraybuffer(env, (void *) buffer, len, NULL, NULL, &arraybuffer); | |
| 95 | assert(err == 0); | |
| 96 | ||
| 97 | js_value_t *typedarray; | |
| 98 | err = js_create_typedarray(env, js_uint8array, len, arraybuffer, 0, &typedarray); | |
| 99 | assert(err == 0); | |
| 100 | ||
| 101 | js_value_t *ctx; | |
| 102 | err = js_get_reference_value(env, socket->ctx, &ctx); | |
| 103 | assert(err == 0); | |
| 104 | ||
| 105 | js_value_t *on_write; | |
| 106 | err = js_get_reference_value(env, socket->on_write, &on_write); | |
| 107 | assert(err == 0); | |
| 108 | ||
| 109 | js_value_t *result; | |
| 110 | err = js_call_function(env, ctx, on_write, 1, &typedarray, &result); | |
| 111 | if (err < 0) return -1; | |
| 112 | ||
| 113 | err = js_get_value_int32(env, result, &len); | |
| 114 | assert(err == 0); | |
| 115 | ||
| 116 | err = js_detach_arraybuffer(env, arraybuffer); | |
| 117 | assert(err == 0); | |
| 118 | ||
| 119 | if (len == 0) { | |
| 120 | BIO_set_retry_write(io); | |
| 121 | ||
| 122 | return -1; | |
| 123 | } | |
| 124 | ||
| 125 | return len; | |
| 126 | } | |
| 127 | ||
| 128 | static long | |
| 129 | bare_tls__on_ctrl(BIO *io, int cmd, long argc, void *argv) { | |
| 130 | switch (cmd) { | |
| 131 | case BIO_CTRL_FLUSH: | |
| 132 | return 1; | |
| 133 | ||
| 134 | default: | |
| 135 | return 0; | |
| 136 | } | |
| 137 | } | |
| 138 | ||
| 139 | static int | |
| 140 | bare_tls__on_alpn_select(SSL *ssl, const uint8_t **out, uint8_t *outlen, const uint8_t *in, unsigned int inlen, void *arg) { | |
| 141 | bare_tls_t *socket = (bare_tls_t *) SSL_get_ex_data(ssl, 0); | |
| 142 | ||
| 143 | if (socket == NULL || socket->alpn == NULL || socket->alpn_len == 0) { | |
| 144 | return SSL_TLSEXT_ERR_NOACK; | |
| 145 | } | |
| 146 | ||
| 147 | if (SSL_select_next_proto((uint8_t **) out, outlen, socket->alpn, (unsigned int) socket->alpn_len, in, inlen) != OPENSSL_NPN_NEGOTIATED) { | |
| 148 | return SSL_TLSEXT_ERR_NOACK; | |
| 149 | } | |
| 150 | ||
| 151 | return SSL_TLSEXT_ERR_OK; | |
| 152 | } | |
| 153 | ||
| 154 | static void | |
| 155 | bare_tls__on_teardown(void *data) { | |
| 156 | int err; | |
| 157 | ||
| 158 | bare_tls_context_t *context = (bare_tls_context_t *) data; | |
| 159 | ||
| 160 | js_env_t *env = context->env; | |
| 161 | ||
| 162 | SSL_CTX_free(context->ssl); | |
| 163 | ||
| 164 | BIO_meth_free(context->io); | |
| 165 | ||
| 166 | err = js_delete_reference(env, context->ctx); | |
| 167 | assert(err == 0); | |
| 168 | } | |
| 169 | ||
| 170 | static js_value_t * | |
| 171 | bare_tls_context(js_env_t *env, js_callback_info_t *info) { | |
| 172 | int err; | |
| 173 | ||
| 174 | js_value_t *handle; | |
| 175 | ||
| 176 | bare_tls_context_t *context; | |
| 177 | err = js_create_arraybuffer(env, sizeof(bare_tls_context_t), (void **) &context, &handle); | |
| 178 | assert(err == 0); | |
| 179 | ||
| 180 | BIO_METHOD *io = context->io = BIO_meth_new(BIO_get_new_index() | BIO_TYPE_SOURCE_SINK, "callback"); | |
| 181 | ||
| 182 | if (io == NULL) goto err; | |
| 183 | ||
| 184 | SSL_CTX *ssl = context->ssl = SSL_CTX_new(TLS_method()); | |
| 185 | ||
| 186 | if (ssl == NULL) { | |
| 187 | BIO_meth_free(io); | |
| 188 | ||
| 189 | goto err; | |
| 190 | } | |
| 191 | ||
| 192 | BIO_meth_set_read(io, bare_tls__on_read); | |
| 193 | BIO_meth_set_write(io, bare_tls__on_write); | |
| 194 | BIO_meth_set_ctrl(io, bare_tls__on_ctrl); | |
| 195 | ||
| 196 | err = SSL_CTX_set_ex_data(ssl, 0, (void *) context); | |
| 197 | assert(err == 1); | |
| 198 | ||
| 199 | err = SSL_CTX_set_min_proto_version(ssl, TLS1_2_VERSION); | |
| 200 | assert(err == 1); | |
| 201 | ||
| 202 | SSL_CTX_set_alpn_select_cb(ssl, bare_tls__on_alpn_select, NULL); | |
| 203 | ||
| 204 | context->env = env; | |
| 205 | ||
| 206 | err = js_add_teardown_callback(env, bare_tls__on_teardown, (void *) context); | |
| 207 | assert(err == 0); | |
| 208 | ||
| 209 | err = js_create_reference(env, handle, 1, &context->ctx); | |
| 210 | assert(err == 0); | |
| 211 | ||
| 212 | return handle; | |
| 213 | ||
| 214 | err: | |
| 215 | js_throw_error(env, ERR_reason_symbol_name(ERR_peek_last_error()), "Context initialisation failed"); | |
| 216 | return NULL; | |
| 217 | } | |
| 218 | ||
| 219 | static js_value_t * | |
| 220 | bare_tls_init(js_env_t *env, js_callback_info_t *info) { | |
| 221 | int err; | |
| 222 | ||
| 223 | size_t argc = 9; | |
| 224 | js_value_t *argv[9]; | |
| 225 | ||
| 226 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 227 | assert(err == 0); | |
| 228 | ||
| 229 | assert(argc == 9); | |
| 230 | ||
| 231 | bare_tls_context_t *context; | |
| 232 | err = js_get_arraybuffer_info(env, argv[0], (void **) &context, NULL); | |
| 233 | assert(err == 0); | |
| 234 | ||
| 235 | js_value_t *handle; | |
| 236 | ||
| 237 | bare_tls_t *socket; | |
| 238 | err = js_create_arraybuffer(env, sizeof(bare_tls_t), (void **) &socket, &handle); | |
| 239 | assert(err == 0); | |
| 240 | ||
| 241 | socket->certificate = NULL; | |
| 242 | socket->key = NULL; | |
| 243 | socket->alpn = NULL; | |
| 244 | socket->alpn_len = 0; | |
| 245 | ||
| 246 | BIO *io = socket->io = BIO_new(context->io); | |
| 247 | ||
| 248 | if (io == NULL) goto err; | |
| 249 | ||
| 250 | SSL *ssl = socket->ssl = SSL_new(context->ssl); | |
| 251 | ||
| 252 | if (ssl == NULL) { | |
| 253 | BIO_free(io); | |
| 254 | ||
| 255 | goto err; | |
| 256 | } | |
| 257 | ||
| 258 | err = BIO_set_ex_data(io, 0, (void *) socket); | |
| 259 | assert(err == 1); | |
| 260 | ||
| 261 | BIO_set_init(io, true); | |
| 262 | ||
| 263 | err = SSL_set_ex_data(ssl, 0, (void *) socket); | |
| 264 | assert(err == 1); | |
| 265 | ||
| 266 | SSL_set_bio(ssl, io, io); | |
| 267 | ||
| 268 | bool is_server; | |
| 269 | err = js_get_value_bool(env, argv[1], &is_server); | |
| 270 | assert(err == 0); | |
| 271 | ||
| 272 | if (is_server) SSL_set_accept_state(ssl); | |
| 273 | else SSL_set_connect_state(ssl); | |
| 274 | ||
| 275 | bool has_cert; | |
| 276 | err = js_is_typedarray(env, argv[2], &has_cert); | |
| 277 | assert(err == 0); | |
| 278 | ||
| 279 | if (has_cert) { | |
| 280 | char *pem; | |
| 281 | size_t len; | |
| 282 | err = js_get_typedarray_info(env, argv[2], NULL, (void **) &pem, &len, NULL, NULL); | |
| 283 | assert(err == 0); | |
| 284 | ||
| 285 | BIO *io = BIO_new(BIO_s_mem()); | |
| 286 | BIO_write(io, pem, (int) len); | |
| 287 | ||
| 288 | X509 *certificate = socket->certificate = PEM_read_bio_X509(io, NULL, NULL, NULL); | |
| 289 | ||
| 290 | BIO_free(io); | |
| 291 | ||
| 292 | if (certificate == NULL) { | |
| 293 | SSL_free(ssl); | |
| 294 | ||
| 295 | goto err; | |
| 296 | } | |
| 297 | ||
| 298 | err = SSL_use_certificate(ssl, certificate); | |
| 299 | ||
| 300 | if (err == 0) { | |
| 301 | SSL_free(ssl); | |
| 302 | ||
| 303 | X509_free(certificate); | |
| 304 | ||
| 305 | goto err; | |
| 306 | } | |
| 307 | } | |
| 308 | ||
| 309 | bool has_key; | |
| 310 | err = js_is_typedarray(env, argv[3], &has_key); | |
| 311 | assert(err == 0); | |
| 312 | ||
| 313 | if (has_key) { | |
| 314 | char *pem; | |
| 315 | size_t len; | |
| 316 | err = js_get_typedarray_info(env, argv[3], NULL, (void **) &pem, &len, NULL, NULL); | |
| 317 | assert(err == 0); | |
| 318 | ||
| 319 | BIO *io = BIO_new(BIO_s_mem()); | |
| 320 | BIO_write(io, pem, (int) len); | |
| 321 | ||
| 322 | EVP_PKEY *key = socket->key = PEM_read_bio_PrivateKey(io, NULL, NULL, NULL); | |
| 323 | ||
| 324 | BIO_free(io); | |
| 325 | ||
| 326 | if (key == NULL) { | |
| 327 | SSL_free(ssl); | |
| 328 | ||
| 329 | goto err; | |
| 330 | } | |
| 331 | ||
| 332 | int res = SSL_use_PrivateKey(ssl, key); | |
| 333 | ||
| 334 | if (res == 0) { | |
| 335 | SSL_free(ssl); | |
| 336 | ||
| 337 | EVP_PKEY_free(key); | |
| 338 | ||
| 339 | goto err; | |
| 340 | } | |
| 341 | } | |
| 342 | ||
| 343 | bool has_host; | |
| 344 | err = js_is_string(env, argv[4], &has_host); | |
| 345 | assert(err == 0); | |
| 346 | ||
| 347 | if (has_host) { | |
| 348 | size_t len; | |
| 349 | err = js_get_value_string_utf8(env, argv[4], NULL, 0, &len); | |
| 350 | assert(err == 0); | |
| 351 | ||
| 352 | len += 1 /* NULL */; | |
| 353 | ||
| 354 | utf8_t *host = malloc(len); | |
| 355 | err = js_get_value_string_utf8(env, argv[4], host, len, NULL); | |
| 356 | assert(err == 0); | |
| 357 | ||
| 358 | err = SSL_set_tlsext_host_name(ssl, (char *) host); | |
| 359 | assert(err == 1); | |
| 360 | ||
| 361 | free(host); | |
| 362 | } | |
| 363 | ||
| 364 | bool has_alpn; | |
| 365 | err = js_is_typedarray(env, argv[5], &has_alpn); | |
| 366 | assert(err == 0); | |
| 367 | ||
| 368 | if (has_alpn) { | |
| 369 | uint8_t *alpn; | |
| 370 | size_t len; | |
| 371 | err = js_get_typedarray_info(env, argv[5], NULL, (void **) &alpn, &len, NULL, NULL); | |
| 372 | assert(err == 0); | |
| 373 | ||
| 374 | if (is_server) { | |
| 375 | socket->alpn = malloc(len); | |
| 376 | socket->alpn_len = len; | |
| 377 | ||
| 378 | memcpy(socket->alpn, alpn, len); | |
| 379 | } else { | |
| 380 | err = SSL_set_alpn_protos(ssl, alpn, len); | |
| 381 | assert(err == 0); | |
| 382 | } | |
| 383 | } | |
| 384 | ||
| 385 | socket->env = env; | |
| 386 | ||
| 387 | err = js_create_reference(env, argv[6], 1, &socket->ctx); | |
| 388 | assert(err == 0); | |
| 389 | ||
| 390 | err = js_create_reference(env, argv[7], 1, &socket->on_read); | |
| 391 | assert(err == 0); | |
| 392 | ||
| 393 | err = js_create_reference(env, argv[8], 1, &socket->on_write); | |
| 394 | assert(err == 0); | |
| 395 | ||
| 396 | return handle; | |
| 397 | ||
| 398 | err: | |
| 399 | js_throw_error(env, ERR_reason_symbol_name(ERR_peek_last_error()), "Socket initialisation failed"); | |
| 400 | return NULL; | |
| 401 | } | |
| 402 | ||
| 403 | static js_value_t * | |
| 404 | bare_tls_destroy(js_env_t *env, js_callback_info_t *info) { | |
| 405 | int err; | |
| 406 | ||
| 407 | size_t argc = 1; | |
| 408 | js_value_t *argv[1]; | |
| 409 | ||
| 410 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 411 | assert(err == 0); | |
| 412 | ||
| 413 | assert(argc == 1); | |
| 414 | ||
| 415 | bare_tls_t *socket; | |
| 416 | err = js_get_arraybuffer_info(env, argv[0], (void **) &socket, NULL); | |
| 417 | assert(err == 0); | |
| 418 | ||
| 419 | SSL_free(socket->ssl); | |
| 420 | ||
| 421 | if (socket->certificate) X509_free(socket->certificate); | |
| 422 | ||
| 423 | if (socket->key) EVP_PKEY_free(socket->key); | |
| 424 | ||
| 425 | if (socket->alpn) free(socket->alpn); | |
| 426 | ||
| 427 | err = js_delete_reference(env, socket->on_read); | |
| 428 | assert(err == 0); | |
| 429 | ||
| 430 | err = js_delete_reference(env, socket->on_write); | |
| 431 | assert(err == 0); | |
| 432 | ||
| 433 | err = js_delete_reference(env, socket->ctx); | |
| 434 | assert(err == 0); | |
| 435 | ||
| 436 | return NULL; | |
| 437 | } | |
| 438 | ||
| 439 | static js_value_t * | |
| 440 | bare_tls_handshake(js_env_t *env, js_callback_info_t *info) { | |
| 441 | int err; | |
| 442 | ||
| 443 | size_t argc = 1; | |
| 444 | js_value_t *argv[1]; | |
| 445 | ||
| 446 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 447 | assert(err == 0); | |
| 448 | ||
| 449 | assert(argc == 1); | |
| 450 | ||
| 451 | bare_tls_t *socket; | |
| 452 | err = js_get_arraybuffer_info(env, argv[0], (void **) &socket, NULL); | |
| 453 | assert(err == 0); | |
| 454 | ||
| 455 | bool done = true; | |
| 456 | ||
| 457 | err = SSL_do_handshake(socket->ssl); | |
| 458 | ||
| 459 | if (err <= 0) { | |
| 460 | err = SSL_get_error(socket->ssl, err); | |
| 461 | ||
| 462 | if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) { | |
| 463 | done = false; | |
| 464 | } else { | |
| 465 | js_throw_error(env, ERR_reason_symbol_name(ERR_peek_last_error()), "Handshake failed"); | |
| 466 | return NULL; | |
| 467 | } | |
| 468 | } | |
| 469 | ||
| 470 | js_value_t *result; | |
| 471 | err = js_get_boolean(env, done, &result); | |
| 472 | assert(err == 0); | |
| 473 | ||
| 474 | return result; | |
| 475 | } | |
| 476 | ||
| 477 | static js_value_t * | |
| 478 | bare_tls_read(js_env_t *env, js_callback_info_t *info) { | |
| 479 | int err; | |
| 480 | ||
| 481 | size_t argc = 2; | |
| 482 | js_value_t *argv[2]; | |
| 483 | ||
| 484 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 485 | assert(err == 0); | |
| 486 | ||
| 487 | assert(argc == 2); | |
| 488 | ||
| 489 | bare_tls_t *socket; | |
| 490 | err = js_get_arraybuffer_info(env, argv[0], (void **) &socket, NULL); | |
| 491 | assert(err == 0); | |
| 492 | ||
| 493 | char *buffer; | |
| 494 | size_t len; | |
| 495 | err = js_get_typedarray_info(env, argv[1], NULL, (void **) &buffer, &len, NULL, NULL); | |
| 496 | assert(err == 0); | |
| 497 | ||
| 498 | bool retry = false; | |
| 499 | bool eof = false; | |
| 500 | ||
| 501 | err = SSL_read(socket->ssl, buffer, len); | |
| 502 | ||
| 503 | if (err <= 0) { | |
| 504 | err = SSL_get_error(socket->ssl, err); | |
| 505 | ||
| 506 | if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) { | |
| 507 | retry = true; | |
| 508 | } else if (SSL_get_shutdown(socket->ssl)) { | |
| 509 | eof = true; | |
| 510 | } else { | |
| 511 | js_throw_error(env, ERR_reason_symbol_name(ERR_peek_last_error()), "Read failed"); | |
| 512 | return NULL; | |
| 513 | } | |
| 514 | } | |
| 515 | ||
| 516 | int res = eof ? 0 : retry ? -1 | |
| 517 | : err; | |
| 518 | ||
| 519 | js_value_t *result; | |
| 520 | err = js_create_int64(env, res, &result); | |
| 521 | assert(err == 0); | |
| 522 | ||
| 523 | return result; | |
| 524 | } | |
| 525 | ||
| 526 | static js_value_t * | |
| 527 | bare_tls_write(js_env_t *env, js_callback_info_t *info) { | |
| 528 | int err; | |
| 529 | ||
| 530 | size_t argc = 2; | |
| 531 | js_value_t *argv[2]; | |
| 532 | ||
| 533 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 534 | assert(err == 0); | |
| 535 | ||
| 536 | assert(argc == 2); | |
| 537 | ||
| 538 | bare_tls_t *socket; | |
| 539 | err = js_get_arraybuffer_info(env, argv[0], (void **) &socket, NULL); | |
| 540 | assert(err == 0); | |
| 541 | ||
| 542 | char *buffer; | |
| 543 | size_t len; | |
| 544 | err = js_get_typedarray_info(env, argv[1], NULL, (void **) &buffer, &len, NULL, NULL); | |
| 545 | assert(err == 0); | |
| 546 | ||
| 547 | bool retry = false; | |
| 548 | ||
| 549 | err = SSL_write(socket->ssl, buffer, len); | |
| 550 | ||
| 551 | if (err <= 0) { | |
| 552 | err = SSL_get_error(socket->ssl, err); | |
| 553 | ||
| 554 | if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) { | |
| 555 | retry = true; | |
| 556 | } else { | |
| 557 | js_throw_error(env, ERR_reason_symbol_name(ERR_peek_last_error()), "Write failed"); | |
| 558 | return NULL; | |
| 559 | } | |
| 560 | } | |
| 561 | ||
| 562 | int res = retry ? 0 : err; | |
| 563 | ||
| 564 | js_value_t *result; | |
| 565 | err = js_create_int64(env, res, &result); | |
| 566 | assert(err == 0); | |
| 567 | ||
| 568 | return result; | |
| 569 | } | |
| 570 | ||
| 571 | static js_value_t * | |
| 572 | bare_tls_shutdown(js_env_t *env, js_callback_info_t *info) { | |
| 573 | int err; | |
| 574 | ||
| 575 | size_t argc = 1; | |
| 576 | js_value_t *argv[1]; | |
| 577 | ||
| 578 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 579 | assert(err == 0); | |
| 580 | ||
| 581 | assert(argc == 1); | |
| 582 | ||
| 583 | bare_tls_t *socket; | |
| 584 | err = js_get_arraybuffer_info(env, argv[0], (void **) &socket, NULL); | |
| 585 | assert(err == 0); | |
| 586 | ||
| 587 | err = SSL_shutdown(socket->ssl); | |
| 588 | ||
| 589 | if (err < 0) { | |
| 590 | js_throw_error(env, ERR_reason_symbol_name(ERR_peek_last_error()), "Shutdown failed"); | |
| 591 | return NULL; | |
| 592 | } | |
| 593 | ||
| 594 | js_value_t *result; | |
| 595 | err = js_get_boolean(env, err == 1, &result); | |
| 596 | assert(err == 0); | |
| 597 | ||
| 598 | return result; | |
| 599 | } | |
| 600 | ||
| 601 | static js_value_t * | |
| 602 | bare_tls_alpn_protocol(js_env_t *env, js_callback_info_t *info) { | |
| 603 | int err; | |
| 604 | ||
| 605 | size_t argc = 1; | |
| 606 | js_value_t *argv[1]; | |
| 607 | ||
| 608 | err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); | |
| 609 | assert(err == 0); | |
| 610 | ||
| 611 | assert(argc == 1); | |
| 612 | ||
| 613 | bare_tls_t *socket; | |
| 614 | err = js_get_arraybuffer_info(env, argv[0], (void **) &socket, NULL); | |
| 615 | assert(err == 0); | |
| 616 | ||
| 617 | const uint8_t *data = NULL; | |
| 618 | unsigned int len = 0; | |
| 619 | ||
| 620 | SSL_get0_alpn_selected(socket->ssl, &data, &len); | |
| 621 | ||
| 622 | js_value_t *result; | |
| 623 | ||
| 624 | if (data && len) { | |
| 625 | err = js_create_string_utf8(env, data, len, &result); | |
| 626 | assert(err == 0); | |
| 627 | } else { | |
| 628 | err = js_get_null(env, &result); | |
| 629 | assert(err == 0); | |
| 630 | } | |
| 631 | ||
| 632 | return result; | |
| 633 | } | |
| 634 | ||
| 635 | static js_value_t * | |
| 636 | bare_tls_exports(js_env_t *env, js_value_t *exports) { | |
| 637 | int err; | |
| 638 | ||
| 639 | #define V(name, fn) \ | |
| 640 | { \ | |
| 641 | js_value_t *val; \ | |
| 642 | err = js_create_function(env, name, -1, fn, NULL, &val); \ | |
| 643 | assert(err == 0); \ | |
| 644 | err = js_set_named_property(env, exports, name, val); \ | |
| 645 | assert(err == 0); \ | |
| 646 | } | |
| 647 | ||
| 648 | V("context", bare_tls_context); | |
| 649 | V("init", bare_tls_init); | |
| 650 | V("destroy", bare_tls_destroy); | |
| 651 | V("handshake", bare_tls_handshake); | |
| 652 | V("read", bare_tls_read); | |
| 653 | V("write", bare_tls_write); | |
| 654 | V("shutdown", bare_tls_shutdown); | |
| 655 | V("alpnProtocol", bare_tls_alpn_protocol); | |
| 656 | #undef V | |
| 657 | ||
| 658 | return exports; | |
| 659 | } | |
| 660 | ||
| 661 | BARE_MODULE(bare_tls, bare_tls_exports) |
| … | … | @@ -0,0 +1 @@ |
| 1 | module.exports = require.addon() |
| … | … | @@ -0,0 +1,31 @@ |
| 1 | import { Duplex, DuplexEvents } from 'bare-stream' | |
| 2 | import constants from './lib/constants' | |
| 3 | import TLSError from './lib/errors' | |
| 4 | ||
| 5 | export { constants, TLSError as errors } | |
| 6 | ||
| 7 | export interface TLSSocketEvents extends DuplexEvents { | |
| 8 | connect: [] | |
| 9 | } | |
| 10 | ||
| 11 | export interface TLSSocketOptions { | |
| 12 | allowHalfOpen?: boolean | |
| 13 | cert?: ArrayBufferView | |
| 14 | eagerOpen?: boolean | |
| 15 | host?: string | |
| 16 | isServer?: boolean | |
| 17 | key?: ArrayBufferView | |
| 18 | } | |
| 19 | ||
| 20 | export interface TLSSocket< | |
| 21 | M extends TLSSocketEvents = TLSSocketEvents | |
| 22 | > extends Duplex<M> { | |
| 23 | readonly socket: Duplex | |
| 24 | readonly encrypted: true | |
| 25 | } | |
| 26 | ||
| 27 | export class TLSSocket { | |
| 28 | constructor(socket: Duplex, opts?: TLSSocketOptions) | |
| 29 | } | |
| 30 | ||
| 31 | export { TLSSocket as Socket } |
| … | … | @@ -0,0 +1,298 @@ |
| 1 | const { Duplex, Writable } = require('bare-stream') | |
| 2 | const binding = require('./binding') | |
| 3 | const constants = require('./lib/constants') | |
| 4 | const errors = require('./lib/errors') | |
| 5 | ||
| 6 | const defaultReadBufferSize = 65536 | |
| 7 | ||
| 8 | const context = binding.context() | |
| 9 | ||
| 10 | exports.Socket = class TLSSocket extends Duplex { | |
| 11 | constructor(socket, opts = {}) { | |
| 12 | const { | |
| 13 | isServer = false, | |
| 14 | cert = null, | |
| 15 | key = null, | |
| 16 | host = null, | |
| 17 | alpnProtocols = null, | |
| 18 | eagerOpen = true, | |
| 19 | allowHalfOpen = true, | |
| 20 | readBufferSize = defaultReadBufferSize | |
| 21 | } = opts | |
| 22 | ||
| 23 | super({ eagerOpen }) | |
| 24 | ||
| 25 | this._state = 0 | |
| 26 | ||
| 27 | this._socket = socket | |
| 28 | this._key = key | |
| 29 | this._cert = cert | |
| 30 | this._allowHalfOpen = allowHalfOpen | |
| 31 | ||
| 32 | this._pendingOpen = null | |
| 33 | this._pendingWrite = null | |
| 34 | ||
| 35 | this._reading = Buffer.alloc(readBufferSize) | |
| 36 | this._buffer = [] | |
| 37 | this._buffered = 0 | |
| 38 | ||
| 39 | let alpn = null | |
| 40 | ||
| 41 | if (alpnProtocols && alpnProtocols.length > 0) { | |
| 42 | const parts = [] | |
| 43 | ||
| 44 | for (const protocol of alpnProtocols) { | |
| 45 | const encoded = Buffer.from(protocol) | |
| 46 | ||
| 47 | if (encoded.byteLength === 0 || encoded.byteLength > 255) { | |
| 48 | throw new RangeError('ALPN protocol name must be 1-255 bytes') | |
| 49 | } | |
| 50 | ||
| 51 | parts.push(Buffer.of(encoded.byteLength), encoded) | |
| 52 | } | |
| 53 | ||
| 54 | alpn = Buffer.concat(parts) | |
| 55 | } | |
| 56 | ||
| 57 | try { | |
| 58 | this._handle = binding.init( | |
| 59 | context, | |
| 60 | isServer, | |
| 61 | cert, | |
| 62 | key, | |
| 63 | host, | |
| 64 | alpn, | |
| 65 | this, | |
| 66 | this._onread, | |
| 67 | this._onwrite | |
| 68 | ) | |
| 69 | } catch (err) { | |
| 70 | this._handle = null | |
| 71 | ||
| 72 | this.destroy(err) | |
| 73 | } | |
| 74 | } | |
| 75 | ||
| 76 | get socket() { | |
| 77 | return this._socket | |
| 78 | } | |
| 79 | ||
| 80 | get encrypted() { | |
| 81 | return true | |
| 82 | } | |
| 83 | ||
| 84 | get alpnProtocol() { | |
| 85 | return binding.alpnProtocol(this._handle) | |
| 86 | } | |
| 87 | ||
| 88 | _onconnect() { | |
| 89 | this._state |= constants.state.CONNECTED | |
| 90 | ||
| 91 | this.emit('connect') | |
| 92 | ||
| 93 | const cb = this._pendingOpen | |
| 94 | this._pendingOpen = null | |
| 95 | cb(null) | |
| 96 | } | |
| 97 | ||
| 98 | _ondata(data) { | |
| 99 | this._buffer.push(data) | |
| 100 | this._buffered += data.byteLength | |
| 101 | ||
| 102 | while (this._buffered > 0) { | |
| 103 | if (this._state & constants.state.CONNECTED) { | |
| 104 | let read | |
| 105 | try { | |
| 106 | read = binding.read(this._handle, this._reading) | |
| 107 | } catch (err) { | |
| 108 | return this.destroy(errors.from(err)) | |
| 109 | } | |
| 110 | ||
| 111 | if (read < 0) break | |
| 112 | ||
| 113 | if (read === 0) { | |
| 114 | this.push(null) | |
| 115 | if (this._allowHalfOpen === false) this.end() | |
| 116 | return | |
| 117 | } | |
| 118 | ||
| 119 | const copy = Buffer.allocUnsafe(read) | |
| 120 | copy.set(this._reading.subarray(0, read)) | |
| 121 | ||
| 122 | this.push(copy) | |
| 123 | } else { | |
| 124 | try { | |
| 125 | if (binding.handshake(this._handle)) this._onconnect() | |
| 126 | else break | |
| 127 | } catch (err) { | |
| 128 | if (this._pendingOpen) { | |
| 129 | const cb = this._pendingOpen | |
| 130 | this._pendingOpen = null | |
| 131 | cb(errors.from(err)) | |
| 132 | } else { | |
| 133 | this.destroy(errors.from(err)) | |
| 134 | } | |
| 135 | return | |
| 136 | } | |
| 137 | } | |
| 138 | } | |
| 139 | } | |
| 140 | ||
| 141 | _ondrain() { | |
| 142 | if (this._pendingWrite === null) return | |
| 143 | const cb = this._pendingWrite | |
| 144 | this._pendingWrite = null | |
| 145 | cb(null) | |
| 146 | } | |
| 147 | ||
| 148 | _onend() { | |
| 149 | this.push(null) | |
| 150 | } | |
| 151 | ||
| 152 | _onerror(err) { | |
| 153 | if (this._pendingOpen) { | |
| 154 | const cb = this._pendingOpen | |
| 155 | this._pendingOpen = null | |
| 156 | cb(err) | |
| 157 | } else { | |
| 158 | this.destroy(err) | |
| 159 | } | |
| 160 | } | |
| 161 | ||
| 162 | _onread(data) { | |
| 163 | if (this._buffered < data.byteLength) return 0 | |
| 164 | ||
| 165 | let offset = 0 | |
| 166 | let remaining = data.byteLength | |
| 167 | ||
| 168 | while (remaining > 0) { | |
| 169 | const chunk = this._buffer[0] | |
| 170 | ||
| 171 | if (chunk.byteLength <= remaining) { | |
| 172 | data.set(chunk, offset) | |
| 173 | ||
| 174 | offset += chunk.byteLength | |
| 175 | remaining -= chunk.byteLength | |
| 176 | ||
| 177 | this._buffer.shift() | |
| 178 | } else { | |
| 179 | data.set(chunk.subarray(0, remaining), offset) | |
| 180 | ||
| 181 | this._buffer[0] = chunk.subarray(remaining) | |
| 182 | ||
| 183 | remaining = 0 | |
| 184 | } | |
| 185 | } | |
| 186 | ||
| 187 | this._buffered -= data.byteLength | |
| 188 | ||
| 189 | return data.byteLength | |
| 190 | } | |
| 191 | ||
| 192 | _onwrite(data) { | |
| 193 | this._socket.write(Buffer.from(data.slice())) | |
| 194 | ||
| 195 | return data.byteLength | |
| 196 | } | |
| 197 | ||
| 198 | _attach() { | |
| 199 | if (this._state & constants.state.ATTACHED) return | |
| 200 | this._state |= constants.state.ATTACHED | |
| 201 | ||
| 202 | this._ondata = this._ondata.bind(this) | |
| 203 | this._ondrain = this._ondrain.bind(this) | |
| 204 | this._onend = this._onend.bind(this) | |
| 205 | this._onerror = this._onerror.bind(this) | |
| 206 | ||
| 207 | this._socket | |
| 208 | .on('data', this._ondata) | |
| 209 | .on('drain', this._ondrain) | |
| 210 | .on('end', this._onend) | |
| 211 | .on('error', this._onerror) | |
| 212 | } | |
| 213 | ||
| 214 | _detach() { | |
| 215 | if (!(this._state & constants.state.ATTACHED)) return | |
| 216 | this._state &= ~constants.state.ATTACHED | |
| 217 | ||
| 218 | this._socket | |
| 219 | .off('data', this._ondata) | |
| 220 | .off('drain', this._ondrain) | |
| 221 | .off('end', this._onend) | |
| 222 | .off('error', this._onerror) | |
| 223 | } | |
| 224 | ||
| 225 | _open(cb) { | |
| 226 | this._pendingOpen = cb | |
| 227 | this._attach() | |
| 228 | ||
| 229 | try { | |
| 230 | if (binding.handshake(this._handle)) this._onconnect() | |
| 231 | } catch (err) { | |
| 232 | this._pendingOpen = null | |
| 233 | ||
| 234 | cb(errors.from(err)) | |
| 235 | } | |
| 236 | } | |
| 237 | ||
| 238 | _write(data, encoding, cb) { | |
| 239 | try { | |
| 240 | binding.write(this._handle, data) | |
| 241 | ||
| 242 | if (Writable.isBackpressured(this._socket)) { | |
| 243 | this._pendingWrite = cb | |
| 244 | } else { | |
| 245 | cb(null) | |
| 246 | } | |
| 247 | } catch (err) { | |
| 248 | this._pendingWrite = null | |
| 249 | ||
| 250 | cb(errors.from(err)) | |
| 251 | } | |
| 252 | } | |
| 253 | ||
| 254 | _final(cb) { | |
| 255 | try { | |
| 256 | binding.shutdown(this._handle) | |
| 257 | ||
| 258 | cb(null) | |
| 259 | } catch (err) { | |
| 260 | cb(err) | |
| 261 | } | |
| 262 | ||
| 263 | this._socket.end() | |
| 264 | } | |
| 265 | ||
| 266 | _predestroy() { | |
| 267 | if (!this._handle) return | |
| 268 | ||
| 269 | this._detach() | |
| 270 | ||
| 271 | binding.destroy(this._handle) | |
| 272 | this._handle = null | |
| 273 | } | |
| 274 | ||
| 275 | _destroy(err, cb) { | |
| 276 | if (!this._handle) return cb(err) | |
| 277 | ||
| 278 | this._detach() | |
| 279 | ||
| 280 | binding.destroy(this._handle) | |
| 281 | this._handle = null | |
| 282 | ||
| 283 | cb(err) | |
| 284 | } | |
| 285 | } | |
| 286 | ||
| 287 | exports.TLSSocket = exports.Socket // For Node.js compatibility | |
| 288 | ||
| 289 | exports.constants = constants | |
| 290 | exports.errors = errors | |
| 291 | ||
| 292 | const net = require('./net') | |
| 293 | ||
| 294 | exports.createConnection = net.createConnection | |
| 295 | exports.createServer = net.createServer | |
| 296 | ||
| 297 | // For Node.js compatibility | |
| 298 | exports.connect = exports.createConnection |
| … | … | @@ -0,0 +1,8 @@ |
| 1 | declare const constants: { | |
| 2 | state: { | |
| 3 | CONNECTED: number | |
| 4 | ATTACHED: number | |
| 5 | } | |
| 6 | } | |
| 7 | ||
| 8 | export = constants |
| … | … | @@ -0,0 +1,6 @@ |
| 1 | module.exports = { | |
| 2 | state: { | |
| 3 | CONNECTED: 0x1, | |
| 4 | ATTACHED: 0x2 | |
| 5 | } | |
| 6 | } |
| … | … | @@ -0,0 +1,5 @@ |
| 1 | declare class TLSError extends Error { | |
| 2 | static from(err: Error): TLSError | |
| 3 | } | |
| 4 | ||
| 5 | export = TLSError |
| … | … | @@ -0,0 +1,18 @@ |
| 1 | module.exports = class TLSError extends Error { | |
| 2 | constructor(msg, code, fn = TLSError) { | |
| 3 | super(`${code}: ${msg}`) | |
| 4 | this.code = code | |
| 5 | ||
| 6 | if (Error.captureStackTrace) { | |
| 7 | Error.captureStackTrace(this, fn) | |
| 8 | } | |
| 9 | } | |
| 10 | ||
| 11 | get name() { | |
| 12 | return 'TLSError' | |
| 13 | } | |
| 14 | ||
| 15 | static from(err) { | |
| 16 | return new TLSError(err.message, err.code, TLSError.from) | |
| 17 | } | |
| 18 | } |
| … | … | @@ -0,0 +1,127 @@ |
| 1 | const EventEmitter = require('bare-events') | |
| 2 | const net = require('bare-net') | |
| 3 | const tls = require('bare-tls') | |
| 4 | ||
| 5 | class TLSNetSocket extends tls.Socket { | |
| 6 | ref() { | |
| 7 | this.socket.ref() | |
| 8 | } | |
| 9 | ||
| 10 | unref() { | |
| 11 | this.socket.unref() | |
| 12 | } | |
| 13 | } | |
| 14 | ||
| 15 | class TLSNetServer extends EventEmitter { | |
| 16 | constructor(opts = {}, onconnection) { | |
| 17 | if (typeof opts === 'function') { | |
| 18 | onconnection = opts | |
| 19 | opts = {} | |
| 20 | } | |
| 21 | ||
| 22 | const { | |
| 23 | cert = null, | |
| 24 | key = null, | |
| 25 | host = null, | |
| 26 | alpnProtocols = null, | |
| 27 | eagerOpen = true, | |
| 28 | allowHalfOpen = true | |
| 29 | } = opts | |
| 30 | ||
| 31 | super() | |
| 32 | ||
| 33 | this._opts = { | |
| 34 | cert, | |
| 35 | key, | |
| 36 | host, | |
| 37 | alpnProtocols, | |
| 38 | eagerOpen, | |
| 39 | allowHalfOpen | |
| 40 | } | |
| 41 | ||
| 42 | this._server = net.createServer(opts) | |
| 43 | this._server | |
| 44 | .on('listening', this._onlistening.bind(this)) | |
| 45 | .on('connection', this._onconnection.bind(this)) | |
| 46 | .on('error', this._onerror.bind(this)) | |
| 47 | .on('close', this._onclose.bind(this)) | |
| 48 | ||
| 49 | if (onconnection) this.on('connection', onconnection) | |
| 50 | } | |
| 51 | ||
| 52 | get listening() { | |
| 53 | return this._server.listening | |
| 54 | } | |
| 55 | ||
| 56 | address() { | |
| 57 | return this._server.address() | |
| 58 | } | |
| 59 | ||
| 60 | listen(...args) { | |
| 61 | this._server.listen(...args) | |
| 62 | return this | |
| 63 | } | |
| 64 | ||
| 65 | close(onclose) { | |
| 66 | if (onclose) this.once('close', onclose) | |
| 67 | this._server.close() | |
| 68 | } | |
| 69 | ||
| 70 | ref() { | |
| 71 | this._server.ref() | |
| 72 | } | |
| 73 | ||
| 74 | unref() { | |
| 75 | this._server.unref() | |
| 76 | } | |
| 77 | ||
| 78 | _onlistening() { | |
| 79 | this.emit('listening') | |
| 80 | } | |
| 81 | ||
| 82 | _onconnection(socket) { | |
| 83 | this.emit( | |
| 84 | 'connection', | |
| 85 | new TLSNetSocket(socket, { ...this._opts, isServer: true }) | |
| 86 | ) | |
| 87 | } | |
| 88 | ||
| 89 | _onerror(err) { | |
| 90 | this.emit('error', err) | |
| 91 | } | |
| 92 | ||
| 93 | _onclose() { | |
| 94 | this.emit('close') | |
| 95 | } | |
| 96 | } | |
| 97 | ||
| 98 | exports.createConnection = function createConnection(...args) { | |
| 99 | let opts = {} | |
| 100 | let onconnect | |
| 101 | ||
| 102 | if (typeof args[0] === 'string') { | |
| 103 | // createConnection(path[, onconnect]) | |
| 104 | opts.path = args[0] | |
| 105 | onconnect = args[1] | |
| 106 | } else if (typeof args[0] === 'number') { | |
| 107 | // createConnection(port[, host][, onconnect]) | |
| 108 | opts.port = args[0] | |
| 109 | ||
| 110 | if (typeof args[1] === 'function') { | |
| 111 | onconnect = args[1] | |
| 112 | } else { | |
| 113 | opts.host = args[1] | |
| 114 | onconnect = args[2] | |
| 115 | } | |
| 116 | } else { | |
| 117 | // createConnection(opts[, onconnect]) | |
| 118 | opts = args[0] || {} | |
| 119 | onconnect = args[1] | |
| 120 | } | |
| 121 | ||
| 122 | return new TLSNetSocket(net.createConnection(opts, onconnect), opts) | |
| 123 | } | |
| 124 | ||
| 125 | exports.createServer = function createServer(opts, onconnection) { | |
| 126 | return new TLSNetServer(opts, onconnection) | |
| 127 | } |
| … | … | @@ -0,0 +1,60 @@ |
| 1 | { | |
| 2 | "name": "bare-tls", | |
| 3 | "version": "2.2.1", | |
| 4 | "description": "Transport Layer Security (TLS) streams for JavaScript", | |
| 5 | "exports": { | |
| 6 | ".": { | |
| 7 | "types": "./index.d.ts", | |
| 8 | "default": "./index.js" | |
| 9 | }, | |
| 10 | "./package": "./package.json", | |
| 11 | "./constants": { | |
| 12 | "types": "./lib/constants.d.ts", | |
| 13 | "default": "./lib/constants.js" | |
| 14 | }, | |
| 15 | "./errors": { | |
| 16 | "types": "./lib/errors.d.ts", | |
| 17 | "default": "./lib/errors.js" | |
| 18 | } | |
| 19 | }, | |
| 20 | "files": [ | |
| 21 | "index.js", | |
| 22 | "index.d.ts", | |
| 23 | "net.js", | |
| 24 | "binding.c", | |
| 25 | "binding.js", | |
| 26 | "CMakeLists.txt", | |
| 27 | "lib", | |
| 28 | "prebuilds" | |
| 29 | ], | |
| 30 | "addon": true, | |
| 31 | "scripts": { | |
| 32 | "test": "prettier . --check && bare test.js" | |
| 33 | }, | |
| 34 | "repository": { | |
| 35 | "type": "git", | |
| 36 | "url": "git+https://github.com/holepunchto/bare-tls.git" | |
| 37 | }, | |
| 38 | "author": "Holepunch", | |
| 39 | "license": "Apache-2.0", | |
| 40 | "bugs": { | |
| 41 | "url": "https://github.com/holepunchto/bare-tls/issues" | |
| 42 | }, | |
| 43 | "homepage": "https://github.com/holepunchto/bare-tls#readme", | |
| 44 | "engines": { | |
| 45 | "bare": ">=1.7.0" | |
| 46 | }, | |
| 47 | "dependencies": { | |
| 48 | "bare-net": "^2.0.1", | |
| 49 | "bare-stream": "^2.6.4" | |
| 50 | }, | |
| 51 | "devDependencies": { | |
| 52 | "bare-events": "^2.6.0", | |
| 53 | "bare-fs": "^4.0.2", | |
| 54 | "brittle": "^3.1.1", | |
| 55 | "cmake-bare": "^1.1.6", | |
| 56 | "cmake-fetch": "^1.0.0", | |
| 57 | "prettier": "^3.4.1", | |
| 58 | "prettier-config-standard": "^7.0.0" | |
| 59 | } | |
| 60 | } |
| … | … | @@ -0,0 +1,201 @@ |
| 1 | Apache License | |
| 2 | Version 2.0, January 2004 | |
| 3 | http://www.apache.org/licenses/ | |
| 4 | ||
| 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | |
| 6 | ||
| 7 | 1. Definitions. | |
| 8 | ||
| 9 | "License" shall mean the terms and conditions for use, reproduction, | |
| 10 | and distribution as defined by Sections 1 through 9 of this document. | |
| 11 | ||
| 12 | "Licensor" shall mean the copyright owner or entity authorized by | |
| 13 | the copyright owner that is granting the License. | |
| 14 | ||
| 15 | "Legal Entity" shall mean the union of the acting entity and all | |
| 16 | other entities that control, are controlled by, or are under common | |
| 17 | control with that entity. For the purposes of this definition, | |
| 18 | "control" means (i) the power, direct or indirect, to cause the | |
| 19 | direction or management of such entity, whether by contract or | |
| 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the | |
| 21 | outstanding shares, or (iii) beneficial ownership of such entity. | |
| 22 | ||
| 23 | "You" (or "Your") shall mean an individual or Legal Entity | |
| 24 | exercising permissions granted by this License. | |
| 25 | ||
| 26 | "Source" form shall mean the preferred form for making modifications, | |
| 27 | including but not limited to software source code, documentation | |
| 28 | source, and configuration files. | |
| 29 | ||
| 30 | "Object" form shall mean any form resulting from mechanical | |
| 31 | transformation or translation of a Source form, including but | |
| 32 | not limited to compiled object code, generated documentation, | |
| 33 | and conversions to other media types. | |
| 34 | ||
| 35 | "Work" shall mean the work of authorship, whether in Source or | |
| 36 | Object form, made available under the License, as indicated by a | |
| 37 | copyright notice that is included in or attached to the work | |
| 38 | (an example is provided in the Appendix below). | |
| 39 | ||
| 40 | "Derivative Works" shall mean any work, whether in Source or Object | |
| 41 | form, that is based on (or derived from) the Work and for which the | |
| 42 | editorial revisions, annotations, elaborations, or other modifications | |
| 43 | represent, as a whole, an original work of authorship. For the purposes | |
| 44 | of this License, Derivative Works shall not include works that remain | |
| 45 | separable from, or merely link (or bind by name) to the interfaces of, | |
| 46 | the Work and Derivative Works thereof. | |
| 47 | ||
| 48 | "Contribution" shall mean any work of authorship, including | |
| 49 | the original version of the Work and any modifications or additions | |
| 50 | to that Work or Derivative Works thereof, that is intentionally | |
| 51 | submitted to Licensor for inclusion in the Work by the copyright owner | |
| 52 | or by an individual or Legal Entity authorized to submit on behalf of | |
| 53 | the copyright owner. For the purposes of this definition, "submitted" | |
| 54 | means any form of electronic, verbal, or written communication sent | |
| 55 | to the Licensor or its representatives, including but not limited to | |
| 56 | communication on electronic mailing lists, source code control systems, | |
| 57 | and issue tracking systems that are managed by, or on behalf of, the | |
| 58 | Licensor for the purpose of discussing and improving the Work, but | |
| 59 | excluding communication that is conspicuously marked or otherwise | |
| 60 | designated in writing by the copyright owner as "Not a Contribution." | |
| 61 | ||
| 62 | "Contributor" shall mean Licensor and any individual or Legal Entity | |
| 63 | on behalf of whom a Contribution has been received by Licensor and | |
| 64 | subsequently incorporated within the Work. | |
| 65 | ||
| 66 | 2. Grant of Copyright License. Subject to the terms and conditions of | |
| 67 | this License, each Contributor hereby grants to You a perpetual, | |
| 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 69 | copyright license to reproduce, prepare Derivative Works of, | |
| 70 | publicly display, publicly perform, sublicense, and distribute the | |
| 71 | Work and such Derivative Works in Source or Object form. | |
| 72 | ||
| 73 | 3. Grant of Patent License. Subject to the terms and conditions of | |
| 74 | this License, each Contributor hereby grants to You a perpetual, | |
| 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 76 | (except as stated in this section) patent license to make, have made, | |
| 77 | use, offer to sell, sell, import, and otherwise transfer the Work, | |
| 78 | where such license applies only to those patent claims licensable | |
| 79 | by such Contributor that are necessarily infringed by their | |
| 80 | Contribution(s) alone or by combination of their Contribution(s) | |
| 81 | with the Work to which such Contribution(s) was submitted. If You | |
| 82 | institute patent litigation against any entity (including a | |
| 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work | |
| 84 | or a Contribution incorporated within the Work constitutes direct | |
| 85 | or contributory patent infringement, then any patent licenses | |
| 86 | granted to You under this License for that Work shall terminate | |
| 87 | as of the date such litigation is filed. | |
| 88 | ||
| 89 | 4. Redistribution. You may reproduce and distribute copies of the | |
| 90 | Work or Derivative Works thereof in any medium, with or without | |
| 91 | modifications, and in Source or Object form, provided that You | |
| 92 | meet the following conditions: | |
| 93 | ||
| 94 | (a) You must give any other recipients of the Work or | |
| 95 | Derivative Works a copy of this License; and | |
| 96 | ||
| 97 | (b) You must cause any modified files to carry prominent notices | |
| 98 | stating that You changed the files; and | |
| 99 | ||
| 100 | (c) You must retain, in the Source form of any Derivative Works | |
| 101 | that You distribute, all copyright, patent, trademark, and | |
| 102 | attribution notices from the Source form of the Work, | |
| 103 | excluding those notices that do not pertain to any part of | |
| 104 | the Derivative Works; and | |
| 105 | ||
| 106 | (d) If the Work includes a "NOTICE" text file as part of its | |
| 107 | distribution, then any Derivative Works that You distribute must | |
| 108 | include a readable copy of the attribution notices contained | |
| 109 | within such NOTICE file, excluding those notices that do not | |
| 110 | pertain to any part of the Derivative Works, in at least one | |
| 111 | of the following places: within a NOTICE text file distributed | |
| 112 | as part of the Derivative Works; within the Source form or | |
| 113 | documentation, if provided along with the Derivative Works; or, | |
| 114 | within a display generated by the Derivative Works, if and | |
| 115 | wherever such third-party notices normally appear. The contents | |
| 116 | of the NOTICE file are for informational purposes only and | |
| 117 | do not modify the License. You may add Your own attribution | |
| 118 | notices within Derivative Works that You distribute, alongside | |
| 119 | or as an addendum to the NOTICE text from the Work, provided | |
| 120 | that such additional attribution notices cannot be construed | |
| 121 | as modifying the License. | |
| 122 | ||
| 123 | You may add Your own copyright statement to Your modifications and | |
| 124 | may provide additional or different license terms and conditions | |
| 125 | for use, reproduction, or distribution of Your modifications, or | |
| 126 | for any such Derivative Works as a whole, provided Your use, | |
| 127 | reproduction, and distribution of the Work otherwise complies with | |
| 128 | the conditions stated in this License. | |
| 129 | ||
| 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, | |
| 131 | any Contribution intentionally submitted for inclusion in the Work | |
| 132 | by You to the Licensor shall be under the terms and conditions of | |
| 133 | this License, without any additional terms or conditions. | |
| 134 | Notwithstanding the above, nothing herein shall supersede or modify | |
| 135 | the terms of any separate license agreement you may have executed | |
| 136 | with Licensor regarding such Contributions. | |
| 137 | ||
| 138 | 6. Trademarks. This License does not grant permission to use the trade | |
| 139 | names, trademarks, service marks, or product names of the Licensor, | |
| 140 | except as required for reasonable and customary use in describing the | |
| 141 | origin of the Work and reproducing the content of the NOTICE file. | |
| 142 | ||
| 143 | 7. Disclaimer of Warranty. Unless required by applicable law or | |
| 144 | agreed to in writing, Licensor provides the Work (and each | |
| 145 | Contributor provides its Contributions) on an "AS IS" BASIS, | |
| 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |
| 147 | implied, including, without limitation, any warranties or conditions | |
| 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | |
| 149 | PARTICULAR PURPOSE. You are solely responsible for determining the | |
| 150 | appropriateness of using or redistributing the Work and assume any | |
| 151 | risks associated with Your exercise of permissions under this License. | |
| 152 | ||
| 153 | 8. Limitation of Liability. In no event and under no legal theory, | |
| 154 | whether in tort (including negligence), contract, or otherwise, | |
| 155 | unless required by applicable law (such as deliberate and grossly | |
| 156 | negligent acts) or agreed to in writing, shall any Contributor be | |
| 157 | liable to You for damages, including any direct, indirect, special, | |
| 158 | incidental, or consequential damages of any character arising as a | |
| 159 | result of this License or out of the use or inability to use the | |
| 160 | Work (including but not limited to damages for loss of goodwill, | |
| 161 | work stoppage, computer failure or malfunction, or any and all | |
| 162 | other commercial damages or losses), even if such Contributor | |
| 163 | has been advised of the possibility of such damages. | |
| 164 | ||
| 165 | 9. Accepting Warranty or Additional Liability. While redistributing | |
| 166 | the Work or Derivative Works thereof, You may choose to offer, | |
| 167 | and charge a fee for, acceptance of support, warranty, indemnity, | |
| 168 | or other liability obligations and/or rights consistent with this | |
| 169 | License. However, in accepting such obligations, You may act only | |
| 170 | on Your own behalf and on Your sole responsibility, not on behalf | |
| 171 | of any other Contributor, and only if You agree to indemnify, | |
| 172 | defend, and hold each Contributor harmless for any liability | |
| 173 | incurred by, or claims asserted against, such Contributor by reason | |
| 174 | of your accepting any such warranty or additional liability. | |
| 175 | ||
| 176 | END OF TERMS AND CONDITIONS | |
| 177 | ||
| 178 | APPENDIX: How to apply the Apache License to your work. | |
| 179 | ||
| 180 | To apply the Apache License to your work, attach the following | |
| 181 | boilerplate notice, with the fields enclosed by brackets "[]" | |
| 182 | replaced with your own identifying information. (Don't include | |
| 183 | the brackets!) The text should be enclosed in the appropriate | |
| 184 | comment syntax for the file format. We also recommend that a | |
| 185 | file or class name and description of purpose be included on the | |
| 186 | same "printed page" as the copyright notice for easier | |
| 187 | identification within third-party archives. | |
| 188 | ||
| 189 | Copyright [yyyy] [name of copyright owner] | |
| 190 | ||
| 191 | Licensed under the Apache License, Version 2.0 (the "License"); | |
| 192 | you may not use this file except in compliance with the License. | |
| 193 | You may obtain a copy of the License at | |
| 194 | ||
| 195 | http://www.apache.org/licenses/LICENSE-2.0 | |
| 196 | ||
| 197 | Unless required by applicable law or agreed to in writing, software | |
| 198 | distributed under the License is distributed on an "AS IS" BASIS, | |
| 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 200 | See the License for the specific language governing permissions and | |
| 201 | limitations under the License. |
| … | … | @@ -0,0 +1,20 @@ |
| 1 | # bare-utils | |
| 2 | ||
| 3 | Node.js-compatibile utility functions for Bare. | |
| 4 | ||
| 5 | ``` | |
| 6 | npm i bare-utils | |
| 7 | ``` | |
| 8 | ||
| 9 | ## Usage | |
| 10 | ||
| 11 | ```js | |
| 12 | const util = require('bare-utils') | |
| 13 | ||
| 14 | util.format('Hello %s', 'world!') | |
| 15 | // Hello world! | |
| 16 | ``` | |
| 17 | ||
| 18 | ## License | |
| 19 | ||
| 20 | Apache-2.0 |
| … | … | @@ -0,0 +1,15 @@ |
| 1 | const { TextEncoder, TextDecoder } = require('bare-encoding') | |
| 2 | ||
| 3 | exports.TextEncoder = TextEncoder | |
| 4 | exports.TextDecoder = TextDecoder | |
| 5 | ||
| 6 | exports.debuglog = require('bare-debug-log') | |
| 7 | exports.format = require('bare-format') | |
| 8 | exports.formatWithOptions = exports.format.formatWithOptions | |
| 9 | exports.inspect = require('bare-inspect') | |
| 10 | exports.styleText = require('bare-stylize') | |
| 11 | ||
| 12 | exports.deprecate = require('./lib/deprecate') | |
| 13 | exports.inherits = require('./lib/inherits') | |
| 14 | exports.promisify = require('./lib/promisify') | |
| 15 | exports.types = require('./lib/types') |
| … | … | @@ -0,0 +1,19 @@ |
| 1 | module.exports = function deprecate(fn, msg, code) { | |
| 2 | let warned = false | |
| 3 | ||
| 4 | return function (...args) { | |
| 5 | if (!warned) { | |
| 6 | warned = true | |
| 7 | ||
| 8 | if (code) { | |
| 9 | console.warn(`[${code}]`, 'DeprecationWarning:', msg) | |
| 10 | } else { | |
| 11 | console.warn('DeprecationWarning:', msg) | |
| 12 | } | |
| 13 | } | |
| 14 | ||
| 15 | if (new.target) return Reflect.construct(fn, args, new.target) | |
| 16 | ||
| 17 | return Reflect.apply(fn, this, args) | |
| 18 | } | |
| 19 | } |
| … | … | @@ -0,0 +1,3 @@ |
| 1 | module.exports = function inherits(ctor, superCtor) { | |
| 2 | if (superCtor) Object.setPrototypeOf(ctor.prototype, superCtor.prototype) | |
| 3 | } |
| … | … | @@ -0,0 +1,12 @@ |
| 1 | module.exports = function promisify(fn) { | |
| 2 | return function (...args) { | |
| 3 | return new Promise((resolve, reject) => { | |
| 4 | args.push((err, ...result) => { | |
| 5 | if (err) reject(err) | |
| 6 | else resolve(...result) | |
| 7 | }) | |
| 8 | ||
| 9 | Reflect.apply(fn, this, args) | |
| 10 | }) | |
| 11 | } | |
| 12 | } |
| … | … | @@ -0,0 +1,103 @@ |
| 1 | const type = require('bare-type') | |
| 2 | ||
| 3 | exports.isAnyArrayBuffer = (value) => { | |
| 4 | const t = type(value) | |
| 5 | return t.isArrayBuffer() || t.isSharedArrayBuffer() | |
| 6 | } | |
| 7 | ||
| 8 | exports.isArrayBufferView = (value) => ArrayBuffer.isView(value) | |
| 9 | ||
| 10 | exports.isArgumentsObject = (value) => type(value).isArguments() | |
| 11 | ||
| 12 | exports.isArrayBuffer = (value) => type(value).isArrayBuffer() | |
| 13 | ||
| 14 | exports.isAsyncFunction = (value) => type(value).isAsyncFunction() | |
| 15 | ||
| 16 | exports.isBigInt64Array = (value) => type(value).isBigInt64Array() | |
| 17 | ||
| 18 | exports.isBigIntObject = (value) => value instanceof BigInt | |
| 19 | ||
| 20 | exports.isBigUint64Array = (value) => type(value).isBigUint64Array() | |
| 21 | ||
| 22 | exports.isBooleanObject = (value) => value instanceof Boolean | |
| 23 | ||
| 24 | exports.isBoxedPrimitive = (value) => | |
| 25 | exports.isBigIntObject(value) || | |
| 26 | exports.isBooleanObject(value) || | |
| 27 | exports.isNumberObject(value) || | |
| 28 | exports.isStringObject(value) || | |
| 29 | exports.isSymbolObject(value) | |
| 30 | ||
| 31 | exports.isCryptoKey = () => { | |
| 32 | throw new Error('Not implemented') | |
| 33 | } | |
| 34 | ||
| 35 | exports.isDataView = (value) => type(value).isDataView() | |
| 36 | ||
| 37 | exports.isDate = (value) => type(value).isDate() | |
| 38 | ||
| 39 | exports.isExternal = (value) => type(value).isExternal() | |
| 40 | ||
| 41 | exports.isFloat16Array = () => { | |
| 42 | throw new Error('Not implemented') | |
| 43 | } | |
| 44 | ||
| 45 | exports.isFloat32Array = (value) => type(value).isFloat32Array() | |
| 46 | ||
| 47 | exports.isFloat64Array = (value) => type(value).isFloat64Array() | |
| 48 | ||
| 49 | exports.isGeneratorFunction = (value) => type(value).isGeneratorFunction() | |
| 50 | ||
| 51 | exports.isGeneratorObject = (value) => type(value).isGenerator() | |
| 52 | ||
| 53 | exports.isInt8Array = (value) => type(value).isInt8Array() | |
| 54 | ||
| 55 | exports.isInt16Array = (value) => type(value).isInt16Array() | |
| 56 | ||
| 57 | exports.isInt32Array = (value) => type(value).isInt32Array() | |
| 58 | ||
| 59 | exports.isKeyObject = () => { | |
| 60 | throw new Error('Not implemented') | |
| 61 | } | |
| 62 | ||
| 63 | exports.isMap = (value) => type(value).isMap() | |
| 64 | ||
| 65 | exports.isMapIterator = () => { | |
| 66 | throw new Error('Not implemented') | |
| 67 | } | |
| 68 | ||
| 69 | exports.isModuleNamespaceObject = (value) => type(value).isModuleNamespace() | |
| 70 | ||
| 71 | exports.isNativeError = () => { | |
| 72 | throw new Error('Not implemented') | |
| 73 | } | |
| 74 | ||
| 75 | exports.isNumberObject = (value) => value instanceof Number | |
| 76 | ||
| 77 | exports.isPromise = (value) => type(value).isPromise() | |
| 78 | ||
| 79 | exports.isProxy = (value) => type(value).isProxy() | |
| 80 | ||
| 81 | exports.isRegExp = (value) => type(value).isRegExp() | |
| 82 | ||
| 83 | exports.isSet = (value) => type(value).isSet() | |
| 84 | ||
| 85 | exports.isSetIterator = () => { | |
| 86 | throw new Error('Not implemented') | |
| 87 | } | |
| 88 | ||
| 89 | exports.isSharedArrayBuffer = (value) => type(value).isSharedArrayBuffer() | |
| 90 | ||
| 91 | exports.isStringObject = (value) => value instanceof String | |
| 92 | ||
| 93 | exports.isSymbolObject = (value) => value instanceof Symbol | |
| 94 | ||
| 95 | exports.isTypedArray = (value) => type(value).isTypedArray() | |
| 96 | ||
| 97 | exports.isUint8Array = (value) => type(value).isUint8Array() | |
| 98 | ||
| 99 | exports.isUint8ClampedArray = (value) => type(value).isUint8ClampedArray() | |
| 100 | ||
| 101 | exports.isUint16Array = (value) => type(value).isUint16Array() | |
| 102 | ||
| 103 | exports.isUint32Array = (value) => type(value).isUint32Array() |
| … | … | @@ -0,0 +1,41 @@ |
| 1 | { | |
| 2 | "name": "bare-utils", | |
| 3 | "version": "1.6.0", | |
| 4 | "description": "Node.js-compatible utility functions for Bare", | |
| 5 | "exports": { | |
| 6 | "./package": "./package.json", | |
| 7 | ".": "./index.js", | |
| 8 | "./types": "./lib/types.js" | |
| 9 | }, | |
| 10 | "files": [ | |
| 11 | "index.js", | |
| 12 | "types.js", | |
| 13 | "lib" | |
| 14 | ], | |
| 15 | "scripts": { | |
| 16 | "test": "prettier . --check && bare test.js" | |
| 17 | }, | |
| 18 | "repository": { | |
| 19 | "type": "git", | |
| 20 | "url": "git+https://github.com/holepunchto/bare-utils.git" | |
| 21 | }, | |
| 22 | "author": "Holepunch", | |
| 23 | "license": "Apache-2.0", | |
| 24 | "bugs": { | |
| 25 | "url": "https://github.com/holepunchto/bare-utils/issues" | |
| 26 | }, | |
| 27 | "homepage": "https://github.com/holepunchto/bare-utils#readme", | |
| 28 | "dependencies": { | |
| 29 | "bare-debug-log": "^2.0.0", | |
| 30 | "bare-encoding": "^1.0.0", | |
| 31 | "bare-format": "^1.0.0", | |
| 32 | "bare-inspect": "^3.0.0", | |
| 33 | "bare-stylize": "^0.0.1", | |
| 34 | "bare-type": "^1.0.6" | |
| 35 | }, | |
| 36 | "devDependencies": { | |
| 37 | "brittle": "^3.5.0", | |
| 38 | "prettier": "^3.5.3", | |
| 39 | "prettier-config-holepunch": "^2.0.0" | |
| 40 | } | |
| 41 | } |
| … | … | @@ -0,0 +1,22 @@ |
| 1 | name: Run linter | |
| 2 | ||
| 3 | on: | |
| 4 | push: | |
| 5 | branches: [ main ] | |
| 6 | pull_request: | |
| 7 | branches: [ main ] | |
| 8 | ||
| 9 | jobs: | |
| 10 | lint: | |
| 11 | runs-on: ubuntu-latest | |
| 12 | steps: | |
| 13 | - uses: actions/checkout@v4 | |
| 14 | ||
| 15 | - name: Setup Node.js | |
| 16 | uses: actions/setup-node@v4 | |
| 17 | ||
| 18 | - name: Install dependencies | |
| 19 | run: npm i | |
| 20 | ||
| 21 | - name: Lint | |
| 22 | run: npm run lint |
| … | … | @@ -0,0 +1,202 @@ |
| 1 | Apache License | |
| 2 | Version 2.0, January 2004 | |
| 3 | http://www.apache.org/licenses/ | |
| 4 | ||
| 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | |
| 6 | ||
| 7 | 1. Definitions. | |
| 8 | ||
| 9 | "License" shall mean the terms and conditions for use, reproduction, | |
| 10 | and distribution as defined by Sections 1 through 9 of this document. | |
| 11 | ||
| 12 | "Licensor" shall mean the copyright owner or entity authorized by | |
| 13 | the copyright owner that is granting the License. | |
| 14 | ||
| 15 | "Legal Entity" shall mean the union of the acting entity and all | |
| 16 | other entities that control, are controlled by, or are under common | |
| 17 | control with that entity. For the purposes of this definition, | |
| 18 | "control" means (i) the power, direct or indirect, to cause the | |
| 19 | direction or management of such entity, whether by contract or | |
| 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the | |
| 21 | outstanding shares, or (iii) beneficial ownership of such entity. | |
| 22 | ||
| 23 | "You" (or "Your") shall mean an individual or Legal Entity | |
| 24 | exercising permissions granted by this License. | |
| 25 | ||
| 26 | "Source" form shall mean the preferred form for making modifications, | |
| 27 | including but not limited to software source code, documentation | |
| 28 | source, and configuration files. | |
| 29 | ||
| 30 | "Object" form shall mean any form resulting from mechanical | |
| 31 | transformation or translation of a Source form, including but | |
| 32 | not limited to compiled object code, generated documentation, | |
| 33 | and conversions to other media types. | |
| 34 | ||
| 35 | "Work" shall mean the work of authorship, whether in Source or | |
| 36 | Object form, made available under the License, as indicated by a | |
| 37 | copyright notice that is included in or attached to the work | |
| 38 | (an example is provided in the Appendix below). | |
| 39 | ||
| 40 | "Derivative Works" shall mean any work, whether in Source or Object | |
| 41 | form, that is based on (or derived from) the Work and for which the | |
| 42 | editorial revisions, annotations, elaborations, or other modifications | |
| 43 | represent, as a whole, an original work of authorship. For the purposes | |
| 44 | of this License, Derivative Works shall not include works that remain | |
| 45 | separable from, or merely link (or bind by name) to the interfaces of, | |
| 46 | the Work and Derivative Works thereof. | |
| 47 | ||
| 48 | "Contribution" shall mean any work of authorship, including | |
| 49 | the original version of the Work and any modifications or additions | |
| 50 | to that Work or Derivative Works thereof, that is intentionally | |
| 51 | submitted to Licensor for inclusion in the Work by the copyright owner | |
| 52 | or by an individual or Legal Entity authorized to submit on behalf of | |
| 53 | the copyright owner. For the purposes of this definition, "submitted" | |
| 54 | means any form of electronic, verbal, or written communication sent | |
| 55 | to the Licensor or its representatives, including but not limited to | |
| 56 | communication on electronic mailing lists, source code control systems, | |
| 57 | and issue tracking systems that are managed by, or on behalf of, the | |
| 58 | Licensor for the purpose of discussing and improving the Work, but | |
| 59 | excluding communication that is conspicuously marked or otherwise | |
| 60 | designated in writing by the copyright owner as "Not a Contribution." | |
| 61 | ||
| 62 | "Contributor" shall mean Licensor and any individual or Legal Entity | |
| 63 | on behalf of whom a Contribution has been received by Licensor and | |
| 64 | subsequently incorporated within the Work. | |
| 65 | ||
| 66 | 2. Grant of Copyright License. Subject to the terms and conditions of | |
| 67 | this License, each Contributor hereby grants to You a perpetual, | |
| 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 69 | copyright license to reproduce, prepare Derivative Works of, | |
| 70 | publicly display, publicly perform, sublicense, and distribute the | |
| 71 | Work and such Derivative Works in Source or Object form. | |
| 72 | ||
| 73 | 3. Grant of Patent License. Subject to the terms and conditions of | |
| 74 | this License, each Contributor hereby grants to You a perpetual, | |
| 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 76 | (except as stated in this section) patent license to make, have made, | |
| 77 | use, offer to sell, sell, import, and otherwise transfer the Work, | |
| 78 | where such license applies only to those patent claims licensable | |
| 79 | by such Contributor that are necessarily infringed by their | |
| 80 | Contribution(s) alone or by combination of their Contribution(s) | |
| 81 | with the Work to which such Contribution(s) was submitted. If You | |
| 82 | institute patent litigation against any entity (including a | |
| 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work | |
| 84 | or a Contribution incorporated within the Work constitutes direct | |
| 85 | or contributory patent infringement, then any patent licenses | |
| 86 | granted to You under this License for that Work shall terminate | |
| 87 | as of the date such litigation is filed. | |
| 88 | ||
| 89 | 4. Redistribution. You may reproduce and distribute copies of the | |
| 90 | Work or Derivative Works thereof in any medium, with or without | |
| 91 | modifications, and in Source or Object form, provided that You | |
| 92 | meet the following conditions: | |
| 93 | ||
| 94 | (a) You must give any other recipients of the Work or | |
| 95 | Derivative Works a copy of this License; and | |
| 96 | ||
| 97 | (b) You must cause any modified files to carry prominent notices | |
| 98 | stating that You changed the files; and | |
| 99 | ||
| 100 | (c) You must retain, in the Source form of any Derivative Works | |
| 101 | that You distribute, all copyright, patent, trademark, and | |
| 102 | attribution notices from the Source form of the Work, | |
| 103 | excluding those notices that do not pertain to any part of | |
| 104 | the Derivative Works; and | |
| 105 | ||
| 106 | (d) If the Work includes a "NOTICE" text file as part of its | |
| 107 | distribution, then any Derivative Works that You distribute must | |
| 108 | include a readable copy of the attribution notices contained | |
| 109 | within such NOTICE file, excluding those notices that do not | |
| 110 | pertain to any part of the Derivative Works, in at least one | |
| 111 | of the following places: within a NOTICE text file distributed | |
| 112 | as part of the Derivative Works; within the Source form or | |
| 113 | documentation, if provided along with the Derivative Works; or, | |
| 114 | within a display generated by the Derivative Works, if and | |
| 115 | wherever such third-party notices normally appear. The contents | |
| 116 | of the NOTICE file are for informational purposes only and | |
| 117 | do not modify the License. You may add Your own attribution | |
| 118 | notices within Derivative Works that You distribute, alongside | |
| 119 | or as an addendum to the NOTICE text from the Work, provided | |
| 120 | that such additional attribution notices cannot be construed | |
| 121 | as modifying the License. | |
| 122 | ||
| 123 | You may add Your own copyright statement to Your modifications and | |
| 124 | may provide additional or different license terms and conditions | |
| 125 | for use, reproduction, or distribution of Your modifications, or | |
| 126 | for any such Derivative Works as a whole, provided Your use, | |
| 127 | reproduction, and distribution of the Work otherwise complies with | |
| 128 | the conditions stated in this License. | |
| 129 | ||
| 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, | |
| 131 | any Contribution intentionally submitted for inclusion in the Work | |
| 132 | by You to the Licensor shall be under the terms and conditions of | |
| 133 | this License, without any additional terms or conditions. | |
| 134 | Notwithstanding the above, nothing herein shall supersede or modify | |
| 135 | the terms of any separate license agreement you may have executed | |
| 136 | with Licensor regarding such Contributions. | |
| 137 | ||
| 138 | 6. Trademarks. This License does not grant permission to use the trade | |
| 139 | names, trademarks, service marks, or product names of the Licensor, | |
| 140 | except as required for reasonable and customary use in describing the | |
| 141 | origin of the Work and reproducing the content of the NOTICE file. | |
| 142 | ||
| 143 | 7. Disclaimer of Warranty. Unless required by applicable law or | |
| 144 | agreed to in writing, Licensor provides the Work (and each | |
| 145 | Contributor provides its Contributions) on an "AS IS" BASIS, | |
| 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |
| 147 | implied, including, without limitation, any warranties or conditions | |
| 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | |
| 149 | PARTICULAR PURPOSE. You are solely responsible for determining the | |
| 150 | appropriateness of using or redistributing the Work and assume any | |
| 151 | risks associated with Your exercise of permissions under this License. | |
| 152 | ||
| 153 | 8. Limitation of Liability. In no event and under no legal theory, | |
| 154 | whether in tort (including negligence), contract, or otherwise, | |
| 155 | unless required by applicable law (such as deliberate and grossly | |
| 156 | negligent acts) or agreed to in writing, shall any Contributor be | |
| 157 | liable to You for damages, including any direct, indirect, special, | |
| 158 | incidental, or consequential damages of any character arising as a | |
| 159 | result of this License or out of the use or inability to use the | |
| 160 | Work (including but not limited to damages for loss of goodwill, | |
| 161 | work stoppage, computer failure or malfunction, or any and all | |
| 162 | other commercial damages or losses), even if such Contributor | |
| 163 | has been advised of the possibility of such damages. | |
| 164 | ||
| 165 | 9. Accepting Warranty or Additional Liability. While redistributing | |
| 166 | the Work or Derivative Works thereof, You may choose to offer, | |
| 167 | and charge a fee for, acceptance of support, warranty, indemnity, | |
| 168 | or other liability obligations and/or rights consistent with this | |
| 169 | License. However, in accepting such obligations, You may act only | |
| 170 | on Your own behalf and on Your sole responsibility, not on behalf | |
| 171 | of any other Contributor, and only if You agree to indemnify, | |
| 172 | defend, and hold each Contributor harmless for any liability | |
| 173 | incurred by, or claims asserted against, such Contributor by reason | |
| 174 | of your accepting any such warranty or additional liability. | |
| 175 | ||
| 176 | END OF TERMS AND CONDITIONS | |
| 177 | ||
| 178 | APPENDIX: How to apply the Apache License to your work. | |
| 179 | ||
| 180 | To apply the Apache License to your work, attach the following | |
| 181 | boilerplate notice, with the fields enclosed by brackets "[]" | |
| 182 | replaced with your own identifying information. (Don't include | |
| 183 | the brackets!) The text should be enclosed in the appropriate | |
| 184 | comment syntax for the file format. We also recommend that a | |
| 185 | file or class name and description of purpose be included on the | |
| 186 | same "printed page" as the copyright notice for easier | |
| 187 | identification within third-party archives. | |
| 188 | ||
| 189 | Copyright [yyyy] [name of copyright owner] | |
| 190 | ||
| 191 | Licensed under the Apache License, Version 2.0 (the "License"); | |
| 192 | you may not use this file except in compliance with the License. | |
| 193 | You may obtain a copy of the License at | |
| 194 | ||
| 195 | http://www.apache.org/licenses/LICENSE-2.0 | |
| 196 | ||
| 197 | Unless required by applicable law or agreed to in writing, software | |
| 198 | distributed under the License is distributed on an "AS IS" BASIS, | |
| 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 200 | See the License for the specific language governing permissions and | |
| 201 | limitations under the License. | |
| 202 |
| … | … | @@ -0,0 +1,9 @@ |
| 1 | # bare-v8-to-istanbul | |
| 2 | Wrapper for v8-to-istanbul to make it work with Bare | |
| 3 | ||
| 4 | ```bash | |
| 5 | npm i bare-v8-to-istanbul | |
| 6 | ``` | |
| 7 | ||
| 8 | ## License | |
| 9 | Apache-2.0 |
| … | … | @@ -0,0 +1,10 @@ |
| 1 | const { isBare } = require('which-runtime') | |
| 2 | ||
| 3 | if (isBare) { | |
| 4 | const originalProcess = global.process | |
| 5 | global.process = require('bare-process') | |
| 6 | ||
| 7 | try { module.exports = require('v8-to-istanbul', { with: { imports: './package.json' } }) } finally { global.process = originalProcess } | |
| 8 | } else { | |
| 9 | module.exports = require('v8-to-istanbul') | |
| 10 | } |
| … | … | @@ -0,0 +1,56 @@ |
| 1 | { | |
| 2 | "name": "bare-v8-to-istanbul", | |
| 3 | "version": "1.0.2", | |
| 4 | "description": "Wrapper for v8-to-istanbul that works with Bare", | |
| 5 | "license": "Apache-2.0", | |
| 6 | "author": "Holepunch", | |
| 7 | "main": "index.js", | |
| 8 | "scripts": { | |
| 9 | "test": "echo \"Error: no test specified\" && exit 1", | |
| 10 | "lint": "standard" | |
| 11 | }, | |
| 12 | "dependencies": { | |
| 13 | "bare-assert": "^1.0.2", | |
| 14 | "bare-fs": "^4.1.2", | |
| 15 | "bare-module": "^6.1.2", | |
| 16 | "bare-path": "^3.0.0", | |
| 17 | "bare-process": "^4.2.0", | |
| 18 | "bare-url": "^2.1.5", | |
| 19 | "bare-utils": "^1.2.0", | |
| 20 | "v8-to-istanbul": "^9.3.0", | |
| 21 | "which-runtime": "^1.2.1" | |
| 22 | }, | |
| 23 | "imports": { | |
| 24 | "assert": { | |
| 25 | "bare": "bare-assert", | |
| 26 | "default": "assert" | |
| 27 | }, | |
| 28 | "fs": { | |
| 29 | "bare": "bare-fs", | |
| 30 | "default": "fs" | |
| 31 | }, | |
| 32 | "module": { | |
| 33 | "bare": "bare-module", | |
| 34 | "default": "module" | |
| 35 | }, | |
| 36 | "path": { | |
| 37 | "bare": "bare-path", | |
| 38 | "default": "path" | |
| 39 | }, | |
| 40 | "process": { | |
| 41 | "bare": "bare-process", | |
| 42 | "default": "process" | |
| 43 | }, | |
| 44 | "url": { | |
| 45 | "bare": "bare-url", | |
| 46 | "default": "url" | |
| 47 | }, | |
| 48 | "util": { | |
| 49 | "bare": "bare-utils", | |
| 50 | "default": "util" | |
| 51 | } | |
| 52 | }, | |
| 53 | "devDependencies": { | |
| 54 | "standard": "^17.1.2" | |
| 55 | } | |
| 56 | } |
| … | … | @@ -0,0 +1,201 @@ |
| 1 | Apache License | |
| 2 | Version 2.0, January 2004 | |
| 3 | http://www.apache.org/licenses/ | |
| 4 | ||
| 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | |
| 6 | ||
| 7 | 1. Definitions. | |
| 8 | ||
| 9 | "License" shall mean the terms and conditions for use, reproduction, | |
| 10 | and distribution as defined by Sections 1 through 9 of this document. | |
| 11 | ||
| 12 | "Licensor" shall mean the copyright owner or entity authorized by | |
| 13 | the copyright owner that is granting the License. | |
| 14 | ||
| 15 | "Legal Entity" shall mean the union of the acting entity and all | |
| 16 | other entities that control, are controlled by, or are under common | |
| 17 | control with that entity. For the purposes of this definition, | |
| 18 | "control" means (i) the power, direct or indirect, to cause the | |
| 19 | direction or management of such entity, whether by contract or | |
| 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the | |
| 21 | outstanding shares, or (iii) beneficial ownership of such entity. | |
| 22 | ||
| 23 | "You" (or "Your") shall mean an individual or Legal Entity | |
| 24 | exercising permissions granted by this License. | |
| 25 | ||
| 26 | "Source" form shall mean the preferred form for making modifications, | |
| 27 | including but not limited to software source code, documentation | |
| 28 | source, and configuration files. | |
| 29 | ||
| 30 | "Object" form shall mean any form resulting from mechanical | |
| 31 | transformation or translation of a Source form, including but | |
| 32 | not limited to compiled object code, generated documentation, | |
| 33 | and conversions to other media types. | |
| 34 | ||
| 35 | "Work" shall mean the work of authorship, whether in Source or | |
| 36 | Object form, made available under the License, as indicated by a | |
| 37 | copyright notice that is included in or attached to the work | |
| 38 | (an example is provided in the Appendix below). | |
| 39 | ||
| 40 | "Derivative Works" shall mean any work, whether in Source or Object | |
| 41 | form, that is based on (or derived from) the Work and for which the | |
| 42 | editorial revisions, annotations, elaborations, or other modifications | |
| 43 | represent, as a whole, an original work of authorship. For the purposes | |
| 44 | of this License, Derivative Works shall not include works that remain | |
| 45 | separable from, or merely link (or bind by name) to the interfaces of, | |
| 46 | the Work and Derivative Works thereof. | |
| 47 | ||
| 48 | "Contribution" shall mean any work of authorship, including | |
| 49 | the original version of the Work and any modifications or additions | |
| 50 | to that Work or Derivative Works thereof, that is intentionally | |
| 51 | submitted to Licensor for inclusion in the Work by the copyright owner | |
| 52 | or by an individual or Legal Entity authorized to submit on behalf of | |
| 53 | the copyright owner. For the purposes of this definition, "submitted" | |
| 54 | means any form of electronic, verbal, or written communication sent | |
| 55 | to the Licensor or its representatives, including but not limited to | |
| 56 | communication on electronic mailing lists, source code control systems, | |
| 57 | and issue tracking systems that are managed by, or on behalf of, the | |
| 58 | Licensor for the purpose of discussing and improving the Work, but | |
| 59 | excluding communication that is conspicuously marked or otherwise | |
| 60 | designated in writing by the copyright owner as "Not a Contribution." | |
| 61 | ||
| 62 | "Contributor" shall mean Licensor and any individual or Legal Entity | |
| 63 | on behalf of whom a Contribution has been received by Licensor and | |
| 64 | subsequently incorporated within the Work. | |
| 65 | ||
| 66 | 2. Grant of Copyright License. Subject to the terms and conditions of | |
| 67 | this License, each Contributor hereby grants to You a perpetual, | |
| 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 69 | copyright license to reproduce, prepare Derivative Works of, | |
| 70 | publicly display, publicly perform, sublicense, and distribute the | |
| 71 | Work and such Derivative Works in Source or Object form. | |
| 72 | ||
| 73 | 3. Grant of Patent License. Subject to the terms and conditions of | |
| 74 | this License, each Contributor hereby grants to You a perpetual, | |
| 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 76 | (except as stated in this section) patent license to make, have made, | |
| 77 | use, offer to sell, sell, import, and otherwise transfer the Work, | |
| 78 | where such license applies only to those patent claims licensable | |
| 79 | by such Contributor that are necessarily infringed by their | |
| 80 | Contribution(s) alone or by combination of their Contribution(s) | |
| 81 | with the Work to which such Contribution(s) was submitted. If You | |
| 82 | institute patent litigation against any entity (including a | |
| 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work | |
| 84 | or a Contribution incorporated within the Work constitutes direct | |
| 85 | or contributory patent infringement, then any patent licenses | |
| 86 | granted to You under this License for that Work shall terminate | |
| 87 | as of the date such litigation is filed. | |
| 88 | ||
| 89 | 4. Redistribution. You may reproduce and distribute copies of the | |
| 90 | Work or Derivative Works thereof in any medium, with or without | |
| 91 | modifications, and in Source or Object form, provided that You | |
| 92 | meet the following conditions: | |
| 93 | ||
| 94 | (a) You must give any other recipients of the Work or | |
| 95 | Derivative Works a copy of this License; and | |
| 96 | ||
| 97 | (b) You must cause any modified files to carry prominent notices | |
| 98 | stating that You changed the files; and | |
| 99 | ||
| 100 | (c) You must retain, in the Source form of any Derivative Works | |
| 101 | that You distribute, all copyright, patent, trademark, and | |
| 102 | attribution notices from the Source form of the Work, | |
| 103 | excluding those notices that do not pertain to any part of | |
| 104 | the Derivative Works; and | |
| 105 | ||
| 106 | (d) If the Work includes a "NOTICE" text file as part of its | |
| 107 | distribution, then any Derivative Works that You distribute must | |
| 108 | include a readable copy of the attribution notices contained | |
| 109 | within such NOTICE file, excluding those notices that do not | |
| 110 | pertain to any part of the Derivative Works, in at least one | |
| 111 | of the following places: within a NOTICE text file distributed | |
| 112 | as part of the Derivative Works; within the Source form or | |
| 113 | documentation, if provided along with the Derivative Works; or, | |
| 114 | within a display generated by the Derivative Works, if and | |
| 115 | wherever such third-party notices normally appear. The contents | |
| 116 | of the NOTICE file are for informational purposes only and | |
| 117 | do not modify the License. You may add Your own attribution | |
| 118 | notices within Derivative Works that You distribute, alongside | |
| 119 | or as an addendum to the NOTICE text from the Work, provided | |
| 120 | that such additional attribution notices cannot be construed | |
| 121 | as modifying the License. | |
| 122 | ||
| 123 | You may add Your own copyright statement to Your modifications and | |
| 124 | may provide additional or different license terms and conditions | |
| 125 | for use, reproduction, or distribution of Your modifications, or | |
| 126 | for any such Derivative Works as a whole, provided Your use, | |
| 127 | reproduction, and distribution of the Work otherwise complies with | |
| 128 | the conditions stated in this License. | |
| 129 | ||
| 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, | |
| 131 | any Contribution intentionally submitted for inclusion in the Work | |
| 132 | by You to the Licensor shall be under the terms and conditions of | |
| 133 | this License, without any additional terms or conditions. | |
| 134 | Notwithstanding the above, nothing herein shall supersede or modify | |
| 135 | the terms of any separate license agreement you may have executed | |
| 136 | with Licensor regarding such Contributions. | |
| 137 | ||
| 138 | 6. Trademarks. This License does not grant permission to use the trade | |
| 139 | names, trademarks, service marks, or product names of the Licensor, | |
| 140 | except as required for reasonable and customary use in describing the | |
| 141 | origin of the Work and reproducing the content of the NOTICE file. | |
| 142 | ||
| 143 | 7. Disclaimer of Warranty. Unless required by applicable law or | |
| 144 | agreed to in writing, Licensor provides the Work (and each | |
| 145 | Contributor provides its Contributions) on an "AS IS" BASIS, | |
| 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |
| 147 | implied, including, without limitation, any warranties or conditions | |
| 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | |
| 149 | PARTICULAR PURPOSE. You are solely responsible for determining the | |
| 150 | appropriateness of using or redistributing the Work and assume any | |
| 151 | risks associated with Your exercise of permissions under this License. | |
| 152 | ||
| 153 | 8. Limitation of Liability. In no event and under no legal theory, | |
| 154 | whether in tort (including negligence), contract, or otherwise, | |
| 155 | unless required by applicable law (such as deliberate and grossly | |
| 156 | negligent acts) or agreed to in writing, shall any Contributor be | |
| 157 | liable to You for damages, including any direct, indirect, special, | |
| 158 | incidental, or consequential damages of any character arising as a | |
| 159 | result of this License or out of the use or inability to use the | |
| 160 | Work (including but not limited to damages for loss of goodwill, | |
| 161 | work stoppage, computer failure or malfunction, or any and all | |
| 162 | other commercial damages or losses), even if such Contributor | |
| 163 | has been advised of the possibility of such damages. | |
| 164 | ||
| 165 | 9. Accepting Warranty or Additional Liability. While redistributing | |
| 166 | the Work or Derivative Works thereof, You may choose to offer, | |
| 167 | and charge a fee for, acceptance of support, warranty, indemnity, | |
| 168 | or other liability obligations and/or rights consistent with this | |
| 169 | License. However, in accepting such obligations, You may act only | |
| 170 | on Your own behalf and on Your sole responsibility, not on behalf | |
| 171 | of any other Contributor, and only if You agree to indemnify, | |
| 172 | defend, and hold each Contributor harmless for any liability | |
| 173 | incurred by, or claims asserted against, such Contributor by reason | |
| 174 | of your accepting any such warranty or additional liability. | |
| 175 | ||
| 176 | END OF TERMS AND CONDITIONS | |
| 177 | ||
| 178 | APPENDIX: How to apply the Apache License to your work. | |
| 179 | ||
| 180 | To apply the Apache License to your work, attach the following | |
| 181 | boilerplate notice, with the fields enclosed by brackets "[]" | |
| 182 | replaced with your own identifying information. (Don't include | |
| 183 | the brackets!) The text should be enclosed in the appropriate | |
| 184 | comment syntax for the file format. We also recommend that a | |
| 185 | file or class name and description of purpose be included on the | |
| 186 | same "printed page" as the copyright notice for easier | |
| 187 | identification within third-party archives. | |
| 188 | ||
| 189 | Copyright [yyyy] [name of copyright owner] | |
| 190 | ||
| 191 | Licensed under the Apache License, Version 2.0 (the "License"); | |
| 192 | you may not use this file except in compliance with the License. | |
| 193 | You may obtain a copy of the License at | |
| 194 | ||
| 195 | http://www.apache.org/licenses/LICENSE-2.0 | |
| 196 | ||
| 197 | Unless required by applicable law or agreed to in writing, software | |
| 198 | distributed under the License is distributed on an "AS IS" BASIS, | |
| 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 200 | See the License for the specific language governing permissions and | |
| 201 | limitations under the License. |
| … | … | @@ -0,0 +1,29 @@ |
| 1 | # bare-ws | |
| 2 | ||
| 3 | WebSocket library for JavaScript. | |
| 4 | ||
| 5 | ``` | |
| 6 | npm i bare-ws | |
| 7 | ``` | |
| 8 | ||
| 9 | ## Usage | |
| 10 | ||
| 11 | ```js | |
| 12 | const ws = require('bare-ws') | |
| 13 | ||
| 14 | const server = new ws.Server({ port: 8080 }, (socket) => { | |
| 15 | socket.on('data', (data) => { | |
| 16 | console.log(data.toString()) | |
| 17 | }) | |
| 18 | }) | |
| 19 | ||
| 20 | server.on('listening', () => { | |
| 21 | const socket = new ws.Socket({ port: 8080 }) | |
| 22 | ||
| 23 | socket.write('Hello WebSocket') | |
| 24 | }) | |
| 25 | ``` | |
| 26 | ||
| 27 | ## License | |
| 28 | ||
| 29 | Apache-2.0 |
| … | … | @@ -0,0 +1,9 @@ |
| 1 | import * as ws from '.' | |
| 2 | ||
| 3 | type WebSocketConstructor = typeof ws.Socket | |
| 4 | ||
| 5 | declare global { | |
| 6 | type WebSocket = ws.Socket | |
| 7 | ||
| 8 | const WebSocket: WebSocketConstructor | |
| 9 | } |
| … | … | @@ -0,0 +1,3 @@ |
| 1 | const ws = require('.') | |
| 2 | ||
| 3 | global.WebSocket = ws.Socket |
| … | … | @@ -0,0 +1,4 @@ |
| 1 | import WebSocket from './lib/socket' | |
| 2 | import WebSocketServer from './lib/server' | |
| 3 | ||
| 4 | export { WebSocket as Socket, WebSocketServer as Server } |
| … | … | @@ -0,0 +1,2 @@ |
| 1 | exports.Server = require('./lib/server') | |
| 2 | exports.Socket = require('./lib/socket') |
| … | … | @@ -0,0 +1,17 @@ |
| 1 | import Buffer from 'bare-buffer' | |
| 2 | ||
| 3 | export const EOL: string | |
| 4 | export const EOF: string | |
| 5 | ||
| 6 | export const GUID: Buffer | |
| 7 | ||
| 8 | export const opcode: { | |
| 9 | CONTINUATION: number | |
| 10 | TEXT: number | |
| 11 | BINARY: number | |
| 12 | CLOSE: number | |
| 13 | PING: number | |
| 14 | PONG: number | |
| 15 | } | |
| 16 | ||
| 17 | export const status: { PROTOCOL_ERROR: number; MESSAGE_TOO_LARGE: number } |
| … | … | @@ -0,0 +1,20 @@ |
| 1 | exports.EOL = '\r\n' | |
| 2 | exports.EOF = exports.EOL.repeat(2) | |
| 3 | ||
| 4 | exports.GUID = Buffer.from('258EAFA5-E914-47DA-95CA-C5AB0DC85B11') | |
| 5 | ||
| 6 | // https://datatracker.ietf.org/doc/html/rfc6455#section-11.8 | |
| 7 | exports.opcode = { | |
| 8 | CONTINUATION: 0x0, | |
| 9 | TEXT: 0x1, | |
| 10 | BINARY: 0x2, | |
| 11 | CLOSE: 0x8, | |
| 12 | PING: 0x9, | |
| 13 | PONG: 0xa | |
| 14 | } | |
| 15 | ||
| 16 | // https://datatracker.ietf.org/doc/html/rfc6455#section-7.4.1 | |
| 17 | exports.status = { | |
| 18 | PROTOCOL_ERROR: 1002, | |
| 19 | MESSAGE_TOO_LARGE: 1009 | |
| 20 | } |
| … | … | @@ -0,0 +1,23 @@ |
| 1 | declare class WebSocketError extends Error { | |
| 2 | constructor(msg: string, code: string, status: number, fn?: WebSocketError, cause?: unknown) | |
| 3 | ||
| 4 | static NETWORK_ERROR(msg: string, cause?: unknown): WebSocketError | |
| 5 | static NOT_CONNECTED(msg?: string): WebSocketError | |
| 6 | static UNEXPECTED_RSV1(msg?: string): WebSocketError | |
| 7 | static UNEXPECTED_RSV2(msg?: string): WebSocketError | |
| 8 | static UNEXPECTED_RSV3(msg?: string): WebSocketError | |
| 9 | static EXPECTED_MASK(msg?: string): WebSocketError | |
| 10 | static EXPECTED_CONTINUATION(msg?: string): WebSocketError | |
| 11 | static UNEXPECTED_CONTINUATION(msg?: string): WebSocketError | |
| 12 | static UNEXPECTED_CONTROL(msg?: string): WebSocketError | |
| 13 | static INVALID_ENCODING(msg?: string): WebSocketError | |
| 14 | static INVALID_UPGRADE_HEADER(msg?: string): WebSocketError | |
| 15 | static INVALID_VERSION_HEADER(msg?: string): WebSocketError | |
| 16 | static INVALID_KEY_HEADER(msg?: string): WebSocketError | |
| 17 | static INVALID_ACCEPT_HEADER(msg?: string): WebSocketError | |
| 18 | static INVALID_OPCODE(msg?: string): WebSocketError | |
| 19 | static INVALID_PAYLOAD_LENGTH(msg?: string): WebSocketError | |
| 20 | static INCOMPLETE_FRAME(msg?: string, length?: number): WebSocketError | |
| 21 | } | |
| 22 | ||
| 23 | export = WebSocketError |
| … | … | @@ -0,0 +1,164 @@ |
| 1 | const { status } = require('./constants') | |
| 2 | ||
| 3 | module.exports = class WebSocketError extends Error { | |
| 4 | constructor(msg, code, status, fn = WebSocketError, cause) { | |
| 5 | super(`${code}: ${msg}`, { cause }) | |
| 6 | this.code = code | |
| 7 | this.status = status | |
| 8 | ||
| 9 | if (Error.captureStackTrace) { | |
| 10 | Error.captureStackTrace(this, fn) | |
| 11 | } | |
| 12 | } | |
| 13 | ||
| 14 | get name() { | |
| 15 | return 'WebSocketError' | |
| 16 | } | |
| 17 | ||
| 18 | static NETWORK_ERROR(msg, cause) { | |
| 19 | return new WebSocketError(msg, 'NETWORK_ERROR', 0, WebSocketError.NETWORK_ERROR, cause) | |
| 20 | } | |
| 21 | ||
| 22 | static NOT_CONNECTED(msg = 'Socket is not connected') { | |
| 23 | return new WebSocketError(msg, 'NOT_CONNECTED', 0, WebSocketError.NOT_CONNECTED) | |
| 24 | } | |
| 25 | ||
| 26 | static UNEXPECTED_RSV1(msg = 'RSV1 must be unset') { | |
| 27 | return new WebSocketError( | |
| 28 | msg, | |
| 29 | 'UNEXPECTED_RSV1', | |
| 30 | status.PROTOCOL_ERROR, | |
| 31 | WebSocketError.UNEXPECTED_RSV1 | |
| 32 | ) | |
| 33 | } | |
| 34 | ||
| 35 | static UNEXPECTED_RSV2(msg = 'RSV2 must be unset') { | |
| 36 | return new WebSocketError( | |
| 37 | msg, | |
| 38 | 'UNEXPECTED_RSV2', | |
| 39 | status.PROTOCOL_ERROR, | |
| 40 | WebSocketError.UNEXPECTED_RSV2 | |
| 41 | ) | |
| 42 | } | |
| 43 | ||
| 44 | static UNEXPECTED_RSV3(msg = 'RSV3 must be unset') { | |
| 45 | return new WebSocketError( | |
| 46 | msg, | |
| 47 | 'UNEXPECTED_RSV3', | |
| 48 | status.PROTOCOL_ERROR, | |
| 49 | WebSocketError.UNEXPECTED_RSV3 | |
| 50 | ) | |
| 51 | } | |
| 52 | ||
| 53 | static EXPECTED_MASK(msg = 'MASK must be set') { | |
| 54 | return new WebSocketError( | |
| 55 | msg, | |
| 56 | 'EXPECTED_MASK', | |
| 57 | status.PROTOCOL_ERROR, | |
| 58 | WebSocketError.EXPECTED_MASK | |
| 59 | ) | |
| 60 | } | |
| 61 | ||
| 62 | static UNEXPECTED_MASK(msg = 'MASK must be unset') { | |
| 63 | return new WebSocketError( | |
| 64 | msg, | |
| 65 | 'UNEXPECTED_MASK', | |
| 66 | status.PROTOCOL_ERROR, | |
| 67 | WebSocketError.UNEXPECTED_MASK | |
| 68 | ) | |
| 69 | } | |
| 70 | ||
| 71 | static EXPECTED_CONTINUATION(msg = 'Expected a continuation frame') { | |
| 72 | return new WebSocketError( | |
| 73 | msg, | |
| 74 | 'EXPECTED_CONTINUATION', | |
| 75 | status.PROTOCOL_ERROR, | |
| 76 | WebSocketError.EXPECTED_CONTINUATION | |
| 77 | ) | |
| 78 | } | |
| 79 | ||
| 80 | static UNEXPECTED_CONTINUATION(msg = 'Unexpected continuation frame') { | |
| 81 | return new WebSocketError( | |
| 82 | msg, | |
| 83 | 'UNEXPECTED_CONTINUATION', | |
| 84 | status.PROTOCOL_ERROR, | |
| 85 | WebSocketError.UNEXPECTED_CONTINUATION | |
| 86 | ) | |
| 87 | } | |
| 88 | ||
| 89 | static UNEXPECTED_CONTROL(msg = 'Unexpected control frame') { | |
| 90 | return new WebSocketError( | |
| 91 | msg, | |
| 92 | 'UNEXPECTED_CONTROL', | |
| 93 | status.PROTOCOL_ERROR, | |
| 94 | WebSocketError.UNEXPECTED_CONTROL | |
| 95 | ) | |
| 96 | } | |
| 97 | ||
| 98 | static INVALID_ENCODING(msg = 'Invalid encoding') { | |
| 99 | return new WebSocketError( | |
| 100 | msg, | |
| 101 | 'INVALID_ENCODING', | |
| 102 | status.PROTOCOL_ERROR, | |
| 103 | WebSocketError.INVALID_ENCODING | |
| 104 | ) | |
| 105 | } | |
| 106 | ||
| 107 | static INVALID_UPGRADE_HEADER(msg = 'Invalid Upgrade header') { | |
| 108 | return new WebSocketError( | |
| 109 | msg, | |
| 110 | 'INVALID_UPGRADE_HEADER', | |
| 111 | status.PROTOCOL_ERROR, | |
| 112 | WebSocketError.INVALID_UPGRADE_HEADER | |
| 113 | ) | |
| 114 | } | |
| 115 | ||
| 116 | static INVALID_VERSION_HEADER(msg = 'Invalid Sec-WebSocket-Version header') { | |
| 117 | return new WebSocketError( | |
| 118 | msg, | |
| 119 | 'INVALID_VERSION_HEADER', | |
| 120 | status.PROTOCOL_ERROR, | |
| 121 | WebSocketError.INVALID_VERSION_HEADER | |
| 122 | ) | |
| 123 | } | |
| 124 | ||
| 125 | static INVALID_KEY_HEADER(msg = 'Invalid Sec-WebSocket-Key header') { | |
| 126 | return new WebSocketError( | |
| 127 | msg, | |
| 128 | 'INVALID_KEY_HEADER', | |
| 129 | status.PROTOCOL_ERROR, | |
| 130 | WebSocketError.INVALID_KEY_HEADER | |
| 131 | ) | |
| 132 | } | |
| 133 | ||
| 134 | static INVALID_ACCEPT_HEADER(msg = 'Invalid Sec-WebSocket-Accept header') { | |
| 135 | return new WebSocketError( | |
| 136 | msg, | |
| 137 | 'INVALID_ACCEPT_HEADER', | |
| 138 | status.PROTOCOL_ERROR, | |
| 139 | WebSocketError.INVALID_ACCEPT_HEADER | |
| 140 | ) | |
| 141 | } | |
| 142 | ||
| 143 | static INVALID_OPCODE(msg = 'Invalid opcode') { | |
| 144 | return new WebSocketError( | |
| 145 | msg, | |
| 146 | 'INVALID_OPCODE', | |
| 147 | status.PROTOCOL_ERROR, | |
| 148 | WebSocketError.INVALID_OPCODE | |
| 149 | ) | |
| 150 | } | |
| 151 | ||
| 152 | static INVALID_PAYLOAD_LENGTH(msg = 'Invalid payload length') { | |
| 153 | return new WebSocketError( | |
| 154 | msg, | |
| 155 | 'INVALID_PAYLOAD_LENGTH', | |
| 156 | status.MESSAGE_TOO_LARGE, | |
| 157 | WebSocketError.INVALID_PAYLOAD_LENGTH | |
| 158 | ) | |
| 159 | } | |
| 160 | ||
| 161 | static INCOMPLETE_FRAME(msg = 'Incomplete frame', length = -1) { | |
| 162 | return new WebSocketError(msg, 'INCOMPLETE_FRAME', length, WebSocketError.INCOMPLETE_FRAME) | |
| 163 | } | |
| 164 | } |
| … | … | @@ -0,0 +1,215 @@ |
| 1 | const crypto = require('bare-crypto') | |
| 2 | const errors = require('./errors') | |
| 3 | ||
| 4 | const EMPTY = Buffer.alloc(0) | |
| 5 | ||
| 6 | const FIN = 0b10000000 | |
| 7 | const RSV1 = 0b01000000 | |
| 8 | const RSV2 = 0b00100000 | |
| 9 | const RSV3 = 0b00010000 | |
| 10 | const OPCODE = 0b00001111 | |
| 11 | const MASK = 0b10000000 | |
| 12 | const LENGTH = 0b01111111 | |
| 13 | ||
| 14 | // https://datatracker.ietf.org/doc/html/rfc6455#section-5 | |
| 15 | module.exports = exports = class Frame { | |
| 16 | constructor(opcode, payload = EMPTY, opts = {}) { | |
| 17 | if (payload && !Buffer.isBuffer(payload)) { | |
| 18 | opts = payload | |
| 19 | payload = EMPTY | |
| 20 | } | |
| 21 | ||
| 22 | const { fin = true, rsv1 = false, rsv2 = false, rsv3 = false, mask = null } = opts | |
| 23 | ||
| 24 | this.fin = fin | |
| 25 | this.rsv1 = rsv1 | |
| 26 | this.rsv2 = rsv2 | |
| 27 | this.rsv3 = rsv3 | |
| 28 | this.opcode = opcode | |
| 29 | this.mask = mask | |
| 30 | this.payload = payload | |
| 31 | } | |
| 32 | ||
| 33 | toBuffer() { | |
| 34 | const state = { start: 0, end: 0, buffer: null } | |
| 35 | ||
| 36 | Frame.preencode(state, this) | |
| 37 | ||
| 38 | state.buffer = Buffer.allocUnsafe(state.end) | |
| 39 | ||
| 40 | Frame.encode(state, this) | |
| 41 | ||
| 42 | return state.buffer | |
| 43 | } | |
| 44 | } | |
| 45 | ||
| 46 | const Frame = exports | |
| 47 | ||
| 48 | exports.preencode = function preencode(state, f) { | |
| 49 | let i = state.end | |
| 50 | ||
| 51 | i++ | |
| 52 | ||
| 53 | const length = f.payload.length | |
| 54 | ||
| 55 | if (length <= 0x7d) i++ | |
| 56 | else { | |
| 57 | if (length <= 0xffff) i += 3 | |
| 58 | else i += 9 | |
| 59 | } | |
| 60 | ||
| 61 | if (f.mask) i += 4 | |
| 62 | ||
| 63 | i += length | |
| 64 | ||
| 65 | state.end = i | |
| 66 | } | |
| 67 | ||
| 68 | exports.encode = function encode(state, f) { | |
| 69 | const b = state.buffer | |
| 70 | ||
| 71 | let i = state.start | |
| 72 | ||
| 73 | const v = new DataView(b.buffer, b.byteOffset, b.byteLength) | |
| 74 | ||
| 75 | b[i] = f.opcode & OPCODE | |
| 76 | ||
| 77 | if (f.fin) b[i] |= FIN | |
| 78 | if (f.rsv1) b[i] |= RSV1 | |
| 79 | if (f.rsv1) b[i] |= RSV2 | |
| 80 | if (f.rsv1) b[i] |= RSV3 | |
| 81 | ||
| 82 | i++ | |
| 83 | ||
| 84 | b[i] = f.mask ? MASK : 0 | |
| 85 | ||
| 86 | const length = f.payload.length | |
| 87 | ||
| 88 | if (length <= 0x7d) b[i++] |= length | |
| 89 | else { | |
| 90 | if (length <= 0xffff) { | |
| 91 | b[i++] |= 0x7e | |
| 92 | ||
| 93 | v.setUint16(i, length, false) | |
| 94 | ||
| 95 | i += 2 | |
| 96 | } else { | |
| 97 | b[i++] |= 0x7f | |
| 98 | ||
| 99 | const high = Math.floor(length / 0x100000000) | |
| 100 | ||
| 101 | v.setUint32(i, high, false) | |
| 102 | ||
| 103 | i += 4 | |
| 104 | ||
| 105 | const low = length & 0xffffffff | |
| 106 | ||
| 107 | v.setUint32(i, low, false) | |
| 108 | ||
| 109 | i += 4 | |
| 110 | } | |
| 111 | } | |
| 112 | ||
| 113 | if (f.mask) { | |
| 114 | crypto.randomFill(f.mask, 0, 4) | |
| 115 | ||
| 116 | b.set(f.mask, i) | |
| 117 | ||
| 118 | i += 4 | |
| 119 | ||
| 120 | for (let j = 0; j < length; j++) { | |
| 121 | b[i + j] = f.payload[j] ^ f.mask[j & 3] | |
| 122 | } | |
| 123 | } else { | |
| 124 | b.set(f.payload, i) | |
| 125 | } | |
| 126 | ||
| 127 | i += length | |
| 128 | ||
| 129 | state.start = i | |
| 130 | } | |
| 131 | ||
| 132 | exports.decode = function decode(state) { | |
| 133 | const s = state.start | |
| 134 | const b = state.buffer | |
| 135 | ||
| 136 | let i = s | |
| 137 | let n = b.byteLength | |
| 138 | ||
| 139 | if (n < 2) throw errors.INCOMPLETE_FRAME() | |
| 140 | ||
| 141 | const view = new DataView(b.buffer, b.byteOffset, b.byteLength) | |
| 142 | ||
| 143 | const fin = !!(b[i] & FIN) | |
| 144 | ||
| 145 | const rsv1 = !!(b[i] & RSV1) | |
| 146 | const rsv2 = !!(b[i] & RSV2) | |
| 147 | const rsv3 = !!(b[i] & RSV3) | |
| 148 | ||
| 149 | const opcode = b[i] & OPCODE | |
| 150 | ||
| 151 | i++ | |
| 152 | n-- | |
| 153 | ||
| 154 | const masked = !!(b[i] & MASK) | |
| 155 | ||
| 156 | let length = b[i] & LENGTH | |
| 157 | ||
| 158 | i++ | |
| 159 | n-- | |
| 160 | ||
| 161 | if (length === 0x7e) { | |
| 162 | if (n < 2) throw errors.INCOMPLETE_FRAME() | |
| 163 | ||
| 164 | length = view.getUint16(i, false) | |
| 165 | ||
| 166 | i += 2 | |
| 167 | n -= 2 | |
| 168 | } else if (length === 0x7f) { | |
| 169 | if (n < 8) throw errors.INCOMPLETE_FRAME() | |
| 170 | ||
| 171 | const high = view.getUint32(i, false) | |
| 172 | ||
| 173 | if (high >= 0x200000) throw errors.INVALID_PAYLOAD_LENGTH() | |
| 174 | ||
| 175 | i += 4 | |
| 176 | n -= 4 | |
| 177 | ||
| 178 | const low = view.getUint32(i, false) | |
| 179 | ||
| 180 | i += 4 | |
| 181 | n -= 4 | |
| 182 | ||
| 183 | length = high * 0x100000000 + low | |
| 184 | } | |
| 185 | ||
| 186 | let mask = null | |
| 187 | ||
| 188 | if (masked) { | |
| 189 | if (n < 4) throw errors.INCOMPLETE_FRAME() | |
| 190 | ||
| 191 | mask = b.subarray(i, i + 4) | |
| 192 | ||
| 193 | i += 4 | |
| 194 | n -= 4 | |
| 195 | } | |
| 196 | ||
| 197 | if (n < length) { | |
| 198 | throw errors.INCOMPLETE_FRAME('Incomplete frame', i - s + length) | |
| 199 | } | |
| 200 | ||
| 201 | const payload = b.subarray(i, i + length) | |
| 202 | ||
| 203 | i += length | |
| 204 | n -= length | |
| 205 | ||
| 206 | if (mask) { | |
| 207 | for (let i = 0; i < length; i++) { | |
| 208 | payload[i] ^= mask[i & 3] | |
| 209 | } | |
| 210 | } | |
| 211 | ||
| 212 | state.start = i | |
| 213 | ||
| 214 | return new Frame(opcode, payload, { fin, rsv1, rsv2, rsv3, mask }) | |
| 215 | } |
| … | … | @@ -0,0 +1,57 @@ |
| 1 | import { type HTTPSServerConnectionOptions } from 'bare-https' | |
| 2 | import { HTTPClientRequest, type HTTPServerConnectionOptions } from 'bare-http1' | |
| 3 | import { Socket as TCPSocket, type TCPSocketAddress } from 'bare-tcp' | |
| 4 | import { type DuplexEvents } from 'bare-stream' | |
| 5 | import EventEmitter from 'bare-events' | |
| 6 | import Buffer from 'bare-buffer' | |
| 7 | import WebSocket from './socket' | |
| 8 | import WebSocketError from './errors' | |
| 9 | ||
| 10 | interface WebSocketServerOptions extends HTTPServerConnectionOptions, HTTPSServerConnectionOptions { | |
| 11 | secure?: boolean | |
| 12 | } | |
| 13 | ||
| 14 | interface WebSocketServerEvents extends DuplexEvents { | |
| 15 | connection: [socket: WebSocket, req: HTTPClientRequest] | |
| 16 | listening: [] | |
| 17 | } | |
| 18 | ||
| 19 | interface WebSocketServer<M extends WebSocketServerEvents = WebSocketServerEvents> | |
| 20 | extends EventEmitter<M> { | |
| 21 | readonly listening: boolean | |
| 22 | ||
| 23 | address(): TCPSocketAddress | |
| 24 | close(cb?: (err?: Error | null) => void): this | |
| 25 | ref(): this | |
| 26 | unref(): this | |
| 27 | } | |
| 28 | ||
| 29 | declare class WebSocketServer { | |
| 30 | constructor(onconnection: (socket: WebSocket, req: HTTPClientRequest) => void) | |
| 31 | ||
| 32 | constructor( | |
| 33 | opts?: WebSocketServerOptions, | |
| 34 | onconnection?: (socket: WebSocket, req: HTTPClientRequest) => void | |
| 35 | ) | |
| 36 | } | |
| 37 | ||
| 38 | declare namespace WebSocketServer { | |
| 39 | export { type WebSocketServerOptions, type WebSocketServerEvents } | |
| 40 | ||
| 41 | export function handshake(req: HTTPClientRequest, cb: (err: WebSocketError | null) => void): void | |
| 42 | ||
| 43 | export function handshake( | |
| 44 | req: HTTPClientRequest, | |
| 45 | socket: TCPSocket, | |
| 46 | cb?: (err: WebSocketError | null) => void | |
| 47 | ): void | |
| 48 | ||
| 49 | export function handshake( | |
| 50 | req: HTTPClientRequest, | |
| 51 | socket?: TCPSocket, | |
| 52 | head?: Buffer, | |
| 53 | cb?: (err: WebSocketError | null) => void | |
| 54 | ): void | |
| 55 | } | |
| 56 | ||
| 57 | export = WebSocketServer |
| … | … | @@ -0,0 +1,131 @@ |
| 1 | const EventEmitter = require('bare-events') | |
| 2 | const http = require('bare-http1') | |
| 3 | const https = require('bare-https') | |
| 4 | const crypto = require('bare-crypto') | |
| 5 | const { GUID, EOL, EOF } = require('./constants') | |
| 6 | const errors = require('./errors') | |
| 7 | const WebSocket = require('./socket') | |
| 8 | ||
| 9 | const EMPTY = Buffer.alloc(0) | |
| 10 | ||
| 11 | const KEY = /^[+/0-9A-Za-z]{22}==$/ | |
| 12 | ||
| 13 | module.exports = exports = class WebSocketServer extends EventEmitter { | |
| 14 | constructor(opts = {}, onconnection) { | |
| 15 | if (typeof opts === 'function') { | |
| 16 | onconnection = opts | |
| 17 | opts = {} | |
| 18 | } | |
| 19 | ||
| 20 | super() | |
| 21 | ||
| 22 | const createServer = opts.secure ? https.createServer : http.createServer | |
| 23 | ||
| 24 | const { | |
| 25 | server = createServer(opts, this._onrequest.bind(this)).listen( | |
| 26 | opts, | |
| 27 | this._onlistening.bind(this) | |
| 28 | ) | |
| 29 | } = opts | |
| 30 | ||
| 31 | this._server = server | |
| 32 | ||
| 33 | this._server.on('upgrade', this._onupgrade.bind(this)) | |
| 34 | ||
| 35 | if (onconnection) this.on('connection', onconnection) | |
| 36 | } | |
| 37 | ||
| 38 | get listening() { | |
| 39 | return this._server.listening | |
| 40 | } | |
| 41 | ||
| 42 | address() { | |
| 43 | return this._server.address() | |
| 44 | } | |
| 45 | ||
| 46 | close(cb) { | |
| 47 | this._server.close(cb) | |
| 48 | ||
| 49 | return this | |
| 50 | } | |
| 51 | ||
| 52 | ref() { | |
| 53 | this._server.ref() | |
| 54 | ||
| 55 | return this | |
| 56 | } | |
| 57 | ||
| 58 | unref() { | |
| 59 | this._server.unref() | |
| 60 | ||
| 61 | return this | |
| 62 | } | |
| 63 | ||
| 64 | _onlistening() { | |
| 65 | this.emit('listening') | |
| 66 | } | |
| 67 | ||
| 68 | _onrequest(req, res) { | |
| 69 | const body = http.constants.status[426] | |
| 70 | ||
| 71 | res.writeHead(426, { | |
| 72 | 'Content-Type': 'text/plain', | |
| 73 | 'Content-Length': body.length | |
| 74 | }) | |
| 75 | ||
| 76 | res.end(body) | |
| 77 | } | |
| 78 | ||
| 79 | _onupgrade(req, socket, head) { | |
| 80 | exports.handshake(req, socket, head, (err) => { | |
| 81 | if (err) return socket.destroy(err) | |
| 82 | ||
| 83 | this.emit('connection', new WebSocket({ socket, isServer: true }), req) | |
| 84 | }) | |
| 85 | } | |
| 86 | } | |
| 87 | ||
| 88 | // https://datatracker.ietf.org/doc/html/rfc6455#section-4.2 | |
| 89 | exports.handshake = function handshake(req, socket = req.socket, head = EMPTY, cb) { | |
| 90 | if (typeof socket === 'function') { | |
| 91 | cb = socket | |
| 92 | socket = req.socket | |
| 93 | head = EMPTY | |
| 94 | } else if (typeof head === 'function') { | |
| 95 | cb = head | |
| 96 | head = EMPTY | |
| 97 | } | |
| 98 | ||
| 99 | if (req.headers.upgrade.toLowerCase() !== 'websocket') { | |
| 100 | return cb(errors.INVALID_UPGRADE_HEADER()) | |
| 101 | } | |
| 102 | ||
| 103 | const version = +req.headers['sec-websocket-version'] | |
| 104 | ||
| 105 | if (version !== 8 && version !== 13) { | |
| 106 | return cb(errors.INVALID_VERSION_HEADER()) | |
| 107 | } | |
| 108 | ||
| 109 | const key = req.headers['sec-websocket-key'] | |
| 110 | ||
| 111 | if (!key || !KEY.test(key)) { | |
| 112 | return cb(errors.INVALID_KEY_HEADER()) | |
| 113 | } | |
| 114 | ||
| 115 | const digest = crypto.createHash('sha1').update(key).update(GUID).digest('base64') | |
| 116 | ||
| 117 | socket.write( | |
| 118 | 'HTTP/1.1 101 Web Socket Protocol Handshake' + | |
| 119 | EOL + | |
| 120 | 'Upgrade: WebSocket' + | |
| 121 | EOL + | |
| 122 | 'Connection: Upgrade' + | |
| 123 | EOL + | |
| 124 | `Sec-WebSocket-Accept: ${digest}` + | |
| 125 | EOF | |
| 126 | ) | |
| 127 | ||
| 128 | if (head.byteLength) socket.unshift(head) | |
| 129 | ||
| 130 | cb(null) | |
| 131 | } |
| … | … | @@ -0,0 +1,40 @@ |
| 1 | import { HTTPClientRequest } from 'bare-http1' | |
| 2 | import { Socket as TCPSocket } from 'bare-tcp' | |
| 3 | import { Duplex, type DuplexEvents } from 'bare-stream' | |
| 4 | import URL from 'bare-url' | |
| 5 | import WebSocketError from './errors' | |
| 6 | ||
| 7 | interface WebSocketOptions { | |
| 8 | host?: string | |
| 9 | hostname?: string | |
| 10 | path?: string | |
| 11 | port?: string | number | |
| 12 | secure?: boolean | |
| 13 | socket?: TCPSocket | |
| 14 | } | |
| 15 | ||
| 16 | interface WebSocketEvents extends DuplexEvents { | |
| 17 | ping: [payload: unknown] | |
| 18 | pong: [payload: unknown] | |
| 19 | } | |
| 20 | ||
| 21 | interface WebSocket<M extends WebSocketEvents = WebSocketEvents> extends Duplex<M> { | |
| 22 | ping(data: unknown): void | |
| 23 | pong(data: unknown): void | |
| 24 | } | |
| 25 | ||
| 26 | declare class WebSocket { | |
| 27 | constructor(opts: WebSocketOptions) | |
| 28 | constructor(url: URL | string, opts?: WebSocketOptions) | |
| 29 | } | |
| 30 | ||
| 31 | declare namespace WebSocket { | |
| 32 | export { type WebSocketOptions, type WebSocketEvents } | |
| 33 | ||
| 34 | export function handshake( | |
| 35 | req: HTTPClientRequest, | |
| 36 | cb: (error: WebSocketError | null) => void | |
| 37 | ): void | |
| 38 | } | |
| 39 | ||
| 40 | export = WebSocket |
| … | … | @@ -0,0 +1,300 @@ |
| 1 | const { Duplex } = require('bare-stream') | |
| 2 | const http = require('bare-http1') | |
| 3 | const https = require('bare-https') | |
| 4 | const crypto = require('bare-crypto') | |
| 5 | const { GUID, opcode } = require('./constants') | |
| 6 | const errors = require('./errors') | |
| 7 | const Frame = require('./frame') | |
| 8 | ||
| 9 | const CLOSE = new Frame(opcode.CLOSE).toBuffer() | |
| 10 | ||
| 11 | module.exports = exports = class WebSocket extends Duplex { | |
| 12 | constructor(url, opts = {}) { | |
| 13 | if (typeof url === 'string') url = new URL(url) | |
| 14 | ||
| 15 | if (isURL(url)) { | |
| 16 | opts = opts ? { ...opts } : {} | |
| 17 | ||
| 18 | opts.host = url.hostname | |
| 19 | opts.path = url.pathname + url.search | |
| 20 | opts.port = url.port ? parseInt(url.port, 10) : defaultPort(url) | |
| 21 | opts.secure = url.protocol === 'https:' || url.protocol === 'wss:' | |
| 22 | } else { | |
| 23 | opts = url ? { ...url } : {} | |
| 24 | ||
| 25 | // For Node.js compatibility | |
| 26 | opts.host = opts.hostname || opts.host | |
| 27 | opts.port = typeof opts.port === 'string' ? parseInt(opts.port, 10) : opts.port | |
| 28 | } | |
| 29 | ||
| 30 | const { isServer = false, socket = null } = opts | |
| 31 | ||
| 32 | super({ eagerOpen: true }) | |
| 33 | ||
| 34 | this._socket = null | |
| 35 | this._isServer = isServer | |
| 36 | this._mask = isServer ? null : Buffer.allocUnsafe(4) | |
| 37 | this._fragments = [] | |
| 38 | ||
| 39 | this._pendingOpen = null | |
| 40 | this._pendingWrite = null | |
| 41 | ||
| 42 | this._buffer = [] | |
| 43 | this._buffered = 0 | |
| 44 | this._frame = -1 | |
| 45 | ||
| 46 | if (socket !== null) this._attach(socket) | |
| 47 | else this._connect(opts) | |
| 48 | } | |
| 49 | ||
| 50 | ping(data) { | |
| 51 | if (this._socket === null) throw errors.NOT_CONNECTED() | |
| 52 | ||
| 53 | if (typeof data === 'string') data = Buffer.from(data) | |
| 54 | ||
| 55 | this._socket.write(new Frame(opcode.PING, data, { mask: this._mask }).toBuffer()) | |
| 56 | } | |
| 57 | ||
| 58 | pong(data) { | |
| 59 | if (this._socket === null) throw errors.NOT_CONNECTED() | |
| 60 | ||
| 61 | if (typeof data === 'string') data = Buffer.from(data) | |
| 62 | ||
| 63 | this._socket.write(new Frame(opcode.PONG, data, { mask: this._mask }).toBuffer()) | |
| 64 | } | |
| 65 | ||
| 66 | _attach(socket) { | |
| 67 | this._socket = socket | |
| 68 | ||
| 69 | this._socket | |
| 70 | .on('error', this._onerror.bind(this)) | |
| 71 | .on('close', this._onclose.bind(this)) | |
| 72 | .on('data', this._ondata.bind(this)) | |
| 73 | .on('drain', this._ondrain.bind(this)) | |
| 74 | } | |
| 75 | ||
| 76 | _connect(opts) { | |
| 77 | const request = opts.secure ? https.request : http.request | |
| 78 | ||
| 79 | const req = request(opts) | |
| 80 | ||
| 81 | exports.handshake(req, (err) => { | |
| 82 | const cb = this._pendingOpen | |
| 83 | this._pendingOpen = null | |
| 84 | ||
| 85 | if (err) req.socket.destroy() | |
| 86 | else this._attach(req.socket) | |
| 87 | ||
| 88 | cb(err) | |
| 89 | }) | |
| 90 | } | |
| 91 | ||
| 92 | _onerror(err) { | |
| 93 | this.destroy(err) | |
| 94 | } | |
| 95 | ||
| 96 | _onclose() { | |
| 97 | this.destroy() | |
| 98 | } | |
| 99 | ||
| 100 | _ondata(data) { | |
| 101 | this._buffer.push(data) | |
| 102 | this._buffered += data.byteLength | |
| 103 | ||
| 104 | while (this._frame === -1 || this._frame <= this._buffered) { | |
| 105 | const buffer = this._buffer.length === 1 ? this._buffer[0] : Buffer.concat(this._buffer) | |
| 106 | ||
| 107 | this._buffer = [buffer] | |
| 108 | ||
| 109 | const state = { start: 0, end: buffer.length, buffer } | |
| 110 | ||
| 111 | try { | |
| 112 | this._onframe(Frame.decode(state)) | |
| 113 | } catch (err) { | |
| 114 | if (err.code === 'INCOMPLETE_FRAME') this._frame = err.status | |
| 115 | else this.destroy(err) | |
| 116 | return | |
| 117 | } | |
| 118 | ||
| 119 | this._buffered -= state.start | |
| 120 | this._buffer = this._buffered > 0 ? [buffer.subarray(state.start)] : [] | |
| 121 | this._frame = -1 | |
| 122 | } | |
| 123 | } | |
| 124 | ||
| 125 | _onframe(frame) { | |
| 126 | if (frame.rsv1) throw errors.UNEXPECTED_RSV1() | |
| 127 | ||
| 128 | if (frame.rsv2) throw errors.UNEXPECTED_RSV2() | |
| 129 | ||
| 130 | if (frame.rsv3) throw errors.UNEXPECTED_RSV3() | |
| 131 | ||
| 132 | if (frame.payload.length > 0 && !frame.mask === this._isServer) { | |
| 133 | throw this._isServer ? errors.EXPECTED_MASK() : errors.UNEXPECTED_MASK() | |
| 134 | } | |
| 135 | ||
| 136 | if (frame.fin === false) { | |
| 137 | if (this._fragments.push(frame) === 1) { | |
| 138 | // First frame | |
| 139 | if (frame.opcode === opcode.CONTINUATION) { | |
| 140 | throw errors.UNEXPECTED_CONTINUATION() | |
| 141 | } | |
| 142 | ||
| 143 | if (frame.opcode >= opcode.CLOSE) { | |
| 144 | throw errors.UNEXPECTED_CONTROL() | |
| 145 | } | |
| 146 | ||
| 147 | return | |
| 148 | } | |
| 149 | ||
| 150 | if (frame.opcode !== opcode.CONTINUATION) { | |
| 151 | throw errors.EXPECTED_CONTINUATION() | |
| 152 | } | |
| 153 | ||
| 154 | return | |
| 155 | } | |
| 156 | ||
| 157 | switch (frame.opcode) { | |
| 158 | case opcode.CLOSE: | |
| 159 | this.push(null) | |
| 160 | this.end() | |
| 161 | return | |
| 162 | ||
| 163 | case opcode.PING: | |
| 164 | this.pong(frame.payload) | |
| 165 | this.emit('ping', frame.payload) | |
| 166 | return | |
| 167 | ||
| 168 | case opcode.PONG: | |
| 169 | this.emit('pong', frame.payload) | |
| 170 | return | |
| 171 | ||
| 172 | case opcode.CONTINUATION: { | |
| 173 | if (this._fragments.length === 0) throw errors.UNEXPECTED_CONTINUATION() | |
| 174 | ||
| 175 | frame.opcode = this._fragments[0].opcode | |
| 176 | ||
| 177 | const payloads = this._fragments.map((frame) => frame.payload) | |
| 178 | ||
| 179 | payloads.push(frame.payload) | |
| 180 | ||
| 181 | frame.payload = Buffer.concat(payloads) | |
| 182 | ||
| 183 | this._fragments = [] | |
| 184 | ||
| 185 | break | |
| 186 | } | |
| 187 | ||
| 188 | default: | |
| 189 | if (this._fragments.length > 0) throw errors.EXPECTED_CONTINUATION() | |
| 190 | } | |
| 191 | ||
| 192 | switch (frame.opcode) { | |
| 193 | case opcode.TEXT: | |
| 194 | case opcode.BINARY: | |
| 195 | this.push(frame.payload) | |
| 196 | break | |
| 197 | ||
| 198 | default: | |
| 199 | throw errors.INVALID_OPCODE() | |
| 200 | } | |
| 201 | } | |
| 202 | ||
| 203 | _ondrain() { | |
| 204 | if (this._pendingWrite === null) return | |
| 205 | const cb = this._pendingWrite | |
| 206 | this._pendingWrite = null | |
| 207 | cb(null) | |
| 208 | } | |
| 209 | ||
| 210 | _open(cb) { | |
| 211 | if (this._socket === null) this._pendingOpen = cb | |
| 212 | else cb(null) | |
| 213 | } | |
| 214 | ||
| 215 | _write(data, encoding, cb) { | |
| 216 | if (encoding !== 'buffer' && encoding !== 'utf8') { | |
| 217 | return cb(errors.INVALID_ENCODING()) | |
| 218 | } | |
| 219 | ||
| 220 | const frame = new Frame(encoding === 'buffer' ? opcode.BINARY : opcode.TEXT, data, { | |
| 221 | mask: this._mask | |
| 222 | }) | |
| 223 | ||
| 224 | if (this._socket.write(frame.toBuffer())) cb(null) | |
| 225 | else this._pendingWrite = cb | |
| 226 | } | |
| 227 | ||
| 228 | _final(cb) { | |
| 229 | this._socket.end(CLOSE) | |
| 230 | cb(null) | |
| 231 | } | |
| 232 | ||
| 233 | _predestroy() { | |
| 234 | if (!this._socket) return | |
| 235 | this._socket.destroy() | |
| 236 | } | |
| 237 | } | |
| 238 | ||
| 239 | // https://datatracker.ietf.org/doc/html/rfc6455#section-4.1 | |
| 240 | exports.handshake = function handshake(req, cb) { | |
| 241 | const key = crypto.randomBytes(16).toString('base64') | |
| 242 | ||
| 243 | req.headers = { | |
| 244 | ...req.headers, | |
| 245 | Connection: 'Upgrade', | |
| 246 | Upgrade: 'websocket', | |
| 247 | 'Sec-WebSocket-Version': 13, | |
| 248 | 'Sec-WebSocket-Key': key | |
| 249 | } | |
| 250 | ||
| 251 | req.on('upgrade', (res, socket, head) => { | |
| 252 | if (res.headers.upgrade.toLowerCase() !== 'websocket') { | |
| 253 | return cb(errors.INVALID_UPGRADE_HEADER()) | |
| 254 | } | |
| 255 | ||
| 256 | const digest = crypto.createHash('sha1').update(key).update(GUID).digest('base64') | |
| 257 | ||
| 258 | if (res.headers['sec-websocket-accept'] !== digest) { | |
| 259 | return cb(errors.INVALID_ACCEPT_HEADER()) | |
| 260 | } | |
| 261 | ||
| 262 | if (head.byteLength) socket.unshift(head) | |
| 263 | ||
| 264 | cb(null) | |
| 265 | }) | |
| 266 | ||
| 267 | req.on('error', (err) => { | |
| 268 | cb(errors.NETWORK_ERROR('Network error', err)) | |
| 269 | }) | |
| 270 | ||
| 271 | req.end() | |
| 272 | } | |
| 273 | ||
| 274 | // https://url.spec.whatwg.org/#default-port | |
| 275 | function defaultPort(url) { | |
| 276 | switch (url.protocol) { | |
| 277 | case 'ftp:': | |
| 278 | return 21 | |
| 279 | case 'http:': | |
| 280 | case 'ws:': | |
| 281 | return 80 | |
| 282 | case 'https:': | |
| 283 | case 'wss:': | |
| 284 | return 443 | |
| 285 | } | |
| 286 | ||
| 287 | return null | |
| 288 | } | |
| 289 | ||
| 290 | // https://url.spec.whatwg.org/#api | |
| 291 | function isURL(url) { | |
| 292 | return ( | |
| 293 | url !== null && | |
| 294 | typeof url === 'object' && | |
| 295 | typeof url.protocol === 'string' && | |
| 296 | typeof url.hostname === 'string' && | |
| 297 | typeof url.pathname === 'string' && | |
| 298 | typeof url.search === 'string' | |
| 299 | ) | |
| 300 | } |
| … | … | @@ -0,0 +1,71 @@ |
| 1 | { | |
| 2 | "name": "bare-ws", | |
| 3 | "version": "2.1.0", | |
| 4 | "description": "WebSocket library for JavaScript", | |
| 5 | "exports": { | |
| 6 | "./package": "./package.json", | |
| 7 | ".": { | |
| 8 | "types": "./index.d.ts", | |
| 9 | "default": "./index.js" | |
| 10 | }, | |
| 11 | "./global": { | |
| 12 | "types": "./global.d.ts", | |
| 13 | "default": "./global.js" | |
| 14 | }, | |
| 15 | "./constants": { | |
| 16 | "types": "./lib/constants.d.ts", | |
| 17 | "default": "./lib/constants.js" | |
| 18 | }, | |
| 19 | "./errors": { | |
| 20 | "types": "./lib/errors.d.ts", | |
| 21 | "default": "./lib/errors.js" | |
| 22 | } | |
| 23 | }, | |
| 24 | "files": [ | |
| 25 | "index.js", | |
| 26 | "index.d.ts", | |
| 27 | "global.js", | |
| 28 | "global.d.ts", | |
| 29 | "lib" | |
| 30 | ], | |
| 31 | "scripts": { | |
| 32 | "test": "prettier . --check && bare test.js" | |
| 33 | }, | |
| 34 | "repository": { | |
| 35 | "type": "git", | |
| 36 | "url": "git+https://github.com/holepunchto/bare-ws.git" | |
| 37 | }, | |
| 38 | "author": "Holepunch", | |
| 39 | "license": "Apache-2.0", | |
| 40 | "bugs": { | |
| 41 | "url": "https://github.com/holepunchto/bare-ws/issues" | |
| 42 | }, | |
| 43 | "homepage": "https://github.com/holepunchto/bare-ws#readme", | |
| 44 | "dependencies": { | |
| 45 | "bare-crypto": "^1.2.0", | |
| 46 | "bare-events": "^2.3.1", | |
| 47 | "bare-http1": "^4.0.0", | |
| 48 | "bare-https": "^2.0.0", | |
| 49 | "bare-stream": "^2.1.2" | |
| 50 | }, | |
| 51 | "devDependencies": { | |
| 52 | "bare-buffer": "^3.0.2", | |
| 53 | "bare-fs": "^4.0.0", | |
| 54 | "bare-url": "^2.1.3", | |
| 55 | "brittle": "^3.3.0", | |
| 56 | "prettier": "^3.4.1", | |
| 57 | "prettier-config-holepunch": "^2.0.0" | |
| 58 | }, | |
| 59 | "peerDependencies": { | |
| 60 | "bare-buffer": "*", | |
| 61 | "bare-url": "*" | |
| 62 | }, | |
| 63 | "peerDependenciesMeta": { | |
| 64 | "bare-buffer": { | |
| 65 | "optional": true | |
| 66 | }, | |
| 67 | "bare-url": { | |
| 68 | "optional": true | |
| 69 | } | |
| 70 | } | |
| 71 | } |
| … | … | @@ -0,0 +1,15 @@ |
| 1 | ISC License | |
| 2 | ||
| 3 | Copyright (c) 2022 Contributors | |
| 4 | ||
| 5 | Permission to use, copy, modify, and/or distribute this software for any | |
| 6 | purpose with or without fee is hereby granted, provided that the above | |
| 7 | copyright notice and this permission notice appear in all copies. | |
| 8 | ||
| 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH | |
| 10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY | |
| 11 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, | |
| 12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM | |
| 13 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR | |
| 14 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR | |
| 15 | PERFORMANCE OF THIS SOFTWARE. |
| … | … | @@ -0,0 +1,80 @@ |
| 1 | # Bits to Bytes | |
| 2 | ||
| 3 | Functions for doing bit manipulation of typed arrays. | |
| 4 | ||
| 5 | ```npm | |
| 6 | npm i bits-to-bytes | |
| 7 | ``` | |
| 8 | ||
| 9 | ## Usage | |
| 10 | ||
| 11 | ```js | |
| 12 | const bits = require('bits-to-bytes') | |
| 13 | ||
| 14 | const buffer = Uint8Array.from([0b11001000]) | |
| 15 | ||
| 16 | bits.get(buffer, 3) | |
| 17 | // true | |
| 18 | ``` | |
| 19 | ||
| 20 | `buffer` may be any typed array with a `number` element type, including `Uint8Array`, `Uint16Array`, and `Uint32Array`. | |
| 21 | ||
| 22 | ## API | |
| 23 | ||
| 24 | #### `const n = bits.byteLength(size)` | |
| 25 | ||
| 26 | Get the number of bytes required to contain `size` bits. | |
| 27 | ||
| 28 | #### `const b = bits.get(buffer, bit)` | |
| 29 | ||
| 30 | Get the given bit, which will either be `true` (set) or `false` (unset). | |
| 31 | ||
| 32 | #### `const changed = bits.set(buffer, bit[, value])` | |
| 33 | ||
| 34 | Set the given bit to `value`, which defaults to `true`. Returns `true` if the bit changed, otherwise `false`. | |
| 35 | ||
| 36 | #### `const changed = bits.setRange(buffer, start, end[, value])` | |
| 37 | ||
| 38 | Set the given bit range to `value`, which defaults to `true`. Returns `true` if any of the bits changed, otherwise `false`. | |
| 39 | ||
| 40 | If you don't need the additional information about whether any of the bits changed, consider `bits.fill()` as a more performant alternative. | |
| 41 | ||
| 42 | #### `buffer = bits.fill(buffer, value[, start[, end]])` | |
| 43 | ||
| 44 | Fill the given bit range with `value`. `start` defaults to `0` and `end` defaults to the bit length of the array. Returns the modified array. | |
| 45 | ||
| 46 | #### `const b = bits.toggle(buffer, bit)` | |
| 47 | ||
| 48 | Toggle the given bit, returning its new value. | |
| 49 | ||
| 50 | #### `const changed = bits.remove(buffer, bit)` | |
| 51 | ||
| 52 | Unset the given bit. Returns `true` if the bit was set, otherwise `false`. | |
| 53 | ||
| 54 | #### `const changed = bits.removeRange(buffer, start, end)` | |
| 55 | ||
| 56 | Unset the given bit range. Returns `true` if any of the bits were set, otherwise `false`. | |
| 57 | ||
| 58 | #### `const index = bits.indexOf(buffer, value[, position])` | |
| 59 | ||
| 60 | Return the index of the first occurrence of `value`, or `-1` if not found. If `position` is given, return the first index that is greater than or equal to `position`. | |
| 61 | ||
| 62 | #### `const index = bits.lastIndexOf(buffer, value[, position])` | |
| 63 | ||
| 64 | Return the index of the last occurrence of `value`, or `-1` if not found. If `position` is given, return the last index that is less than or equal to `position`. | |
| 65 | ||
| 66 | #### `const buffer = bits.of(...values)` | |
| 67 | ||
| 68 | Create a buffer containing the given bits. | |
| 69 | ||
| 70 | #### `const buffer = bits.from(values)` | |
| 71 | ||
| 72 | Create a buffer containing the given bits. | |
| 73 | ||
| 74 | #### `const iterator = bits.iterator(buffer)` | |
| 75 | ||
| 76 | Create an iterator over bits. | |
| 77 | ||
| 78 | ## License | |
| 79 | ||
| 80 | ISC |
| … | … | @@ -0,0 +1,161 @@ |
| 1 | const b4a = require('b4a') | |
| 2 | ||
| 3 | function byteLength (size) { | |
| 4 | return Math.ceil(size / 8) | |
| 5 | } | |
| 6 | ||
| 7 | function get (buffer, bit) { | |
| 8 | const n = buffer.BYTES_PER_ELEMENT * 8 | |
| 9 | ||
| 10 | const offset = bit & (n - 1) | |
| 11 | const i = (bit - offset) / n | |
| 12 | ||
| 13 | return (buffer[i] & (1 << offset)) !== 0 | |
| 14 | } | |
| 15 | ||
| 16 | function set (buffer, bit, value = true) { | |
| 17 | const n = buffer.BYTES_PER_ELEMENT * 8 | |
| 18 | ||
| 19 | const offset = bit & (n - 1) | |
| 20 | const i = (bit - offset) / n | |
| 21 | const mask = 1 << offset | |
| 22 | ||
| 23 | if (value) { | |
| 24 | if ((buffer[i] & mask) !== 0) return false | |
| 25 | } else { | |
| 26 | if ((buffer[i] & mask) === 0) return false | |
| 27 | } | |
| 28 | ||
| 29 | buffer[i] ^= mask | |
| 30 | return true | |
| 31 | } | |
| 32 | ||
| 33 | function setRange (buffer, start, end, value = true) { | |
| 34 | const n = buffer.BYTES_PER_ELEMENT * 8 | |
| 35 | ||
| 36 | let remaining = end - start | |
| 37 | let offset = start & (n - 1) | |
| 38 | let i = (start - offset) / n | |
| 39 | ||
| 40 | let changed = false | |
| 41 | ||
| 42 | while (remaining > 0) { | |
| 43 | const mask = (2 ** Math.min(remaining, n - offset) - 1) << offset | |
| 44 | ||
| 45 | if (value) { | |
| 46 | if ((buffer[i] & mask) !== mask) { | |
| 47 | buffer[i] |= mask | |
| 48 | changed = true | |
| 49 | } | |
| 50 | } else { | |
| 51 | if ((buffer[i] & mask) !== 0) { | |
| 52 | buffer[i] &= ~mask | |
| 53 | changed = true | |
| 54 | } | |
| 55 | } | |
| 56 | ||
| 57 | remaining -= n - offset | |
| 58 | offset = 0 | |
| 59 | i++ | |
| 60 | } | |
| 61 | ||
| 62 | return changed | |
| 63 | } | |
| 64 | ||
| 65 | function fill (buffer, value, start = 0, end = buffer.byteLength * 8) { | |
| 66 | const n = buffer.BYTES_PER_ELEMENT * 8 | |
| 67 | let i, j | |
| 68 | ||
| 69 | { | |
| 70 | const offset = start & (n - 1) | |
| 71 | i = (start - offset) / n | |
| 72 | ||
| 73 | if (offset !== 0) { | |
| 74 | const mask = (2 ** Math.min(n - offset, end - start) - 1) << offset | |
| 75 | ||
| 76 | if (value) buffer[i] |= mask | |
| 77 | else buffer[i] &= ~mask | |
| 78 | ||
| 79 | i++ | |
| 80 | } | |
| 81 | } | |
| 82 | ||
| 83 | { | |
| 84 | const offset = end & (n - 1) | |
| 85 | j = (end - offset) / n | |
| 86 | ||
| 87 | if (offset !== 0 && j >= i) { | |
| 88 | const mask = (2 ** offset) - 1 | |
| 89 | ||
| 90 | if (value) buffer[j] |= mask | |
| 91 | else buffer[j] &= ~mask | |
| 92 | } | |
| 93 | } | |
| 94 | ||
| 95 | return buffer.fill(value ? (2 ** n) - 1 : 0, i, j) | |
| 96 | } | |
| 97 | ||
| 98 | function toggle (buffer, bit) { | |
| 99 | const n = buffer.BYTES_PER_ELEMENT * 8 | |
| 100 | ||
| 101 | const offset = bit & (n - 1) | |
| 102 | const i = (bit - offset) / n | |
| 103 | const mask = 1 << offset | |
| 104 | ||
| 105 | buffer[i] ^= mask | |
| 106 | return (buffer[i] & mask) !== 0 | |
| 107 | } | |
| 108 | ||
| 109 | function remove (buffer, bit) { | |
| 110 | return set(buffer, bit, false) | |
| 111 | } | |
| 112 | ||
| 113 | function removeRange (buffer, start, end) { | |
| 114 | return setRange(buffer, start, end, false) | |
| 115 | } | |
| 116 | ||
| 117 | function indexOf (buffer, value, position = 0) { | |
| 118 | for (let i = position, n = buffer.byteLength * 8; i < n; i++) { | |
| 119 | if (get(buffer, i) === value) return i | |
| 120 | } | |
| 121 | ||
| 122 | return -1 | |
| 123 | } | |
| 124 | ||
| 125 | function lastIndexOf (buffer, value, position = buffer.byteLength * 8 - 1) { | |
| 126 | for (let i = position; i >= 0; i--) { | |
| 127 | if (get(buffer, i) === value) return i | |
| 128 | } | |
| 129 | ||
| 130 | return -1 | |
| 131 | } | |
| 132 | ||
| 133 | function of (...bits) { | |
| 134 | return from(bits) | |
| 135 | } | |
| 136 | ||
| 137 | function from (bits) { | |
| 138 | const buffer = b4a.alloc(byteLength(bits.length)) | |
| 139 | for (let i = 0; i < bits.length; i++) set(buffer, i, bits[i]) | |
| 140 | return buffer | |
| 141 | } | |
| 142 | ||
| 143 | function * iterator (buffer) { | |
| 144 | for (let i = 0, n = buffer.byteLength * 8; i < n; i++) yield get(buffer, i) | |
| 145 | } | |
| 146 | ||
| 147 | module.exports = { | |
| 148 | byteLength, | |
| 149 | get, | |
| 150 | set, | |
| 151 | setRange, | |
| 152 | fill, | |
| 153 | toggle, | |
| 154 | remove, | |
| 155 | removeRange, | |
| 156 | indexOf, | |
| 157 | lastIndexOf, | |
| 158 | of, | |
| 159 | from, | |
| 160 | iterator | |
| 161 | } |
| … | … | @@ -0,0 +1,29 @@ |
| 1 | { | |
| 2 | "name": "bits-to-bytes", | |
| 3 | "version": "1.3.0", | |
| 4 | "description": "Functions for doing bit manipulation of typed arrays", | |
| 5 | "main": "index.js", | |
| 6 | "files": [ | |
| 7 | "index.js" | |
| 8 | ], | |
| 9 | "scripts": { | |
| 10 | "test": "standard && brittle test.mjs" | |
| 11 | }, | |
| 12 | "repository": { | |
| 13 | "type": "git", | |
| 14 | "url": "git+https://github.com/holepunchto/bits-to-bytes.git" | |
| 15 | }, | |
| 16 | "author": "Kasper Isager Dalsgarð <kasper@funktionel.co>", | |
| 17 | "license": "ISC", | |
| 18 | "bugs": { | |
| 19 | "url": "https://github.com/holepunchto/bits-to-bytes/issues" | |
| 20 | }, | |
| 21 | "homepage": "https://github.com/holepunchto/bits-to-bytes#readme", | |
| 22 | "dependencies": { | |
| 23 | "b4a": "^1.5.0" | |
| 24 | }, | |
| 25 | "devDependencies": { | |
| 26 | "brittle": "^2.3.1", | |
| 27 | "standard": "^17.0.0" | |
| 28 | } | |
| 29 | } |
| … | … | @@ -0,0 +1,201 @@ |
| 1 | Apache License | |
| 2 | Version 2.0, January 2004 | |
| 3 | http://www.apache.org/licenses/ | |
| 4 | ||
| 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | |
| 6 | ||
| 7 | 1. Definitions. | |
| 8 | ||
| 9 | "License" shall mean the terms and conditions for use, reproduction, | |
| 10 | and distribution as defined by Sections 1 through 9 of this document. | |
| 11 | ||
| 12 | "Licensor" shall mean the copyright owner or entity authorized by | |
| 13 | the copyright owner that is granting the License. | |
| 14 | ||
| 15 | "Legal Entity" shall mean the union of the acting entity and all | |
| 16 | other entities that control, are controlled by, or are under common | |
| 17 | control with that entity. For the purposes of this definition, | |
| 18 | "control" means (i) the power, direct or indirect, to cause the | |
| 19 | direction or management of such entity, whether by contract or | |
| 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the | |
| 21 | outstanding shares, or (iii) beneficial ownership of such entity. | |
| 22 | ||
| 23 | "You" (or "Your") shall mean an individual or Legal Entity | |
| 24 | exercising permissions granted by this License. | |
| 25 | ||
| 26 | "Source" form shall mean the preferred form for making modifications, | |
| 27 | including but not limited to software source code, documentation | |
| 28 | source, and configuration files. | |
| 29 | ||
| 30 | "Object" form shall mean any form resulting from mechanical | |
| 31 | transformation or translation of a Source form, including but | |
| 32 | not limited to compiled object code, generated documentation, | |
| 33 | and conversions to other media types. | |
| 34 | ||
| 35 | "Work" shall mean the work of authorship, whether in Source or | |
| 36 | Object form, made available under the License, as indicated by a | |
| 37 | copyright notice that is included in or attached to the work | |
| 38 | (an example is provided in the Appendix below). | |
| 39 | ||
| 40 | "Derivative Works" shall mean any work, whether in Source or Object | |
| 41 | form, that is based on (or derived from) the Work and for which the | |
| 42 | editorial revisions, annotations, elaborations, or other modifications | |
| 43 | represent, as a whole, an original work of authorship. For the purposes | |
| 44 | of this License, Derivative Works shall not include works that remain | |
| 45 | separable from, or merely link (or bind by name) to the interfaces of, | |
| 46 | the Work and Derivative Works thereof. | |
| 47 | ||
| 48 | "Contribution" shall mean any work of authorship, including | |
| 49 | the original version of the Work and any modifications or additions | |
| 50 | to that Work or Derivative Works thereof, that is intentionally | |
| 51 | submitted to Licensor for inclusion in the Work by the copyright owner | |
| 52 | or by an individual or Legal Entity authorized to submit on behalf of | |
| 53 | the copyright owner. For the purposes of this definition, "submitted" | |
| 54 | means any form of electronic, verbal, or written communication sent | |
| 55 | to the Licensor or its representatives, including but not limited to | |
| 56 | communication on electronic mailing lists, source code control systems, | |
| 57 | and issue tracking systems that are managed by, or on behalf of, the | |
| 58 | Licensor for the purpose of discussing and improving the Work, but | |
| 59 | excluding communication that is conspicuously marked or otherwise | |
| 60 | designated in writing by the copyright owner as "Not a Contribution." | |
| 61 | ||
| 62 | "Contributor" shall mean Licensor and any individual or Legal Entity | |
| 63 | on behalf of whom a Contribution has been received by Licensor and | |
| 64 | subsequently incorporated within the Work. | |
| 65 | ||
| 66 | 2. Grant of Copyright License. Subject to the terms and conditions of | |
| 67 | this License, each Contributor hereby grants to You a perpetual, | |
| 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 69 | copyright license to reproduce, prepare Derivative Works of, | |
| 70 | publicly display, publicly perform, sublicense, and distribute the | |
| 71 | Work and such Derivative Works in Source or Object form. | |
| 72 | ||
| 73 | 3. Grant of Patent License. Subject to the terms and conditions of | |
| 74 | this License, each Contributor hereby grants to You a perpetual, | |
| 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 76 | (except as stated in this section) patent license to make, have made, | |
| 77 | use, offer to sell, sell, import, and otherwise transfer the Work, | |
| 78 | where such license applies only to those patent claims licensable | |
| 79 | by such Contributor that are necessarily infringed by their | |
| 80 | Contribution(s) alone or by combination of their Contribution(s) | |
| 81 | with the Work to which such Contribution(s) was submitted. If You | |
| 82 | institute patent litigation against any entity (including a | |
| 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work | |
| 84 | or a Contribution incorporated within the Work constitutes direct | |
| 85 | or contributory patent infringement, then any patent licenses | |
| 86 | granted to You under this License for that Work shall terminate | |
| 87 | as of the date such litigation is filed. | |
| 88 | ||
| 89 | 4. Redistribution. You may reproduce and distribute copies of the | |
| 90 | Work or Derivative Works thereof in any medium, with or without | |
| 91 | modifications, and in Source or Object form, provided that You | |
| 92 | meet the following conditions: | |
| 93 | ||
| 94 | (a) You must give any other recipients of the Work or | |
| 95 | Derivative Works a copy of this License; and | |
| 96 | ||
| 97 | (b) You must cause any modified files to carry prominent notices | |
| 98 | stating that You changed the files; and | |
| 99 | ||
| 100 | (c) You must retain, in the Source form of any Derivative Works | |
| 101 | that You distribute, all copyright, patent, trademark, and | |
| 102 | attribution notices from the Source form of the Work, | |
| 103 | excluding those notices that do not pertain to any part of | |
| 104 | the Derivative Works; and | |
| 105 | ||
| 106 | (d) If the Work includes a "NOTICE" text file as part of its | |
| 107 | distribution, then any Derivative Works that You distribute must | |
| 108 | include a readable copy of the attribution notices contained | |
| 109 | within such NOTICE file, excluding those notices that do not | |
| 110 | pertain to any part of the Derivative Works, in at least one | |
| 111 | of the following places: within a NOTICE text file distributed | |
| 112 | as part of the Derivative Works; within the Source form or | |
| 113 | documentation, if provided along with the Derivative Works; or, | |
| 114 | within a display generated by the Derivative Works, if and | |
| 115 | wherever such third-party notices normally appear. The contents | |
| 116 | of the NOTICE file are for informational purposes only and | |
| 117 | do not modify the License. You may add Your own attribution | |
| 118 | notices within Derivative Works that You distribute, alongside | |
| 119 | or as an addendum to the NOTICE text from the Work, provided | |
| 120 | that such additional attribution notices cannot be construed | |
| 121 | as modifying the License. | |
| 122 | ||
| 123 | You may add Your own copyright statement to Your modifications and | |
| 124 | may provide additional or different license terms and conditions | |
| 125 | for use, reproduction, or distribution of Your modifications, or | |
| 126 | for any such Derivative Works as a whole, provided Your use, | |
| 127 | reproduction, and distribution of the Work otherwise complies with | |
| 128 | the conditions stated in this License. | |
| 129 | ||
| 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, | |
| 131 | any Contribution intentionally submitted for inclusion in the Work | |
| 132 | by You to the Licensor shall be under the terms and conditions of | |
| 133 | this License, without any additional terms or conditions. | |
| 134 | Notwithstanding the above, nothing herein shall supersede or modify | |
| 135 | the terms of any separate license agreement you may have executed | |
| 136 | with Licensor regarding such Contributions. | |
| 137 | ||
| 138 | 6. Trademarks. This License does not grant permission to use the trade | |
| 139 | names, trademarks, service marks, or product names of the Licensor, | |
| 140 | except as required for reasonable and customary use in describing the | |
| 141 | origin of the Work and reproducing the content of the NOTICE file. | |
| 142 | ||
| 143 | 7. Disclaimer of Warranty. Unless required by applicable law or | |
| 144 | agreed to in writing, Licensor provides the Work (and each | |
| 145 | Contributor provides its Contributions) on an "AS IS" BASIS, | |
| 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |
| 147 | implied, including, without limitation, any warranties or conditions | |
| 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | |
| 149 | PARTICULAR PURPOSE. You are solely responsible for determining the | |
| 150 | appropriateness of using or redistributing the Work and assume any | |
| 151 | risks associated with Your exercise of permissions under this License. | |
| 152 | ||
| 153 | 8. Limitation of Liability. In no event and under no legal theory, | |
| 154 | whether in tort (including negligence), contract, or otherwise, | |
| 155 | unless required by applicable law (such as deliberate and grossly | |
| 156 | negligent acts) or agreed to in writing, shall any Contributor be | |
| 157 | liable to You for damages, including any direct, indirect, special, | |
| 158 | incidental, or consequential damages of any character arising as a | |
| 159 | result of this License or out of the use or inability to use the | |
| 160 | Work (including but not limited to damages for loss of goodwill, | |
| 161 | work stoppage, computer failure or malfunction, or any and all | |
| 162 | other commercial damages or losses), even if such Contributor | |
| 163 | has been advised of the possibility of such damages. | |
| 164 | ||
| 165 | 9. Accepting Warranty or Additional Liability. While redistributing | |
| 166 | the Work or Derivative Works thereof, You may choose to offer, | |
| 167 | and charge a fee for, acceptance of support, warranty, indemnity, | |
| 168 | or other liability obligations and/or rights consistent with this | |
| 169 | License. However, in accepting such obligations, You may act only | |
| 170 | on Your own behalf and on Your sole responsibility, not on behalf | |
| 171 | of any other Contributor, and only if You agree to indemnify, | |
| 172 | defend, and hold each Contributor harmless for any liability | |
| 173 | incurred by, or claims asserted against, such Contributor by reason | |
| 174 | of your accepting any such warranty or additional liability. | |
| 175 | ||
| 176 | END OF TERMS AND CONDITIONS | |
| 177 | ||
| 178 | APPENDIX: How to apply the Apache License to your work. | |
| 179 | ||
| 180 | To apply the Apache License to your work, attach the following | |
| 181 | boilerplate notice, with the fields enclosed by brackets "[]" | |
| 182 | replaced with your own identifying information. (Don't include | |
| 183 | the brackets!) The text should be enclosed in the appropriate | |
| 184 | comment syntax for the file format. We also recommend that a | |
| 185 | file or class name and description of purpose be included on the | |
| 186 | same "printed page" as the copyright notice for easier | |
| 187 | identification within third-party archives. | |
| 188 | ||
| 189 | Copyright [yyyy] [name of copyright owner] | |
| 190 | ||
| 191 | Licensed under the Apache License, Version 2.0 (the "License"); | |
| 192 | you may not use this file except in compliance with the License. | |
| 193 | You may obtain a copy of the License at | |
| 194 | ||
| 195 | http://www.apache.org/licenses/LICENSE-2.0 | |
| 196 | ||
| 197 | Unless required by applicable law or agreed to in writing, software | |
| 198 | distributed under the License is distributed on an "AS IS" BASIS, | |
| 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 200 | See the License for the specific language governing permissions and | |
| 201 | limitations under the License. |
| … | … | @@ -0,0 +1,61 @@ |
| 1 | # blind-relay | |
| 2 | ||
| 3 | Blind relay for [UDX](https://github.com/holepunchto/udx-native) over [Protomux](https://github.com/mafintosh/protomux) channels. By acting as a blind relay, a host may accept pairing requests from other hosts and relay UDX stream messages between them, similar to Traversal Using Relays around NAT (TURN). | |
| 4 | ||
| 5 | ```sh | |
| 6 | npm i blind-relay | |
| 7 | ``` | |
| 8 | ||
| 9 | ## Protocol | |
| 10 | ||
| 11 | ```mermaid | |
| 12 | sequenceDiagram | |
| 13 | actor a as peer a | |
| 14 | actor r as relay | |
| 15 | actor b as peer b | |
| 16 | ||
| 17 | par | |
| 18 | a ->> r: pair { isInitiator: true, token, id, seq } | |
| 19 | and | |
| 20 | b ->> r: pair { isInitiator: false, token, id, seq } | |
| 21 | end | |
| 22 | ||
| 23 | note over r: The relay pairs peers a and b based on the token, allocates a new stream for both peers, and sends back the stream info | |
| 24 | ||
| 25 | par | |
| 26 | r -->> a: pair { isInitiator: true, token, id, seq } | |
| 27 | and | |
| 28 | r -->> b: pair { isInitiator: false, token, id, seq } | |
| 29 | end | |
| 30 | ||
| 31 | par | |
| 32 | a ->> r: unpair { token } | |
| 33 | ||
| 34 | note left of r: The relay deallocates the stream previously allocated to peer a | |
| 35 | and | |
| 36 | b ->> r: unpair { token } | |
| 37 | ||
| 38 | note right of r: The relay deallocates the stream previously allocated to peer b | |
| 39 | end | |
| 40 | ``` | |
| 41 | ||
| 42 | ### Messages | |
| 43 | ||
| 44 | All types are specified as their corresponding [compact-encoding](https://github.com/compact-encoding) codec. | |
| 45 | ||
| 46 | #### `pair` (`0`) | |
| 47 | ||
| 48 | 1. `bitfield(1)` Flags. | |
| 49 | 1. `isInitiator` | |
| 50 | 2. `fixed32` Preexchanged token. | |
| 51 | 3. `uint` Stream ID of the sender. | |
| 52 | 4. `uint` Initial stream sequence no. of the sender. | |
| 53 | ||
| 54 | #### `unpair` (`1`) | |
| 55 | ||
| 56 | 1. `bitfield(0)` Flags, reserved. | |
| 57 | 2. `fixed32` Preexchanged token. | |
| 58 | ||
| 59 | ## License | |
| 60 | ||
| 61 | Apache-2.0 |
| … | … | @@ -0,0 +1,523 @@ |
| 1 | const EventEmitter = require('events') | |
| 2 | const Protomux = require('protomux') | |
| 3 | const { Readable } = require('streamx') | |
| 4 | const sodium = require('sodium-universal') | |
| 5 | const b4a = require('b4a') | |
| 6 | const c = require('compact-encoding') | |
| 7 | const bitfield = require('compact-encoding-bitfield') | |
| 8 | const bits = require('bits-to-bytes') | |
| 9 | const errors = require('./lib/errors') | |
| 10 | ||
| 11 | exports.Server = class BlindRelayServer extends EventEmitter { | |
| 12 | constructor (opts = {}) { | |
| 13 | super() | |
| 14 | ||
| 15 | const { | |
| 16 | createStream | |
| 17 | } = opts | |
| 18 | ||
| 19 | this._createStream = createStream | |
| 20 | this._pairing = new Map() | |
| 21 | this._sessions = new Set() | |
| 22 | } | |
| 23 | ||
| 24 | get sessions () { | |
| 25 | return this._sessions[Symbol.iterator]() | |
| 26 | } | |
| 27 | ||
| 28 | accept (stream, opts) { | |
| 29 | const session = new BlindRelaySession(this, stream, opts) | |
| 30 | ||
| 31 | this._sessions.add(session) | |
| 32 | ||
| 33 | return session | |
| 34 | } | |
| 35 | ||
| 36 | async close () { | |
| 37 | const ending = [] | |
| 38 | ||
| 39 | for (const session of this._sessions) { | |
| 40 | ending.push(session.end()) | |
| 41 | } | |
| 42 | ||
| 43 | await Promise.all(ending) | |
| 44 | ||
| 45 | this._pairing.clear() | |
| 46 | } | |
| 47 | } | |
| 48 | ||
| 49 | class BlindRelaySession extends EventEmitter { | |
| 50 | constructor (server, stream, opts = {}) { | |
| 51 | super() | |
| 52 | ||
| 53 | const { | |
| 54 | id, | |
| 55 | handshake, | |
| 56 | handshakeEncoding | |
| 57 | } = opts | |
| 58 | ||
| 59 | this._server = server | |
| 60 | this._mux = Protomux.from(stream) | |
| 61 | ||
| 62 | this._channel = this._mux.createChannel({ | |
| 63 | protocol: 'blind-relay', | |
| 64 | id, | |
| 65 | handshake: handshake ? handshakeEncoding || c.raw : null, | |
| 66 | onopen: this._onopen.bind(this), | |
| 67 | onclose: this._onclose.bind(this), | |
| 68 | ondestroy: this._ondestroy.bind(this) | |
| 69 | }) | |
| 70 | ||
| 71 | this._pair = this._channel.addMessage({ | |
| 72 | encoding: m.pair, | |
| 73 | onmessage: this._onpair.bind(this) | |
| 74 | }) | |
| 75 | ||
| 76 | this._unpair = this._channel.addMessage({ | |
| 77 | encoding: m.unpair, | |
| 78 | onmessage: this._onunpair.bind(this) | |
| 79 | }) | |
| 80 | ||
| 81 | this._ending = null | |
| 82 | this._destroyed = false | |
| 83 | this._error = null | |
| 84 | this._pairing = new Set() | |
| 85 | this._streams = new Map() | |
| 86 | ||
| 87 | this._onerror = (err) => this.emit('error', err) | |
| 88 | ||
| 89 | this._channel.open(handshake) | |
| 90 | } | |
| 91 | ||
| 92 | get closed () { | |
| 93 | return this._channel.closed | |
| 94 | } | |
| 95 | ||
| 96 | get mux () { | |
| 97 | return this._mux | |
| 98 | } | |
| 99 | ||
| 100 | get stream () { | |
| 101 | return this._mux.stream | |
| 102 | } | |
| 103 | ||
| 104 | _onopen () { | |
| 105 | this.emit('open') | |
| 106 | } | |
| 107 | ||
| 108 | _onclose () { | |
| 109 | this._ending = Promise.resolve() | |
| 110 | ||
| 111 | const err = this._error || errors.CHANNEL_CLOSED() | |
| 112 | ||
| 113 | for (const token of this._pairing) { | |
| 114 | this._server._pairing.delete(token.toString('hex')) | |
| 115 | } | |
| 116 | ||
| 117 | for (const stream of this._streams.values()) { | |
| 118 | stream | |
| 119 | .off('error', this._onerror) | |
| 120 | .on('error', noop) | |
| 121 | .destroy(err) | |
| 122 | } | |
| 123 | ||
| 124 | this._pairing.clear() | |
| 125 | this._streams.clear() | |
| 126 | ||
| 127 | this._server._sessions.delete(this) | |
| 128 | ||
| 129 | this.emit('close') | |
| 130 | } | |
| 131 | ||
| 132 | _ondestroy () { | |
| 133 | this._destroyed = true | |
| 134 | this.emit('destroy') | |
| 135 | } | |
| 136 | ||
| 137 | _onpair ({ isInitiator, token, id: remoteId }) { | |
| 138 | const keyString = token.toString('hex') | |
| 139 | ||
| 140 | let pair = this._server._pairing.get(keyString) | |
| 141 | ||
| 142 | if (pair === undefined) { | |
| 143 | pair = new BlindRelayPair(token) | |
| 144 | this._server._pairing.set(keyString, pair) | |
| 145 | } else if (pair.links[+isInitiator]) return | |
| 146 | ||
| 147 | this._pairing.add(keyString) | |
| 148 | ||
| 149 | pair.links[+isInitiator] = new BlindRelayLink(this, isInitiator, remoteId) | |
| 150 | ||
| 151 | if (!pair.paired) return | |
| 152 | ||
| 153 | this._server._pairing.delete(keyString) | |
| 154 | ||
| 155 | // 1st pass: Create the raw streams needed for each end of the link. | |
| 156 | for (const link of pair.links) { | |
| 157 | link.createStream() | |
| 158 | } | |
| 159 | ||
| 160 | // 2nd pass: Connect the raw streams and set up handlers. | |
| 161 | for (const { isInitiator, session, stream } of pair.links) { | |
| 162 | const remote = pair.remote(isInitiator) | |
| 163 | ||
| 164 | stream | |
| 165 | .on('error', session._onerror) | |
| 166 | .on('close', () => session._streams.delete(keyString)) | |
| 167 | .relayTo(remote.stream) | |
| 168 | ||
| 169 | session._pairing.delete(keyString) | |
| 170 | session._streams.set(keyString, stream) | |
| 171 | } | |
| 172 | ||
| 173 | // 3rd pass: Let either end of the link know the streams were set up. | |
| 174 | for (const { isInitiator, session, remoteId, stream } of pair.links) { | |
| 175 | session._pair.send({ | |
| 176 | isInitiator, | |
| 177 | token, | |
| 178 | id: stream.id, | |
| 179 | seq: 0 | |
| 180 | }) | |
| 181 | ||
| 182 | session._endMaybe() | |
| 183 | ||
| 184 | session.emit('pair', isInitiator, token, stream, remoteId) | |
| 185 | } | |
| 186 | } | |
| 187 | ||
| 188 | _onunpair ({ token }) { | |
| 189 | const keyString = token.toString('hex') | |
| 190 | ||
| 191 | const pair = this._server._pairing.get(keyString) | |
| 192 | ||
| 193 | if (pair) { | |
| 194 | for (const link of pair.links) { | |
| 195 | if (link) link.session._pairing.delete(keyString) | |
| 196 | } | |
| 197 | ||
| 198 | return this._server._pairing.delete(keyString) | |
| 199 | } | |
| 200 | ||
| 201 | const stream = this._streams.get(keyString) | |
| 202 | ||
| 203 | if (stream) { | |
| 204 | stream | |
| 205 | .off('error', this._onerror) | |
| 206 | .on('error', noop) | |
| 207 | .destroy(errors.PAIRING_CANCELLED()) | |
| 208 | ||
| 209 | this._streams.delete(keyString) | |
| 210 | } | |
| 211 | } | |
| 212 | ||
| 213 | cork () { | |
| 214 | this._channel.cork() | |
| 215 | } | |
| 216 | ||
| 217 | uncork () { | |
| 218 | this._channel.uncork() | |
| 219 | } | |
| 220 | ||
| 221 | async end () { | |
| 222 | if (this._ending) return this._ending | |
| 223 | ||
| 224 | this._ending = EventEmitter.once(this, 'close') | |
| 225 | this._endMaybe() | |
| 226 | ||
| 227 | return this._ending | |
| 228 | } | |
| 229 | ||
| 230 | _endMaybe () { | |
| 231 | if (this._ending && this._pairing.size === 0) { | |
| 232 | this._channel.close() | |
| 233 | } | |
| 234 | } | |
| 235 | ||
| 236 | destroy (err) { | |
| 237 | if (this._destroyed) return | |
| 238 | this._destroyed = true | |
| 239 | ||
| 240 | this._error = err || errors.CHANNEL_DESTROYED() | |
| 241 | this._channel.close() | |
| 242 | } | |
| 243 | } | |
| 244 | ||
| 245 | class BlindRelayPair { | |
| 246 | constructor (token) { | |
| 247 | this.token = token | |
| 248 | this.links = [null, null] | |
| 249 | } | |
| 250 | ||
| 251 | get paired () { | |
| 252 | return this.links[0] !== null && this.links[1] !== null | |
| 253 | } | |
| 254 | ||
| 255 | remote (isInitiator) { | |
| 256 | return this.links[isInitiator ? 0 : 1] | |
| 257 | } | |
| 258 | } | |
| 259 | ||
| 260 | class BlindRelayLink { | |
| 261 | constructor (session, isInitiator, remoteId) { | |
| 262 | this.session = session | |
| 263 | this.isInitiator = isInitiator | |
| 264 | this.remoteId = remoteId | |
| 265 | this.stream = null | |
| 266 | } | |
| 267 | ||
| 268 | createStream () { | |
| 269 | if (this.stream) return | |
| 270 | ||
| 271 | this.stream = this.session._server._createStream({ | |
| 272 | firewall: this._onfirewall.bind(this) | |
| 273 | }) | |
| 274 | } | |
| 275 | ||
| 276 | _onfirewall (socket, port, host) { | |
| 277 | this.stream.connect(socket, this.remoteId, port, host) | |
| 278 | ||
| 279 | return false | |
| 280 | } | |
| 281 | } | |
| 282 | ||
| 283 | exports.Client = class BlindRelayClient extends EventEmitter { | |
| 284 | static _clients = new WeakMap() | |
| 285 | ||
| 286 | static from (stream, opts) { | |
| 287 | let client = this._clients.get(stream) | |
| 288 | if (client) return client | |
| 289 | client = new this(stream, opts) | |
| 290 | this._clients.set(stream, client) | |
| 291 | return client | |
| 292 | } | |
| 293 | ||
| 294 | constructor (stream, opts = {}) { | |
| 295 | super() | |
| 296 | ||
| 297 | const { | |
| 298 | id, | |
| 299 | handshake, | |
| 300 | handshakeEncoding | |
| 301 | } = opts | |
| 302 | ||
| 303 | this._mux = Protomux.from(stream) | |
| 304 | ||
| 305 | this._channel = this._mux.createChannel({ | |
| 306 | protocol: 'blind-relay', | |
| 307 | id, | |
| 308 | handshake: handshake ? handshakeEncoding || c.raw : null, | |
| 309 | onopen: this._onopen.bind(this), | |
| 310 | onclose: this._onclose.bind(this), | |
| 311 | ondestroy: this._ondestroy.bind(this) | |
| 312 | }) | |
| 313 | ||
| 314 | this._pair = this._channel.addMessage({ | |
| 315 | encoding: m.pair, | |
| 316 | onmessage: this._onpair.bind(this) | |
| 317 | }) | |
| 318 | ||
| 319 | this._unpair = this._channel.addMessage({ | |
| 320 | encoding: m.unpair | |
| 321 | }) | |
| 322 | ||
| 323 | this._ending = false | |
| 324 | this._destroyed = false | |
| 325 | this._error = null | |
| 326 | this._requests = new Map() | |
| 327 | ||
| 328 | this._channel.open(handshake) | |
| 329 | } | |
| 330 | ||
| 331 | get closed () { | |
| 332 | return this._channel.closed | |
| 333 | } | |
| 334 | ||
| 335 | get mux () { | |
| 336 | return this._mux | |
| 337 | } | |
| 338 | ||
| 339 | get stream () { | |
| 340 | return this._mux.stream | |
| 341 | } | |
| 342 | ||
| 343 | get requests () { | |
| 344 | return this._requests.values() | |
| 345 | } | |
| 346 | ||
| 347 | _onopen () { | |
| 348 | this.emit('open') | |
| 349 | } | |
| 350 | ||
| 351 | _onclose () { | |
| 352 | this._ending = Promise.resolve() | |
| 353 | ||
| 354 | const err = this._error || errors.CHANNEL_CLOSED() | |
| 355 | ||
| 356 | for (const request of this._requests.values()) { | |
| 357 | request.destroy(err) | |
| 358 | } | |
| 359 | ||
| 360 | this._requests.clear() | |
| 361 | ||
| 362 | this.constructor._clients.delete(this.stream) | |
| 363 | ||
| 364 | this.emit('close') | |
| 365 | } | |
| 366 | ||
| 367 | _ondestroy () { | |
| 368 | this._destroyed = true | |
| 369 | this.emit('destroy') | |
| 370 | } | |
| 371 | ||
| 372 | _onpair ({ isInitiator, token, id: remoteId }) { | |
| 373 | const request = this._requests.get(token.toString('hex')) | |
| 374 | ||
| 375 | if (request === undefined || request.isInitiator !== isInitiator) return | |
| 376 | ||
| 377 | request.push(remoteId) | |
| 378 | request.push(null) | |
| 379 | ||
| 380 | this.emit('pair', request.isInitiator, request.token, request.stream, remoteId) | |
| 381 | } | |
| 382 | ||
| 383 | pair (isInitiator, token, stream) { | |
| 384 | if (this._destroyed) throw errors.CHANNEL_DESTROYED() | |
| 385 | ||
| 386 | const keyString = token.toString('hex') | |
| 387 | ||
| 388 | if (this._requests.has(keyString)) throw errors.ALREADY_PAIRING() | |
| 389 | ||
| 390 | const request = new BlindRelayRequest(this, isInitiator, token, stream) | |
| 391 | ||
| 392 | this._requests.set(keyString, request) | |
| 393 | ||
| 394 | return request | |
| 395 | } | |
| 396 | ||
| 397 | unpair (token) { | |
| 398 | if (this._destroyed) throw errors.CHANNEL_DESTROYED() | |
| 399 | ||
| 400 | const request = this._requests.get(token.toString('hex')) | |
| 401 | ||
| 402 | if (request) request.destroy(errors.PAIRING_CANCELLED()) | |
| 403 | ||
| 404 | this._unpair.send({ token }) | |
| 405 | } | |
| 406 | ||
| 407 | cork () { | |
| 408 | this._channel.cork() | |
| 409 | } | |
| 410 | ||
| 411 | uncork () { | |
| 412 | this._channel.uncork() | |
| 413 | } | |
| 414 | ||
| 415 | async end () { | |
| 416 | if (this._ending) return this._ending | |
| 417 | ||
| 418 | this._ending = EventEmitter.once(this, 'close') | |
| 419 | this._endMaybe() | |
| 420 | ||
| 421 | return this._ending | |
| 422 | } | |
| 423 | ||
| 424 | _endMaybe () { | |
| 425 | if (this._ending && this._requests.size === 0) { | |
| 426 | this._channel.close() | |
| 427 | } | |
| 428 | } | |
| 429 | ||
| 430 | destroy (err) { | |
| 431 | if (this._destroyed) return | |
| 432 | this._destroyed = true | |
| 433 | ||
| 434 | this._error = err || errors.CHANNEL_DESTROYED() | |
| 435 | this._channel.close() | |
| 436 | } | |
| 437 | } | |
| 438 | ||
| 439 | class BlindRelayRequest extends Readable { | |
| 440 | constructor (client, isInitiator, token, stream) { | |
| 441 | super() | |
| 442 | ||
| 443 | this.client = client | |
| 444 | this.isInitiator = isInitiator | |
| 445 | this.token = token | |
| 446 | this.stream = stream | |
| 447 | } | |
| 448 | ||
| 449 | _open (cb) { | |
| 450 | if (this.client._destroyed) return cb(errors.CHANNEL_DESTROYED()) | |
| 451 | ||
| 452 | this.client._pair.send({ | |
| 453 | isInitiator: this.isInitiator, | |
| 454 | token: this.token, | |
| 455 | id: this.stream.id, | |
| 456 | seq: 0 | |
| 457 | }) | |
| 458 | ||
| 459 | cb(null) | |
| 460 | } | |
| 461 | ||
| 462 | _destroy (cb) { | |
| 463 | this.client._requests.delete(this.token.toString('hex')) | |
| 464 | ||
| 465 | cb(null) | |
| 466 | ||
| 467 | this.client._endMaybe() | |
| 468 | } | |
| 469 | } | |
| 470 | ||
| 471 | exports.token = function token (buf = b4a.allocUnsafe(32)) { | |
| 472 | sodium.randombytes_buf(buf) | |
| 473 | return buf | |
| 474 | } | |
| 475 | ||
| 476 | function noop () {} | |
| 477 | ||
| 478 | const m = exports.messages = {} | |
| 479 | ||
| 480 | const flags = bitfield(7) | |
| 481 | ||
| 482 | m.pair = { | |
| 483 | preencode (state, m) { | |
| 484 | flags.preencode(state) | |
| 485 | c.fixed32.preencode(state, m.token) | |
| 486 | c.uint.preencode(state, m.id) | |
| 487 | c.uint.preencode(state, m.seq) | |
| 488 | }, | |
| 489 | encode (state, m) { | |
| 490 | flags.encode(state, bits.of(m.isInitiator)) | |
| 491 | c.fixed32.encode(state, m.token) | |
| 492 | c.uint.encode(state, m.id) | |
| 493 | c.uint.encode(state, m.seq) | |
| 494 | }, | |
| 495 | decode (state) { | |
| 496 | const [isInitiator] = bits.iterator(flags.decode(state)) | |
| 497 | ||
| 498 | return { | |
| 499 | isInitiator, | |
| 500 | token: c.fixed32.decode(state), | |
| 501 | id: c.uint.decode(state), | |
| 502 | seq: c.uint.decode(state) | |
| 503 | } | |
| 504 | } | |
| 505 | } | |
| 506 | ||
| 507 | m.unpair = { | |
| 508 | preencode (state, m) { | |
| 509 | flags.preencode(state) | |
| 510 | c.fixed32.preencode(state, m.token) | |
| 511 | }, | |
| 512 | encode (state, m) { | |
| 513 | flags.encode(state, bits.of()) | |
| 514 | c.fixed32.encode(state, m.token) | |
| 515 | }, | |
| 516 | decode (state) { | |
| 517 | flags.decode(state) | |
| 518 | ||
| 519 | return { | |
| 520 | token: c.fixed32.decode(state) | |
| 521 | } | |
| 522 | } | |
| 523 | } |
| … | … | @@ -0,0 +1,34 @@ |
| 1 | module.exports = class BlindRelayError extends Error { | |
| 2 | constructor (msg, code, fn = BlindRelayError) { | |
| 3 | super(`${code}: ${msg}`) | |
| 4 | this.code = code | |
| 5 | ||
| 6 | if (Error.captureStackTrace) { | |
| 7 | Error.captureStackTrace(this, fn) | |
| 8 | } | |
| 9 | } | |
| 10 | ||
| 11 | get name () { | |
| 12 | return 'BlindRelayError' | |
| 13 | } | |
| 14 | ||
| 15 | static DUPLICATE_CHANNEL (msg = 'Duplicate channel') { | |
| 16 | return new BlindRelayError(msg, 'DUPLICATE_CHANNEL', BlindRelayError.DUPLICATE_CHANNEL) | |
| 17 | } | |
| 18 | ||
| 19 | static CHANNEL_CLOSED (msg = 'Channel closed') { | |
| 20 | return new BlindRelayError(msg, 'CHANNEL_CLOSED', BlindRelayError.CHANNEL_CLOSED) | |
| 21 | } | |
| 22 | ||
| 23 | static CHANNEL_DESTROYED (msg = 'Channel destroyed') { | |
| 24 | return new BlindRelayError(msg, 'CHANNEL_DESTROYED', BlindRelayError.CHANNEL_DESTROYED) | |
| 25 | } | |
| 26 | ||
| 27 | static ALREADY_PAIRING (msg = 'Already pairing') { | |
| 28 | return new BlindRelayError(msg, 'ALREADY_PAIRING', BlindRelayError.ALREADY_PAIRING) | |
| 29 | } | |
| 30 | ||
| 31 | static PAIRING_CANCELLED (msg = 'Pairing cancelled') { | |
| 32 | return new BlindRelayError(msg, 'PAIRING_CANCELLED', BlindRelayError.PAIRING_CANCELLED) | |
| 33 | } | |
| 34 | } |
| … | … | @@ -0,0 +1,45 @@ |
| 1 | { | |
| 2 | "name": "blind-relay", | |
| 3 | "version": "1.4.0", | |
| 4 | "description": "Blind relay for UDX over Protomux channels", | |
| 5 | "main": "index.js", | |
| 6 | "files": [ | |
| 7 | "index.js", | |
| 8 | "lib" | |
| 9 | ], | |
| 10 | "scripts": { | |
| 11 | "test": "standard && brittle test.mjs" | |
| 12 | }, | |
| 13 | "repository": { | |
| 14 | "type": "git", | |
| 15 | "url": "git+https://github.com/holepunchto/blind-relay.git" | |
| 16 | }, | |
| 17 | "author": "Holepunch", | |
| 18 | "license": "Apache-2.0", | |
| 19 | "bugs": { | |
| 20 | "url": "https://github.com/holepunchto/blind-relay/issues" | |
| 21 | }, | |
| 22 | "homepage": "https://github.com/holepunchto/blind-relay#readme", | |
| 23 | "imports": { | |
| 24 | "events": { | |
| 25 | "bare": "bare-events", | |
| 26 | "default": "events" | |
| 27 | } | |
| 28 | }, | |
| 29 | "dependencies": { | |
| 30 | "b4a": "^1.6.4", | |
| 31 | "bare-events": "^2.2.0", | |
| 32 | "bits-to-bytes": "^1.3.0", | |
| 33 | "compact-encoding": "^2.12.0", | |
| 34 | "compact-encoding-bitfield": "^1.0.0", | |
| 35 | "protomux": "^3.5.1", | |
| 36 | "sodium-universal": "^5.0.0", | |
| 37 | "streamx": "^2.15.1" | |
| 38 | }, | |
| 39 | "devDependencies": { | |
| 40 | "brittle": "^3.2.1", | |
| 41 | "hyperdht": "^6.6.1", | |
| 42 | "standard": "^17.0.0", | |
| 43 | "udx-native": "^1.6.1" | |
| 44 | } | |
| 45 | } |
| … | … | @@ -0,0 +1,23 @@ |
| 1 | name: Build Status | |
| 2 | on: | |
| 3 | push: | |
| 4 | branches: | |
| 5 | - master | |
| 6 | pull_request: | |
| 7 | branches: | |
| 8 | - master | |
| 9 | jobs: | |
| 10 | build: | |
| 11 | strategy: | |
| 12 | matrix: | |
| 13 | node-version: [lts/*] | |
| 14 | os: [ubuntu-latest, macos-latest, windows-latest] | |
| 15 | runs-on: ${{ matrix.os }} | |
| 16 | steps: | |
| 17 | - uses: actions/checkout@v2 | |
| 18 | - name: Use Node.js ${{ matrix.node-version }} | |
| 19 | uses: actions/setup-node@v2 | |
| 20 | with: | |
| 21 | node-version: ${{ matrix.node-version }} | |
| 22 | - run: npm install | |
| 23 | - run: npm test |
| … | … | @@ -0,0 +1,21 @@ |
| 1 | The MIT License (MIT) | |
| 2 | ||
| 3 | Copyright (c) 2021 Mathias Buus | |
| 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 | |
| 13 | all 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 | |
| 21 | THE SOFTWARE. |
| … | … | @@ -0,0 +1,32 @@ |
| 1 | # bogon | |
| 2 | ||
| 3 | Check if an IP is a bogon | |
| 4 | ||
| 5 | ``` | |
| 6 | npm install bogon | |
| 7 | ``` | |
| 8 | ||
| 9 | https://ipinfo.io/bogon | |
| 10 | ||
| 11 | ||
| 12 | ## Usage | |
| 13 | ||
| 14 | ``` js | |
| 15 | const bogon = require('bogon') | |
| 16 | ||
| 17 | console.log(bogon('192.168.0.1')) // true | |
| 18 | console.log(bogon('8.8.8.8')) // false | |
| 19 | ``` | |
| 20 | ||
| 21 | As a utility it also exposes an `isPrivate` helper | |
| 22 | to detect if a bogon IP is a private IP address on a local network. | |
| 23 | ||
| 24 | ``` js | |
| 25 | console.log(bogon.isPrivate('192.168.0.1')) // true | |
| 26 | console.log(bogon('224.0.1.1')) // true | |
| 27 | console.log(bogon.isPrivate('224.0.1.1')) // false | |
| 28 | ``` | |
| 29 | ||
| 30 | ## License | |
| 31 | ||
| 32 | MIT |
| … | … | @@ -0,0 +1,34 @@ |
| 1 | import bench from 'nanobench' | |
| 2 | import b4a from 'b4a' | |
| 3 | ||
| 4 | import isBogon from './index.js' | |
| 5 | ||
| 6 | bench('string', (b) => { | |
| 7 | const ip = '255.255.255.255' | |
| 8 | ||
| 9 | b.start() | |
| 10 | ||
| 11 | let result | |
| 12 | ||
| 13 | for (let i = 0; i < 10000000; i++) { | |
| 14 | result = isBogon(ip) | |
| 15 | } | |
| 16 | ||
| 17 | b.log(result) | |
| 18 | b.end() | |
| 19 | }) | |
| 20 | ||
| 21 | bench('buffer', (b) => { | |
| 22 | const ip = b4a.from([255, 255, 255, 255]) | |
| 23 | ||
| 24 | b.start() | |
| 25 | ||
| 26 | let result | |
| 27 | ||
| 28 | for (let i = 0; i < 10000000; i++) { | |
| 29 | result = isBogon(ip) | |
| 30 | } | |
| 31 | ||
| 32 | b.log(result) | |
| 33 | b.end() | |
| 34 | }) |
| … | … | @@ -0,0 +1,122 @@ |
| 1 | // https://ipinfo.io/bogon | |
| 2 | ||
| 3 | const b4a = require('b4a') | |
| 4 | const c = require('compact-encoding') | |
| 5 | const net = require('compact-encoding-net') | |
| 6 | ||
| 7 | module.exports = exports = function isBogon (ip) { | |
| 8 | return isBogonIP(ensureBuffer(ip)) | |
| 9 | } | |
| 10 | ||
| 11 | exports.isBogon = exports | |
| 12 | ||
| 13 | exports.isPrivate = function isPrivate (ip) { | |
| 14 | return isPrivateIP(ensureBuffer(ip)) | |
| 15 | } | |
| 16 | ||
| 17 | exports.isReserved = function isReserved (ip) { | |
| 18 | return isReservedIP(ensureBuffer(ip)) | |
| 19 | } | |
| 20 | ||
| 21 | function isBogonIP (ip) { | |
| 22 | return isPrivateIP(ip) || isReservedIP(ip) | |
| 23 | } | |
| 24 | ||
| 25 | function isPrivateIP (ip) { | |
| 26 | return ip.byteLength === 4 ? isPrivateIPv4(ip) : false // IPv6 has no private IPs | |
| 27 | } | |
| 28 | ||
| 29 | function isPrivateIPv4 (ip) { | |
| 30 | return ( | |
| 31 | // 10.0.0.0/8 Private-use networks | |
| 32 | (ip[0] === 10) || | |
| 33 | // 100.64.0.0/10 Carrier-grade NAT | |
| 34 | (ip[0] === 100 && ip[1] >= 64 && ip[1] <= 127) || | |
| 35 | // 127.0.0.0/8 Loopback + Name collision occurrence (127.0.53.53) | |
| 36 | (ip[0] === 127) || | |
| 37 | // 169.254.0.0/16 Link local | |
| 38 | (ip[0] === 169 && ip[1] === 254) || | |
| 39 | // 172.16.0.0/12 Private-use networks | |
| 40 | (ip[0] === 172 && ip[1] >= 16 && ip[1] <= 31) || | |
| 41 | // 192.168.0.0/16 Private-use networks | |
| 42 | (ip[0] === 192 && ip[1] === 168) | |
| 43 | ) | |
| 44 | } | |
| 45 | ||
| 46 | function isReservedIP (ip) { | |
| 47 | return ip.byteLength === 4 ? isReservedIPv4(ip) : isReservedIPv6(ip) | |
| 48 | } | |
| 49 | ||
| 50 | function isReservedIPv4 (ip) { | |
| 51 | return ( | |
| 52 | // 0.0.0.0/8 "This" network | |
| 53 | (ip[0] === 0) || | |
| 54 | // 192.0.0.0/24 IETF protocol assignments | |
| 55 | (ip[0] === 192 && ip[1] === 0 && ip[2] === 0) || | |
| 56 | // 192.0.2.0/24 TEST-NET-1 | |
| 57 | (ip[0] === 192 && ip[1] === 0 && ip[2] === 2) || | |
| 58 | // 198.18.0.0/15 Network interconnect device benchmark testing | |
| 59 | (ip[0] === 198 && ip[1] >= 18 && ip[1] <= 19) || | |
| 60 | // 198.51.100.0/24 TEST-NET-2 | |
| 61 | (ip[0] === 198 && ip[1] === 51 && ip[2] === 100) || | |
| 62 | // 203.0.113.0/24 TEST-NET-3 | |
| 63 | (ip[0] === 203 && ip[1] === 0 && ip[2] === 113) || | |
| 64 | // 224.0.0.0/4 Multicast | |
| 65 | (ip[0] >= 224 && ip[0] <= 239) || | |
| 66 | // 240.0.0.0/4 Reserved for future use | |
| 67 | (ip[0] >= 240) || | |
| 68 | // 255.255.255.255/32 | |
| 69 | (ip[0] === 255 && ip[1] === 255 && ip[2] === 255 && ip[3] === 255) | |
| 70 | ) | |
| 71 | } | |
| 72 | ||
| 73 | function isReservedIPv6 (ip) { | |
| 74 | return ( | |
| 75 | // ::/128 Node-scope unicast unspecified address | |
| 76 | // ::1/128 Node-scope unicast loopback address | |
| 77 | ( | |
| 78 | ip[0] === 0 && ip[1] === 0 && ip[2] === 0 && ip[3] === 0 && ip[4] === 0 && | |
| 79 | ip[5] === 0 && ip[6] === 0 && ip[7] === 0 && ip[8] === 0 && ip[9] === 0 && | |
| 80 | ip[10] === 0 && ip[11] === 0 && ip[12] === 0 && ip[13] === 0 && ip[14] === 0 && | |
| 81 | ip[15] <= 1 | |
| 82 | ) || | |
| 83 | // ::ffff:0:0/96 IPv4-mapped addresses | |
| 84 | // ::/96 IPv4-compatible addresses | |
| 85 | ( | |
| 86 | ip[0] === 0 && ip[1] === 0 && ip[2] === 0 && ip[3] === 0 && ip[4] === 0 && | |
| 87 | ip[5] === 0 && ip[6] === 0 && ip[7] === 0 && ip[8] === 0 && ip[9] === 0 && | |
| 88 | (ip[10] === 0 || ip[10] === 0xff) && | |
| 89 | (ip[11] === 0 || ip[11] === 0xff) | |
| 90 | ) || | |
| 91 | // 100::/64 Remotely triggered black hole addresses | |
| 92 | (ip[0] === 0x01 && ip[1] === 0 && ip[2] === 0 && ip[3] === 0 && ip[4] === 0 && ip[5] === 0 && ip[6] === 0 && ip[7] === 0) || | |
| 93 | // 2001:10::/28 Overlay routable cryptographic hash identifiers (ORCHID) | |
| 94 | (ip[0] === 0x20 && ip[1] === 0x01 && ip[2] === 0 && ip[3] >= 0x10 && ip[3] <= 0x1f) || | |
| 95 | // 2001:20::/28 Overlay routable cryptographic hash identifiers version 2 (ORCHIDv2) | |
| 96 | (ip[0] === 0x20 && ip[1] === 0x01 && ip[2] === 0 && ip[3] >= 0x20 && ip[3] <= 0x2f) || | |
| 97 | // 2001:db8::/32 Documentation prefix | |
| 98 | (ip[0] === 0x20 && ip[1] === 0x01 && ip[2] === 0x0d && ip[3] === 0xb8) || | |
| 99 | // fc00::/7 Unique local addresses (ULA) | |
| 100 | (ip[0] >= 0xfc && ip[0] <= 0xfd) || | |
| 101 | // fe80::/10 Link-local unicast | |
| 102 | (ip[0] === 0xfe && ip[1] >= 0x80 && ip[1] <= 0xbf) || | |
| 103 | // ff00::/8 Multicast | |
| 104 | (ip[0] === 0xff) | |
| 105 | ) | |
| 106 | } | |
| 107 | ||
| 108 | const state = c.state(0, 0, b4a.allocUnsafe(1 /* family */ + 16)) | |
| 109 | ||
| 110 | function ensureBuffer (ip) { | |
| 111 | if (b4a.isBuffer(ip)) return ip | |
| 112 | ||
| 113 | net.ip.preencode(state, ip) | |
| 114 | net.ip.encode(state, ip) | |
| 115 | ||
| 116 | const buffer = state.buffer.subarray(1 /* family */, state.end) | |
| 117 | ||
| 118 | state.start = 0 | |
| 119 | state.end = 0 | |
| 120 | ||
| 121 | return buffer | |
| 122 | } |
| … | … | @@ -0,0 +1,28 @@ |
| 1 | { | |
| 2 | "name": "bogon", | |
| 3 | "version": "1.2.0", | |
| 4 | "description": "Check if an IP is a bogon", | |
| 5 | "main": "index.js", | |
| 6 | "scripts": { | |
| 7 | "test": "standard && brittle test.mjs" | |
| 8 | }, | |
| 9 | "dependencies": { | |
| 10 | "compact-encoding": "^2.11.0", | |
| 11 | "compact-encoding-net": "^1.2.0" | |
| 12 | }, | |
| 13 | "devDependencies": { | |
| 14 | "brittle": "^3.0.4", | |
| 15 | "nanobench": "^2.1.1", | |
| 16 | "standard": "^17.0.0" | |
| 17 | }, | |
| 18 | "repository": { | |
| 19 | "type": "git", | |
| 20 | "url": "https://github.com/mafintosh/bogon.git" | |
| 21 | }, | |
| 22 | "author": "Mathias Buus (@mafintosh)", | |
| 23 | "license": "MIT", | |
| 24 | "bugs": { | |
| 25 | "url": "https://github.com/mafintosh/bogon/issues" | |
| 26 | }, | |
| 27 | "homepage": "https://github.com/mafintosh/bogon" | |
| 28 | } |
| … | … | @@ -0,0 +1,23 @@ |
| 1 | import test from 'brittle' | |
| 2 | import b4a from 'b4a' | |
| 3 | ||
| 4 | import { isBogon } from './index.js' | |
| 5 | ||
| 6 | test('ipv4', (t) => { | |
| 7 | t.ok(isBogon('127.0.0.1'), 'loopback, string') | |
| 8 | t.ok(isBogon(b4a.from([127, 0, 0, 1])), 'loopback, buffer') | |
| 9 | ||
| 10 | t.absent(isBogon('84.123.96.194'), 'not bogon, string') | |
| 11 | t.absent(isBogon(b4a.from([84, 123, 96, 194])), 'not bogon, buffer') | |
| 12 | }) | |
| 13 | ||
| 14 | test('ipv6', (t) => { | |
| 15 | t.ok(isBogon('::1'), 'loopback, string') | |
| 16 | t.ok(isBogon(b4a.from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1])), 'loopback, buffer') | |
| 17 | ||
| 18 | t.absent(isBogon('89c7:2c35:937e:3171:10c:831a:a32e:bff4'), 'not bogon, string') | |
| 19 | t.absent(isBogon(b4a.from([0x89, 0xc7, 0x2c, 0x35, 0x93, 0x7e, 0x31, 0x71, 0x10, 0xc, 0x83, 0x1a, 0xa3, 0x2e, 0xbf, 0xf4])), 'not bogon, buffer') | |
| 20 | ||
| 21 | t.ok(isBogon('::ffff:127.0.0.1'), 'ipv4-mapped') | |
| 22 | t.ok(isBogon('fe80::c001:1dff:fee0:0'), 'link-local') | |
| 23 | }) |
| … | … | @@ -0,0 +1,201 @@ |
| 1 | Apache License | |
| 2 | Version 2.0, January 2004 | |
| 3 | http://www.apache.org/licenses/ | |
| 4 | ||
| 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | |
| 6 | ||
| 7 | 1. Definitions. | |
| 8 | ||
| 9 | "License" shall mean the terms and conditions for use, reproduction, | |
| 10 | and distribution as defined by Sections 1 through 9 of this document. | |
| 11 | ||
| 12 | "Licensor" shall mean the copyright owner or entity authorized by | |
| 13 | the copyright owner that is granting the License. | |
| 14 | ||
| 15 | "Legal Entity" shall mean the union of the acting entity and all | |
| 16 | other entities that control, are controlled by, or are under common | |
| 17 | control with that entity. For the purposes of this definition, | |
| 18 | "control" means (i) the power, direct or indirect, to cause the | |
| 19 | direction or management of such entity, whether by contract or | |
| 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the | |
| 21 | outstanding shares, or (iii) beneficial ownership of such entity. | |
| 22 | ||
| 23 | "You" (or "Your") shall mean an individual or Legal Entity | |
| 24 | exercising permissions granted by this License. | |
| 25 | ||
| 26 | "Source" form shall mean the preferred form for making modifications, | |
| 27 | including but not limited to software source code, documentation | |
| 28 | source, and configuration files. | |
| 29 | ||
| 30 | "Object" form shall mean any form resulting from mechanical | |
| 31 | transformation or translation of a Source form, including but | |
| 32 | not limited to compiled object code, generated documentation, | |
| 33 | and conversions to other media types. | |
| 34 | ||
| 35 | "Work" shall mean the work of authorship, whether in Source or | |
| 36 | Object form, made available under the License, as indicated by a | |
| 37 | copyright notice that is included in or attached to the work | |
| 38 | (an example is provided in the Appendix below). | |
| 39 | ||
| 40 | "Derivative Works" shall mean any work, whether in Source or Object | |
| 41 | form, that is based on (or derived from) the Work and for which the | |
| 42 | editorial revisions, annotations, elaborations, or other modifications | |
| 43 | represent, as a whole, an original work of authorship. For the purposes | |
| 44 | of this License, Derivative Works shall not include works that remain | |
| 45 | separable from, or merely link (or bind by name) to the interfaces of, | |
| 46 | the Work and Derivative Works thereof. | |
| 47 | ||
| 48 | "Contribution" shall mean any work of authorship, including | |
| 49 | the original version of the Work and any modifications or additions | |
| 50 | to that Work or Derivative Works thereof, that is intentionally | |
| 51 | submitted to Licensor for inclusion in the Work by the copyright owner | |
| 52 | or by an individual or Legal Entity authorized to submit on behalf of | |
| 53 | the copyright owner. For the purposes of this definition, "submitted" | |
| 54 | means any form of electronic, verbal, or written communication sent | |
| 55 | to the Licensor or its representatives, including but not limited to | |
| 56 | communication on electronic mailing lists, source code control systems, | |
| 57 | and issue tracking systems that are managed by, or on behalf of, the | |
| 58 | Licensor for the purpose of discussing and improving the Work, but | |
| 59 | excluding communication that is conspicuously marked or otherwise | |
| 60 | designated in writing by the copyright owner as "Not a Contribution." | |
| 61 | ||
| 62 | "Contributor" shall mean Licensor and any individual or Legal Entity | |
| 63 | on behalf of whom a Contribution has been received by Licensor and | |
| 64 | subsequently incorporated within the Work. | |
| 65 | ||
| 66 | 2. Grant of Copyright License. Subject to the terms and conditions of | |
| 67 | this License, each Contributor hereby grants to You a perpetual, | |
| 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 69 | copyright license to reproduce, prepare Derivative Works of, | |
| 70 | publicly display, publicly perform, sublicense, and distribute the | |
| 71 | Work and such Derivative Works in Source or Object form. | |
| 72 | ||
| 73 | 3. Grant of Patent License. Subject to the terms and conditions of | |
| 74 | this License, each Contributor hereby grants to You a perpetual, | |
| 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 76 | (except as stated in this section) patent license to make, have made, | |
| 77 | use, offer to sell, sell, import, and otherwise transfer the Work, | |
| 78 | where such license applies only to those patent claims licensable | |
| 79 | by such Contributor that are necessarily infringed by their | |
| 80 | Contribution(s) alone or by combination of their Contribution(s) | |
| 81 | with the Work to which such Contribution(s) was submitted. If You | |
| 82 | institute patent litigation against any entity (including a | |
| 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work | |
| 84 | or a Contribution incorporated within the Work constitutes direct | |
| 85 | or contributory patent infringement, then any patent licenses | |
| 86 | granted to You under this License for that Work shall terminate | |
| 87 | as of the date such litigation is filed. | |
| 88 | ||
| 89 | 4. Redistribution. You may reproduce and distribute copies of the | |
| 90 | Work or Derivative Works thereof in any medium, with or without | |
| 91 | modifications, and in Source or Object form, provided that You | |
| 92 | meet the following conditions: | |
| 93 | ||
| 94 | (a) You must give any other recipients of the Work or | |
| 95 | Derivative Works a copy of this License; and | |
| 96 | ||
| 97 | (b) You must cause any modified files to carry prominent notices | |
| 98 | stating that You changed the files; and | |
| 99 | ||
| 100 | (c) You must retain, in the Source form of any Derivative Works | |
| 101 | that You distribute, all copyright, patent, trademark, and | |
| 102 | attribution notices from the Source form of the Work, | |
| 103 | excluding those notices that do not pertain to any part of | |
| 104 | the Derivative Works; and | |
| 105 | ||
| 106 | (d) If the Work includes a "NOTICE" text file as part of its | |
| 107 | distribution, then any Derivative Works that You distribute must | |
| 108 | include a readable copy of the attribution notices contained | |
| 109 | within such NOTICE file, excluding those notices that do not | |
| 110 | pertain to any part of the Derivative Works, in at least one | |
| 111 | of the following places: within a NOTICE text file distributed | |
| 112 | as part of the Derivative Works; within the Source form or | |
| 113 | documentation, if provided along with the Derivative Works; or, | |
| 114 | within a display generated by the Derivative Works, if and | |
| 115 | wherever such third-party notices normally appear. The contents | |
| 116 | of the NOTICE file are for informational purposes only and | |
| 117 | do not modify the License. You may add Your own attribution | |
| 118 | notices within Derivative Works that You distribute, alongside | |
| 119 | or as an addendum to the NOTICE text from the Work, provided | |
| 120 | that such additional attribution notices cannot be construed | |
| 121 | as modifying the License. | |
| 122 | ||
| 123 | You may add Your own copyright statement to Your modifications and | |
| 124 | may provide additional or different license terms and conditions | |
| 125 | for use, reproduction, or distribution of Your modifications, or | |
| 126 | for any such Derivative Works as a whole, provided Your use, | |
| 127 | reproduction, and distribution of the Work otherwise complies with | |
| 128 | the conditions stated in this License. | |
| 129 | ||
| 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, | |
| 131 | any Contribution intentionally submitted for inclusion in the Work | |
| 132 | by You to the Licensor shall be under the terms and conditions of | |
| 133 | this License, without any additional terms or conditions. | |
| 134 | Notwithstanding the above, nothing herein shall supersede or modify | |
| 135 | the terms of any separate license agreement you may have executed | |
| 136 | with Licensor regarding such Contributions. | |
| 137 | ||
| 138 | 6. Trademarks. This License does not grant permission to use the trade | |
| 139 | names, trademarks, service marks, or product names of the Licensor, | |
| 140 | except as required for reasonable and customary use in describing the | |
| 141 | origin of the Work and reproducing the content of the NOTICE file. | |
| 142 | ||
| 143 | 7. Disclaimer of Warranty. Unless required by applicable law or | |
| 144 | agreed to in writing, Licensor provides the Work (and each | |
| 145 | Contributor provides its Contributions) on an "AS IS" BASIS, | |
| 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |
| 147 | implied, including, without limitation, any warranties or conditions | |
| 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | |
| 149 | PARTICULAR PURPOSE. You are solely responsible for determining the | |
| 150 | appropriateness of using or redistributing the Work and assume any | |
| 151 | risks associated with Your exercise of permissions under this License. | |
| 152 | ||
| 153 | 8. Limitation of Liability. In no event and under no legal theory, | |
| 154 | whether in tort (including negligence), contract, or otherwise, | |
| 155 | unless required by applicable law (such as deliberate and grossly | |
| 156 | negligent acts) or agreed to in writing, shall any Contributor be | |
| 157 | liable to You for damages, including any direct, indirect, special, | |
| 158 | incidental, or consequential damages of any character arising as a | |
| 159 | result of this License or out of the use or inability to use the | |
| 160 | Work (including but not limited to damages for loss of goodwill, | |
| 161 | work stoppage, computer failure or malfunction, or any and all | |
| 162 | other commercial damages or losses), even if such Contributor | |
| 163 | has been advised of the possibility of such damages. | |
| 164 | ||
| 165 | 9. Accepting Warranty or Additional Liability. While redistributing | |
| 166 | the Work or Derivative Works thereof, You may choose to offer, | |
| 167 | and charge a fee for, acceptance of support, warranty, indemnity, | |
| 168 | or other liability obligations and/or rights consistent with this | |
| 169 | License. However, in accepting such obligations, You may act only | |
| 170 | on Your own behalf and on Your sole responsibility, not on behalf | |
| 171 | of any other Contributor, and only if You agree to indemnify, | |
| 172 | defend, and hold each Contributor harmless for any liability | |
| 173 | incurred by, or claims asserted against, such Contributor by reason | |
| 174 | of your accepting any such warranty or additional liability. | |
| 175 | ||
| 176 | END OF TERMS AND CONDITIONS | |
| 177 | ||
| 178 | APPENDIX: How to apply the Apache License to your work. | |
| 179 | ||
| 180 | To apply the Apache License to your work, attach the following | |
| 181 | boilerplate notice, with the fields enclosed by brackets "[]" | |
| 182 | replaced with your own identifying information. (Don't include | |
| 183 | the brackets!) The text should be enclosed in the appropriate | |
| 184 | comment syntax for the file format. We also recommend that a | |
| 185 | file or class name and description of purpose be included on the | |
| 186 | same "printed page" as the copyright notice for easier | |
| 187 | identification within third-party archives. | |
| 188 | ||
| 189 | Copyright [yyyy] [name of copyright owner] | |
| 190 | ||
| 191 | Licensed under the Apache License, Version 2.0 (the "License"); | |
| 192 | you may not use this file except in compliance with the License. | |
| 193 | You may obtain a copy of the License at | |
| 194 | ||
| 195 | http://www.apache.org/licenses/LICENSE-2.0 | |
| 196 | ||
| 197 | Unless required by applicable law or agreed to in writing, software | |
| 198 | distributed under the License is distributed on an "AS IS" BASIS, | |
| 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 200 | See the License for the specific language governing permissions and | |
| 201 | limitations under the License. |
| … | … | @@ -0,0 +1,673 @@ |
| 1 | # brittle | |
| 2 | ||
| 3 | > tap à la mode | |
| 4 | ||
| 5 | A TAP test runner built for modern times. | |
| 6 | ||
| 7 | <img width=300 height=200 src=brittle.png> | |
| 8 | ||
| 9 | ## Usage | |
| 10 | ||
| 11 | First install brittle from npm | |
| 12 | ||
| 13 | ``` | |
| 14 | npm i brittle | |
| 15 | ``` | |
| 16 | ||
| 17 | Then start writing tests | |
| 18 | ||
| 19 | ```javascript | |
| 20 | import test from 'brittle' | |
| 21 | ||
| 22 | test('basic', function (t) { | |
| 23 | t.is(typeof Date.now(), 'number') | |
| 24 | t.not(typeof Date.now(), 'string') | |
| 25 | ||
| 26 | t.ok(Date.now() > 0) | |
| 27 | t.absent(null) | |
| 28 | ||
| 29 | t.comment('text') | |
| 30 | ||
| 31 | t.alike({ a: 1 }, { a: 1 }) | |
| 32 | t.unlike({ a: 2 }, { a: 3 }) | |
| 33 | ||
| 34 | t.pass() | |
| 35 | t.fail() | |
| 36 | }) | |
| 37 | ||
| 38 | test('asynchronous', async function (t) { | |
| 39 | await new Promise(r => setTimeout(r, 250)) | |
| 40 | t.pass() | |
| 41 | }) | |
| 42 | ||
| 43 | test('plans', function (t) { | |
| 44 | t.plan(2) | |
| 45 | t.pass() | |
| 46 | setTimeout(() => t.pass(), 250) | |
| 47 | }) | |
| 48 | ||
| 49 | test('classic subtest', function (t) { | |
| 50 | t.test('subtest', function (sub) { | |
| 51 | sub.plan(1) | |
| 52 | sub.pass() | |
| 53 | }) | |
| 54 | }) | |
| 55 | ||
| 56 | test('inverted subtest', function (t) { | |
| 57 | const sub = t.test('subtest') | |
| 58 | sub.plan(1) | |
| 59 | sub.pass() | |
| 60 | }) | |
| 61 | ||
| 62 | test('executions', async function (t) { | |
| 63 | t.execution(() => 'should not throw') | |
| 64 | await t.execution(async () => 'should not reject') | |
| 65 | }) | |
| 66 | ||
| 67 | test('exceptions', async function (t) { | |
| 68 | t.exception(() => { throw Error('expected to throw') }) | |
| 69 | await t.exception(async () => { throw Error('expected to reject') }) | |
| 70 | }) | |
| 71 | ||
| 72 | const a = test('inverted test without plan needs end()') | |
| 73 | a.pass() | |
| 74 | a.end() | |
| 75 | ||
| 76 | const b = test('inverted test with plan') | |
| 77 | b.plan(1) | |
| 78 | b.pass() | |
| 79 | ||
| 80 | const c = test('inverted tests can be awaited') | |
| 81 | c.plan(1) | |
| 82 | setTimeout(() => c.pass(), 250) | |
| 83 | await c | |
| 84 | ``` | |
| 85 | ||
| 86 | Every assertion can have a message, i.e. `t.pass('msg')`, `t.ok(false, 'should be true')`, etc.\ | |
| 87 | There are also utilities like `t.timeout(ms)`, `t.teardown(fn)`, etc.\ | |
| 88 | Check the API but also all the [assertions here](#assertions) and [utilities here](#utilities). | |
| 89 | ||
| 90 | ## Test Runtimes | |
| 91 | When installed as a dependency or globally, the following commands for using different runtimes are available: | |
| 92 | * `brittle-node`/`brittle` - Runs tests using [Node.js](https://nodejs.org/). | |
| 93 | * `brittle-bare` - Runs tests using [Bare](https://docs.pears.com/bare-reference/overview). | |
| 94 | * `brittle-pear` - Runs tests using [Pear](https://docs.pears.com/). | |
| 95 | ||
| 96 | ## API | |
| 97 | ||
| 98 | ```js | |
| 99 | import { test, solo, skip, hook, todo, configure } from 'brittle' | |
| 100 | ``` | |
| 101 | ||
| 102 | #### `test([name], [options], callback)` | |
| 103 | ||
| 104 | Create a classic test with an optional `name`. | |
| 105 | ||
| 106 | #### Available `options` for any test creation: | |
| 107 | * `timeout` (`30000`) - milliseconds to wait before ending a stalling test. | |
| 108 | * `solo` (`false`) - Skip all other tests except the `solo()` ones. | |
| 109 | * `hook` (`false`) - setup and teardown resources. | |
| 110 | * `skip` (`false`) - skip this test, alternatively use the `skip()` function. | |
| 111 | * `todo` (`false`) - mark this test as todo and skip it, alternatively use the `todo()` function. | |
| 112 | * `stealth` (`false`) - only print test summary. | |
| 113 | ||
| 114 | The `callback` function (can be async) receives an object called `assert`.\ | |
| 115 | `assert` (or `t`) provides the assertions and utilities interface. | |
| 116 | ||
| 117 | ```js | |
| 118 | import test from 'brittle' | |
| 119 | ||
| 120 | test('basic', function (t) { | |
| 121 | t.pass() | |
| 122 | }) | |
| 123 | ``` | |
| 124 | ||
| 125 | Test files can be executed directly with `node`, as they're normal Node.js programs. | |
| 126 | ||
| 127 | The `test` method is conveniently both the default export and named exported method: | |
| 128 | ||
| 129 | ```js | |
| 130 | import { test } from 'brittle' | |
| 131 | ``` | |
| 132 | ||
| 133 | Classic tests will run sequentially, buffering pending tests until any prior test catches up. | |
| 134 | ||
| 135 | Any test function returns a promise so you can optionally await for its result: | |
| 136 | ||
| 137 | ```js | |
| 138 | const isOk = await test('basic', function (t) { | |
| 139 | t.pass() | |
| 140 | }) | |
| 141 | ``` | |
| 142 | ||
| 143 | #### `test([name], [options]) => assert` | |
| 144 | ||
| 145 | Create an inverted test with an optional `name`. | |
| 146 | ||
| 147 | All `options` for inverted tests are [listed here](#available-options-for-any-test-creation). | |
| 148 | ||
| 149 | An object called `assert` (or `t`) is returned, the same as the classic test. | |
| 150 | ||
| 151 | This time it's also a promise, it can be awaited and it resolves at test completion. | |
| 152 | ||
| 153 | ```js | |
| 154 | import test from 'brittle' | |
| 155 | ||
| 156 | const t = test('basic') | |
| 157 | ||
| 158 | t.plan(1) | |
| 159 | ||
| 160 | setTimeout(() => { | |
| 161 | t.pass() | |
| 162 | }, 1000) | |
| 163 | ||
| 164 | await t // Won't proceed past here until plan is fulfilled | |
| 165 | ``` | |
| 166 | ||
| 167 | For inverted tests without a plan, the `end` method must be called: | |
| 168 | ||
| 169 | ```js | |
| 170 | const t = test('basic') | |
| 171 | ||
| 172 | setTimeout(() => { | |
| 173 | t.pass() | |
| 174 | t.end() | |
| 175 | }, 1000) | |
| 176 | ||
| 177 | await t | |
| 178 | ``` | |
| 179 | ||
| 180 | The `end()` method can be called inline, for inverted tests without a plan: | |
| 181 | ||
| 182 | ```js | |
| 183 | const t = test('basic') | |
| 184 | t.pass() | |
| 185 | t.end() | |
| 186 | ``` | |
| 187 | ||
| 188 | Control flow of inverted is entirely dependent on where its `assert` is awaited.\ | |
| 189 | The following executes one test after another: | |
| 190 | ||
| 191 | ```js | |
| 192 | const a = test('first test') | |
| 193 | const b = test('second test') | |
| 194 | a.plan(1) | |
| 195 | b.plan(1) | |
| 196 | a.pass() | |
| 197 | await a | |
| 198 | b.pass() | |
| 199 | await b | |
| 200 | ``` | |
| 201 | ||
| 202 | Awaiting the promise gives you its result: | |
| 203 | ||
| 204 | ```js | |
| 205 | const t = test('first test') | |
| 206 | t.plan(1) | |
| 207 | t.pass() | |
| 208 | const isOk = await t | |
| 209 | ``` | |
| 210 | ||
| 211 | #### `stealth([name], [options], callback)` | |
| 212 | #### `stealth([name], [options]) => assert` | |
| 213 | ||
| 214 | Create a stealth test.\ | |
| 215 | This will provide a new sub-assert object that only prints the test summary without assertions and ends the current test upon a failed assertion. | |
| 216 | ||
| 217 | All `options` are the same as `test` which are [listed here](#available-options-for-any-test-creation). | |
| 218 | ||
| 219 | #### `t.test([name], [options], callback)` | |
| 220 | #### `t.test([name], [options]) => assert` | |
| 221 | ||
| 222 | A subtest can be created by calling `test` on an `assert` (or `t`) object.\ | |
| 223 | This will provide a new sub-assert object. | |
| 224 | ||
| 225 | All `options` for subtests are [listed here](#available-options-for-any-test-creation). | |
| 226 | ||
| 227 | Using this in inverted style can be very useful for flow control within a test: | |
| 228 | ||
| 229 | ```js | |
| 230 | test('basic', async function (t) { | |
| 231 | const a = t.test('sub test') | |
| 232 | const b = t.test('other sub test') | |
| 233 | ||
| 234 | a.plan(1) | |
| 235 | b.plan(1) | |
| 236 | ||
| 237 | setTimeout(() => a.ok(true), Math.random() * 1000) | |
| 238 | setTimeout(() => b.ok(true), Math.random() * 1000) | |
| 239 | ||
| 240 | // Won't proceed past here until both a and b plans are fulfilled | |
| 241 | await a | |
| 242 | await b | |
| 243 | ||
| 244 | t.pass() | |
| 245 | }) | |
| 246 | ``` | |
| 247 | ||
| 248 | Subtest test options can be set by passing an object to the `test` function: | |
| 249 | ||
| 250 | ```js | |
| 251 | test('parent', { timeout: 1000 }, function (t) { | |
| 252 | t.test('basic using parent config', async function (t) { | |
| 253 | await new Promise(r => setTimeout(r, 500)) | |
| 254 | t.pass() | |
| 255 | }) | |
| 256 | ||
| 257 | t.test('another basic using parent config', function (t) { | |
| 258 | t.pass() | |
| 259 | }) | |
| 260 | }) | |
| 261 | ``` | |
| 262 | ||
| 263 | You can also await for its result as well: | |
| 264 | ||
| 265 | ```js | |
| 266 | test('basic', async function (t) { | |
| 267 | t.plan(1) | |
| 268 | t.pass() | |
| 269 | const isOk = await t | |
| 270 | console.log(isOk) | |
| 271 | }) | |
| 272 | ``` | |
| 273 | ||
| 274 | #### `t.stealth([name], [options], callback)` | |
| 275 | #### `t.stealth([name], [options]) => assert` | |
| 276 | ||
| 277 | Create a stealth sub-test.\ | |
| 278 | This will provide a new sub-assert object that only prints the test summary without assertions and ends the current test upon a failed assertion. | |
| 279 | ||
| 280 | All `options` are the same as `test` which are [listed here](#available-options-for-any-test-creation). | |
| 281 | ||
| 282 | #### `solo([name], [options], callback)` | |
| 283 | #### `solo([name], [options]) => assert` | |
| 284 | ||
| 285 | Filter out other tests by using the `solo` method: | |
| 286 | ||
| 287 | ```js | |
| 288 | import { test, solo } from 'brittle' | |
| 289 | ||
| 290 | test('this test is skipped', function (t) { | |
| 291 | t.pass() | |
| 292 | }) | |
| 293 | ||
| 294 | solo('some test', function (t) { | |
| 295 | t.pass() | |
| 296 | }) | |
| 297 | ``` | |
| 298 | ||
| 299 | If a `solo` function is used, `test` functions will not execute.\ | |
| 300 | ||
| 301 | If `solo` is used in a future tick (for example, in a `setTimeout` callback),\ | |
| 302 | after `test` has already been used those tests won't be filtered. | |
| 303 | ||
| 304 | A few ways to enable `solo` functions: | |
| 305 | - Use `configure({ solo: true })` before any tests. | |
| 306 | - You can call `solo()` without callback underneath the imports. | |
| 307 | - Using the `--solo` flag with the `brittle` test runner. | |
| 308 | ||
| 309 | It can also be used as an inverted test: | |
| 310 | ||
| 311 | ```js | |
| 312 | const t = test.solo('inverted some test') | |
| 313 | t.pass() | |
| 314 | t.end() | |
| 315 | ``` | |
| 316 | ||
| 317 | #### `skip([name], [options], callback)` | |
| 318 | ||
| 319 | Skip a test: | |
| 320 | ||
| 321 | ```js | |
| 322 | import { test, skip } from 'brittle' | |
| 323 | ||
| 324 | skip('this test is skipped', function (t) { | |
| 325 | t.pass() | |
| 326 | }) | |
| 327 | ||
| 328 | test('middle test', function (t) { | |
| 329 | t.pass() | |
| 330 | }) | |
| 331 | ||
| 332 | test.skip('another skipped test', function (t) { | |
| 333 | t.pass() | |
| 334 | }) | |
| 335 | ``` | |
| 336 | ||
| 337 | Only the `middle test` will be executed. | |
| 338 | ||
| 339 | #### `hook([name], [options], callback)` | |
| 340 | ||
| 341 | Use before tests for setting up and after tests for tearing down. Runs the same way as `test` except always executes regardless of `solo` usage. | |
| 342 | ||
| 343 | ```js | |
| 344 | import { test, solo, hook } from 'brittle' | |
| 345 | ||
| 346 | hook('setup hook', function (t) { | |
| 347 | // setup resources | |
| 348 | }) | |
| 349 | ||
| 350 | solo('solo test', function (t) { | |
| 351 | t.pass() | |
| 352 | }) | |
| 353 | ||
| 354 | test('middle test', function (t) { | |
| 355 | t.pass() | |
| 356 | }) | |
| 357 | ||
| 358 | hook('teardown hook', function (t) { | |
| 359 | // teardown resources | |
| 360 | }) | |
| 361 | ``` | |
| 362 | ||
| 363 | The `setup hook`, `solo test` and `teardown hook` will be executed. | |
| 364 | ||
| 365 | #### `configure([options])` | |
| 366 | ||
| 367 | The `configure` function can be used to set options for all tests (including child tests).\ | |
| 368 | It must be executed before any tests. | |
| 369 | ||
| 370 | #### Options | |
| 371 | ||
| 372 | * `timeout` (`30000`) - milliseconds to wait before ending a stalling test | |
| 373 | * `bail` (`false`) - exit the process on first test failure | |
| 374 | * `solo` (`false`) - skip all other tests except the `solo()` ones | |
| 375 | * `source` (`true`) - shows error `source` information | |
| 376 | * `unstealth` (`false`) - show assertions even if `stealth` is used | |
| 377 | * `coverage` (`false`) - enable coverage reporting (string path of the output directory or `true` for default) | |
| 378 | ||
| 379 | ```js | |
| 380 | import { configure } from 'brittle' | |
| 381 | ||
| 382 | configure({ timeout: 15000 }) // All tests will have a 15 seconds timeout | |
| 383 | ``` | |
| 384 | ||
| 385 | ### Assertions | |
| 386 | ||
| 387 | #### `t.is(actual, expected, [message])` | |
| 388 | ||
| 389 | Compare `actual` to `expected` with `===` | |
| 390 | ||
| 391 | #### `t.not(actual, expected, [message])` | |
| 392 | ||
| 393 | Compare `actual` to `expected` with `!==` | |
| 394 | ||
| 395 | #### `t.alike(actual, expected, [message])` | |
| 396 | ||
| 397 | Object comparison, comparing all primitives on the | |
| 398 | `actual` object to those on the `expected` object | |
| 399 | using `===`. | |
| 400 | ||
| 401 | #### `t.unlike(actual, expected, [message])` | |
| 402 | ||
| 403 | Object comparison, comparing all primitives on the | |
| 404 | `actual` object to those on the `expected` object | |
| 405 | using `!==`. | |
| 406 | ||
| 407 | #### `t.ok(value, [message])` | |
| 408 | ||
| 409 | Checks that `value` is truthy: `!!value === true` | |
| 410 | ||
| 411 | #### `t.absent(value, [message])` | |
| 412 | ||
| 413 | Checks that `value` is falsy: `!!value === false` | |
| 414 | ||
| 415 | #### `t.pass([message])` | |
| 416 | ||
| 417 | Asserts success. Useful for explicitly confirming | |
| 418 | that a function was called, or that behavior is | |
| 419 | as expected. | |
| 420 | ||
| 421 | #### `t.fail([message])` | |
| 422 | ||
| 423 | Asserts failure. Useful for explicitly checking | |
| 424 | that a function should not be called. | |
| 425 | ||
| 426 | #### `t.exception(Promise|function|async function, [error], [message])` | |
| 427 | ||
| 428 | Verify that a function throws, or a promise rejects. | |
| 429 | ||
| 430 | ```js | |
| 431 | t.exception(() => { throw Error('an err') }, /an err/) | |
| 432 | await t.exception(async () => { throw Error('an err') }, /an err/) | |
| 433 | await t.exception(Promise.reject(Error('an err')), /an err/) | |
| 434 | ``` | |
| 435 | ||
| 436 | If the error is an instance of any of the following native error constructors, | |
| 437 | then this will still result in failure since native errors often tend to be unintentational. | |
| 438 | ||
| 439 | * `SyntaxError` | |
| 440 | * `ReferenceError` | |
| 441 | * `TypeError` | |
| 442 | * `EvalError` | |
| 443 | * `RangeError` | |
| 444 | ||
| 445 | If a `t.exception` is async, then you're supposed to await it. | |
| 446 | ||
| 447 | #### `t.exception.all(Promise|function|async function, [error], [message])` | |
| 448 | ||
| 449 | Verify that a function throws, or a promise rejects, including native errors. | |
| 450 | ||
| 451 | ```js | |
| 452 | t.exception.all(() => { throw Error('an err') }, /an err/) | |
| 453 | await t.exception.all(async () => { throw Error('an err') }, /an err/) | |
| 454 | await t.exception.all(Promise.reject(new SyntaxError('native error')), /native error/) | |
| 455 | ``` | |
| 456 | ||
| 457 | The `t.exception.all` method is an escape-hatch so it can be used with the | |
| 458 | normally filtered native errors. | |
| 459 | ||
| 460 | If a `t.exception.all` is async, then you're supposed to await it. | |
| 461 | ||
| 462 | #### `t.execution(Promise|function|async function, [message])` | |
| 463 | ||
| 464 | Assert that a function executes instead of throwing or that a promise resolves instead of rejecting. Resolves to the execution time, in milliseconds, of the function or promise. | |
| 465 | ||
| 466 | ```js | |
| 467 | t.execution(() => {}) | |
| 468 | await t.execution(async () => {}) | |
| 469 | await t.execution(Promise.resolve('cool')) | |
| 470 | ``` | |
| 471 | ||
| 472 | If a `t.execution` is async, then you're supposed to await it | |
| 473 | ||
| 474 | #### `t.is.coercively(actual, expected, [message])` | |
| 475 | ||
| 476 | Compare `actual` to `expected` with `==`. | |
| 477 | ||
| 478 | #### `t.not.coercively(actual, expected, [message])` | |
| 479 | ||
| 480 | Compare `actual` to `expected` with `!=`. | |
| 481 | ||
| 482 | #### `t.alike.coercively(actual, expected, [message])` | |
| 483 | ||
| 484 | Object comparison, comparing all primitives on the | |
| 485 | `actual` object to those on the `expected` object | |
| 486 | using `==`. | |
| 487 | ||
| 488 | #### `t.unlike.coercively(actual, expected, [message])` | |
| 489 | ||
| 490 | Object comparison, comparing all primitives on the | |
| 491 | `actual` object to those on the `expected` object | |
| 492 | using `!=`. | |
| 493 | ||
| 494 | ||
| 495 | ### Utilities | |
| 496 | ||
| 497 | #### `t.plan(n)` | |
| 498 | ||
| 499 | Constrain a test to an explicit amount of assertions. | |
| 500 | ||
| 501 | #### `t.tmp() -> <Promise<String>>` | |
| 502 | ||
| 503 | Creates a temporary folder and returns a promise that resolves its path. Once a test either succeeds or fails, the temporary folder is removed. | |
| 504 | ||
| 505 | #### `t.teardown(function|async function, [options])` | |
| 506 | ||
| 507 | **Options:** | |
| 508 | ||
| 509 | * `order` (`0`) - set the ascending position priority for a teardown to be executed. | |
| 510 | * `force` (`false`) - run the teardown on failure as well as success | |
| 511 | ||
| 512 | The function passed to `teardown` is called right after a test ends: | |
| 513 | ||
| 514 | ```js | |
| 515 | test('basic', function (t) { | |
| 516 | const timeoutId = setTimeout(() => {}, 1000) | |
| 517 | ||
| 518 | t.teardown(async function () { | |
| 519 | clearTimeout(timeoutId) | |
| 520 | await doMoreCleanUp() | |
| 521 | }) | |
| 522 | ||
| 523 | t.ok('cool') | |
| 524 | }) | |
| 525 | ``` | |
| 526 | ||
| 527 | If `teardown` is called multiple times in a test, every function passed will be called after the test ends: | |
| 528 | ||
| 529 | ```js | |
| 530 | test('basic', function (t) { | |
| 531 | t.teardown(doSomeCleanUp) | |
| 532 | ||
| 533 | const timeoutId = setTimeout(() => {}, 1000) | |
| 534 | t.teardown(() => clearTimeout(timeoutId)) | |
| 535 | ||
| 536 | t.ok('again, cool') | |
| 537 | }) | |
| 538 | ``` | |
| 539 | ||
| 540 | Set `order: -Infinity` to always be in first place, and vice versa with `order: Infinity`.\ | |
| 541 | If two teardowns have the same `order` they are ordered per time of invocation within that order group. | |
| 542 | ||
| 543 | ```js | |
| 544 | test('teardown order', function (t) { | |
| 545 | t.teardown(async function () { | |
| 546 | await new Promise(r => setTimeout(r, 200)) | |
| 547 | console.log('teardown B') | |
| 548 | }) | |
| 549 | ||
| 550 | t.teardown(async function () { | |
| 551 | await new Promise(r => setTimeout(r, 200)) | |
| 552 | console.log('teardown A') | |
| 553 | }, { order: -1 }) | |
| 554 | ||
| 555 | t.teardown(async function () { | |
| 556 | await new Promise(r => setTimeout(r, 200)) | |
| 557 | console.log('teardown C') | |
| 558 | }, { order: 1 }) | |
| 559 | ||
| 560 | t.pass() | |
| 561 | }) | |
| 562 | ``` | |
| 563 | ||
| 564 | The `A` teardown is executed first, then `B`, and finally `C` due to the `order` option. | |
| 565 | ||
| 566 | #### `t.timeout(ms)` | |
| 567 | ||
| 568 | Fail the test after a given timeout. | |
| 569 | ||
| 570 | #### `t.comment(message)` | |
| 571 | ||
| 572 | Inject a TAP comment into the output. | |
| 573 | ||
| 574 | #### `t.end()` | |
| 575 | ||
| 576 | Force end a test.\ | |
| 577 | `end` is determined by `assert` resolution or when a containing async function completes.\ | |
| 578 | In case of inverted tests, they're required to be explicitly called. | |
| 579 | ||
| 580 | ||
| 581 | ### Readable Properties | |
| 582 | ||
| 583 | #### `t.name` | |
| 584 | The name of the test. | |
| 585 | ||
| 586 | #### `t.passes` | |
| 587 | The number of assertions that passed within the test. | |
| 588 | ||
| 589 | #### `t.fails` | |
| 590 | The number of assertions that failed within the test. | |
| 591 | ||
| 592 | #### `t.assertions` | |
| 593 | The number of assertions that were executed within the test. | |
| 594 | ||
| 595 | ## Runner | |
| 596 | ||
| 597 | ### Default timeout | |
| 598 | ||
| 599 | The default timeout is 30 seconds. | |
| 600 | ||
| 601 | ### Example of `package.json` with `test` script | |
| 602 | ||
| 603 | The following would run all `.js` files in the test folder: | |
| 604 | ||
| 605 | ```json | |
| 606 | { | |
| 607 | "name": "my-app", | |
| 608 | "version": "1.0.0", | |
| 609 | "scripts": { | |
| 610 | "test": "brittle test/*.js" | |
| 611 | }, | |
| 612 | "devDependencies": { | |
| 613 | "brittle": "^3.0.0-alpha.3" | |
| 614 | } | |
| 615 | } | |
| 616 | ``` | |
| 617 | ||
| 618 | ## CLI | |
| 619 | ||
| 620 | ```sh | |
| 621 | npm install -g brittle | |
| 622 | ``` | |
| 623 | ||
| 624 | ```shell | |
| 625 | brittle [flags] <files> | |
| 626 | ||
| 627 | Flags: | |
| 628 | --version, -v Print the current version | |
| 629 | --solo, -s Engage solo mode | |
| 630 | --bail, -b Bail out on first assert failure | |
| 631 | --coverage, -c Turn on coverage | |
| 632 | --cov-dir <dir> Configure coverage output directory (default: ./coverage) | |
| 633 | --trace Trace all active promises and print them if the test fails | |
| 634 | --timeout, -t <timeout> Set the test timeout in milliseconds (default: 30000) | |
| 635 | --runner, -r <runner> Generates an out file that contains all target tests | |
| 636 | --mine, -m <miners> Keep running the tests in <miners> processes until they fail. | |
| 637 | --unstealth, -u Show assertions even if stealth is used | |
| 638 | --help|-h Show help | |
| 639 | ``` | |
| 640 | ||
| 641 | Note globbing is supported: | |
| 642 | ```sh | |
| 643 | brittle --coverage path/to/test/*.js | |
| 644 | ``` | |
| 645 | ||
| 646 | Auto generate a single file containing "all tests": | |
| 647 | ```shell | |
| 648 | brittle -r test/all.js test/*.js | |
| 649 | ||
| 650 | node test/all.js | |
| 651 | ``` | |
| 652 | ||
| 653 | You can use an environment variable to also set flags: | |
| 654 | ```shell | |
| 655 | BRITTLE="--coverage --bail" brittle test.js | |
| 656 | ``` | |
| 657 | ||
| 658 | Force disable coverage with an environment variable: | |
| 659 | ```shell | |
| 660 | BRITTLE_COVERAGE=false brittle test.js | |
| 661 | ``` | |
| 662 | ### Coverage | |
| 663 | If the `--coverage` flag is set, brittle will output the coverage summary as a table at the end of execution and generate a json coverage report in the coverage output directory (configurable using `--cov-dir`). | |
| 664 | ||
| 665 | The coverage output directory will contain a `coverage-final.json` file which contains an istanbul json coverage report and a `v8-coverage.json` file which contains the raw v8 coverage data. | |
| 666 | ||
| 667 | Istanbul can be used to convert the istanbul json report into other formats. e.g.: | |
| 668 | ``` | |
| 669 | npx istanbul report html | |
| 670 | ``` | |
| 671 | ||
| 672 | ## License | |
| 673 | Apache-2.0 |
| … | … | @@ -0,0 +1,8 @@ |
| 1 | #!/usr/bin/env bare | |
| 2 | ||
| 3 | if (!global.Bare && global.process) { | |
| 4 | const { status } = require('child_process').spawnSync('bare', process.argv.slice(1), { stdio: 'inherit', shell: true }) | |
| 5 | process.exit(status) | |
| 6 | } | |
| 7 | ||
| 8 | require('../cmd.js') |
| … | … | @@ -0,0 +1,3 @@ |
| 1 | #!/usr/bin/env node | |
| 2 | ||
| 3 | require('../cmd.js') |
| … | … | @@ -0,0 +1,11 @@ |
| 1 | #!/usr/bin/env pear | |
| 2 | ||
| 3 | /* global Pear */ | |
| 4 | if (!global.Pear && global.process) { | |
| 5 | const { status } = require('child_process').spawnSync('pear', process.argv.slice(1), { stdio: 'inherit', shell: true }) | |
| 6 | process.exit(status) | |
| 7 | } | |
| 8 | ||
| 9 | if (!Pear.run) throw new Error('Incompatible with current Pear version (must be v2+)') | |
| 10 | ||
| 11 | require('../brittle/cmd.js') |
| … | … | @@ -0,0 +1,251 @@ |
| 1 | #!/usr/bin/env node | |
| 2 | ||
| 3 | const path = require('path') | |
| 4 | const os = require('os') | |
| 5 | const { command, flag, rest } = require('paparam') | |
| 6 | const Globbie = require('globbie') | |
| 7 | const { spawn } = require('child_process') | |
| 8 | const process = require('process') | |
| 9 | const TracingPromise = require('./lib/tracing-promise') | |
| 10 | const pkg = require('./package') | |
| 11 | ||
| 12 | const args = (process.env.BRITTLE || '').split(/\s|,/g).map(s => s.trim()).filter(s => s).concat(process.argv.slice(2)) | |
| 13 | const cmd = command('brittle', | |
| 14 | flag('--version|-v', 'Print the current version'), | |
| 15 | flag('--solo, -s', 'Engage solo mode'), | |
| 16 | flag('--bail, -b', 'Bail out on first assert failure'), | |
| 17 | flag('--coverage, -c', 'Turn on coverage'), | |
| 18 | flag('--cov-dir <dir>', 'Configure coverage output directory (default: ./coverage)'), | |
| 19 | flag('--trace', 'Trace all active promises and print them if the test fails'), | |
| 20 | flag('--timeout, -t <timeout>', 'Set the test timeout in milliseconds (default: 30000)'), | |
| 21 | flag('--runner, -r <runner>', 'Generates an out file that contains all target tests'), | |
| 22 | flag('--mine, -m <miners>', 'Keep running the tests in <miners> processes until they fail.'), | |
| 23 | flag('--unstealth, -u', 'Print out assertions even if stealth is used'), | |
| 24 | rest('<files>') | |
| 25 | ).parse(args) | |
| 26 | if (!cmd) process.exit(0) | |
| 27 | ||
| 28 | const { version } = cmd.flags | |
| 29 | if (version) { | |
| 30 | console.log(`v${pkg.version}`) | |
| 31 | process.exit(0) | |
| 32 | } | |
| 33 | ||
| 34 | const argv = cmd.flags | |
| 35 | ||
| 36 | const files = [] | |
| 37 | for (const g of cmd.rest || []) { | |
| 38 | const glob = new Globbie(g, { sync: true }) | |
| 39 | const matches = glob.match() | |
| 40 | ||
| 41 | if (matches.length === 0) { | |
| 42 | if (g[0] === '-') continue | |
| 43 | console.error(`Error: no files found when resolving ${g}`) | |
| 44 | process.exit(1) | |
| 45 | } | |
| 46 | ||
| 47 | files.push(...matches) | |
| 48 | } | |
| 49 | ||
| 50 | if (files.length === 0) { | |
| 51 | console.error('Error: No test files were specified') | |
| 52 | process.exit(1) | |
| 53 | } | |
| 54 | ||
| 55 | const { solo, bail, timeout, coverage, covDir, mine, trace, unstealth } = argv | |
| 56 | ||
| 57 | process.title = 'brittle' | |
| 58 | ||
| 59 | if (trace && !mine) { | |
| 60 | TracingPromise.enable() | |
| 61 | process.on('exit', function (code, signal) { | |
| 62 | if (!code && !signal) return | |
| 63 | console.error() | |
| 64 | console.error('Printing tracing info since the tests failed:') | |
| 65 | console.error() | |
| 66 | TracingPromise.print() | |
| 67 | }) | |
| 68 | } | |
| 69 | ||
| 70 | if (argv.runner) { | |
| 71 | const fs = require('fs') | |
| 72 | ||
| 73 | if (argv.runner === true) { | |
| 74 | console.error('--runner must be a path to the generated test runner') | |
| 75 | process.exit(2) | |
| 76 | } | |
| 77 | ||
| 78 | const out = path.resolve(argv.runner) | |
| 79 | const dir = path.dirname(out) | |
| 80 | ||
| 81 | let s = '' | |
| 82 | ||
| 83 | s += 'runTests()\n\nasync function runTests () {\n const test = (await import(\'brittle\')).default\n\n' | |
| 84 | ||
| 85 | if (bail || solo || unstealth || timeout) { | |
| 86 | s += ` test.configure({ bail: ${!!bail}, solo: ${!!solo}, unstealth: ${!!unstealth}, timeout: ${timeout} })\n` | |
| 87 | } | |
| 88 | ||
| 89 | s += ' test.pause()\n\n' | |
| 90 | ||
| 91 | for (const f of files) { | |
| 92 | const t = path.resolve(f) | |
| 93 | if (t === out) continue | |
| 94 | ||
| 95 | let r = path.relative(dir, t).replace(/\\/g, '/') | |
| 96 | if (r[0] !== '.') r = './' + r | |
| 97 | s += ' await import(\'' + r + '\')\n' | |
| 98 | } | |
| 99 | ||
| 100 | s = s.trimRight() | |
| 101 | ||
| 102 | s += '\n\n test.resume()\n}\n' | |
| 103 | s = '// This runner is auto-generated by Brittle\n\n' + s | |
| 104 | ||
| 105 | try { | |
| 106 | fs.mkdirSync(dir) | |
| 107 | } catch {} | |
| 108 | ||
| 109 | fs.writeFileSync(out, s) | |
| 110 | process.exit(0) | |
| 111 | } | |
| 112 | ||
| 113 | if (coverage && process.env.BRITTLE_COVERAGE !== 'false') require('bare-cov')({ dir: covDir }) | |
| 114 | ||
| 115 | if (mine) startMining().catch() | |
| 116 | else start().catch(onerror) | |
| 117 | ||
| 118 | function onerror (err) { | |
| 119 | console.error(err.stack) | |
| 120 | process.exit(1) | |
| 121 | } | |
| 122 | ||
| 123 | async function start () { | |
| 124 | const brittle = require('./') | |
| 125 | ||
| 126 | if (bail || solo || unstealth || timeout) { | |
| 127 | brittle.configure({ bail, solo, unstealth, timeout: timeout ? Number(timeout) : undefined }) | |
| 128 | } | |
| 129 | ||
| 130 | brittle.pause() | |
| 131 | ||
| 132 | for (const f of files) { | |
| 133 | await import('file://' + path.resolve(f)) | |
| 134 | } | |
| 135 | ||
| 136 | brittle.resume() | |
| 137 | } | |
| 138 | ||
| 139 | async function startMining () { | |
| 140 | const args = [__filename] | |
| 141 | .concat(solo ? ['--solo'] : []) | |
| 142 | .concat(bail ? ['--bail'] : []) | |
| 143 | .concat(unstealth ? ['--unstealth'] : []) | |
| 144 | .concat(trace ? ['--trace'] : []) | |
| 145 | .concat(timeout ? ['--timeout', timeout + ''] : []) | |
| 146 | .concat(files) | |
| 147 | ||
| 148 | const running = new Set() | |
| 149 | const max = Number(argv.mine) || 1 | |
| 150 | ||
| 151 | let runs = 0 | |
| 152 | let bailed = false | |
| 153 | let newline = false | |
| 154 | ||
| 155 | const interval = setInterval(function () { | |
| 156 | console.log('Still mining... Total runs: ' + runs) | |
| 157 | newline = true | |
| 158 | }, 1000) | |
| 159 | ||
| 160 | bump() | |
| 161 | ||
| 162 | process.once('SIGINT', bail) | |
| 163 | process.once('SIGTERM', bail) | |
| 164 | ||
| 165 | function bail () { | |
| 166 | bailed = true | |
| 167 | clearInterval(interval) | |
| 168 | for (const r of running) r.kill() | |
| 169 | } | |
| 170 | ||
| 171 | async function bump () { | |
| 172 | if (running.size >= max || bailed) return | |
| 173 | ||
| 174 | const r = run() | |
| 175 | running.add(r) | |
| 176 | ||
| 177 | const { exitCode, signalCode, output } = await r.promise | |
| 178 | running.delete(r) | |
| 179 | runs++ | |
| 180 | ||
| 181 | if (bailed) return | |
| 182 | ||
| 183 | if (!exitCode && !signalCode) { | |
| 184 | bump() | |
| 185 | bump() | |
| 186 | return | |
| 187 | } | |
| 188 | ||
| 189 | bailed = true | |
| 190 | ||
| 191 | clearInterval(interval) | |
| 192 | ||
| 193 | if (newline) console.log() | |
| 194 | if (exitCode) console.log('Runner failed with exit code ' + exitCode + '!') | |
| 195 | else console.log('Runner failed with signal code ' + signalCode + ' (' + signalToName(signalCode) + ')!') | |
| 196 | ||
| 197 | console.log('Shutting down the rest and printing output...') | |
| 198 | ||
| 199 | for (const r of running) { | |
| 200 | r.kill() | |
| 201 | await r.promise | |
| 202 | } | |
| 203 | ||
| 204 | console.log('Done! The tests took ' + runs + ' runs to fail.') | |
| 205 | console.log() | |
| 206 | ||
| 207 | for (const { stdout, data } of output) { | |
| 208 | if (stdout) process.stdout.write(data) | |
| 209 | else process.stderr.write(data) | |
| 210 | } | |
| 211 | ||
| 212 | clearInterval(interval) | |
| 213 | process.exitCode = exitCode || 1 | |
| 214 | } | |
| 215 | ||
| 216 | function run () { | |
| 217 | const p = spawn(process.execPath, args) | |
| 218 | ||
| 219 | const output = [] | |
| 220 | ||
| 221 | p.stdout.on('data', (data) => output.push({ stdout: true, data })) | |
| 222 | p.stderr.on('data', (data) => output.push({ stdout: false, data })) | |
| 223 | ||
| 224 | const stdoutClosed = new Promise(resolve => p.stdout.on('close', resolve)) | |
| 225 | const stderrClosed = new Promise(resolve => p.stderr.on('close', resolve)) | |
| 226 | ||
| 227 | const promise = new Promise((resolve) => { | |
| 228 | p.on('exit', async (exitCode, signalCode) => { | |
| 229 | await stdoutClosed | |
| 230 | await stderrClosed | |
| 231 | resolve({ | |
| 232 | exitCode, | |
| 233 | signalCode, | |
| 234 | output | |
| 235 | }) | |
| 236 | }) | |
| 237 | }) | |
| 238 | ||
| 239 | return { | |
| 240 | promise, | |
| 241 | kill: () => p.kill() | |
| 242 | } | |
| 243 | } | |
| 244 | } | |
| 245 | ||
| 246 | function signalToName (code) { | |
| 247 | for (const [k, v] of Object.entries(os.constants.signals)) { | |
| 248 | if (v === code) return k | |
| 249 | } | |
| 250 | return null | |
| 251 | } |
| … | … | @@ -0,0 +1,885 @@ |
| 1 | const sameObject = require('same-object') | |
| 2 | const tmp = require('test-tmp') | |
| 3 | const b4a = require('b4a') | |
| 4 | const { getSnapshot, createTypedArray } = require('./lib/snapshot') | |
| 5 | const { INDENT, RUNNER, IS_NODE, IS_BARE, DEFAULT_TIMEOUT } = require('./lib/constants') | |
| 6 | const AssertionError = require('./lib/assertion-error') | |
| 7 | const TracingPromise = require('./lib/tracing-promise') | |
| 8 | const Promise = TracingPromise.Untraced // never trace internal onces | |
| 9 | ||
| 10 | const highDefTimer = IS_NODE ? highDefTimerNode : highDefTimerFallback | |
| 11 | const program = IS_NODE ? process : global.Bare | |
| 12 | ||
| 13 | // loaded on demand since it's error flow and we want ultra fast positive test runs | |
| 14 | const lazy = { | |
| 15 | _errors: null, | |
| 16 | _tmatch: null, | |
| 17 | get errors () { | |
| 18 | if (!lazy._errors) lazy._errors = require('./lib/errors.js') | |
| 19 | return lazy._errors | |
| 20 | }, | |
| 21 | get tmatch () { | |
| 22 | if (!lazy._tmatch) lazy._tmatch = require('tmatch') | |
| 23 | return lazy._tmatch | |
| 24 | } | |
| 25 | } | |
| 26 | ||
| 27 | class Runner { | |
| 28 | constructor () { | |
| 29 | this.tests = { count: 0, pass: 0 } | |
| 30 | this.assertions = { count: 0, pass: 0 } | |
| 31 | ||
| 32 | this.next = null | |
| 33 | this.solos = new Set() | |
| 34 | this.padded = true | |
| 35 | this.started = false | |
| 36 | this.defaultTimeout = DEFAULT_TIMEOUT | |
| 37 | this.bail = false | |
| 38 | this.unstealth = false | |
| 39 | this.skipAll = false | |
| 40 | this.explicitSolo = false | |
| 41 | this.source = true | |
| 42 | ||
| 43 | this._timer = highDefTimer() | |
| 44 | this._log = console.log.bind(console) | |
| 45 | this._paused = null | |
| 46 | this._resume = null | |
| 47 | ||
| 48 | const ondeadlock = () => { | |
| 49 | if (this.next && this.next._checkDeadlock === false) return | |
| 50 | program.off('beforeExit', ondeadlock) | |
| 51 | this.end() | |
| 52 | } | |
| 53 | ||
| 54 | program.on('beforeExit', ondeadlock) | |
| 55 | } | |
| 56 | ||
| 57 | resume () { | |
| 58 | if (!this._paused) return | |
| 59 | this._resume() | |
| 60 | this._resume = this._paused = null | |
| 61 | } | |
| 62 | ||
| 63 | pause () { | |
| 64 | if (this._paused) return | |
| 65 | this._paused = new Promise((resolve) => { this._resume = resolve }) | |
| 66 | } | |
| 67 | ||
| 68 | async _wait () { | |
| 69 | await wait() | |
| 70 | await this._paused | |
| 71 | } | |
| 72 | ||
| 73 | async queue (test) { | |
| 74 | this.start() | |
| 75 | ||
| 76 | if (test._isSolo) { | |
| 77 | this.solos.add(test) | |
| 78 | } | |
| 79 | ||
| 80 | await this._wait() | |
| 81 | ||
| 82 | if (this.explicitSolo && !test._isSolo) { | |
| 83 | return false | |
| 84 | } | |
| 85 | ||
| 86 | if (this._shouldTest(test)) { | |
| 87 | while (this.next !== null) { | |
| 88 | const next = this.next | |
| 89 | await next | |
| 90 | if (next === this.next) this.next = null | |
| 91 | } | |
| 92 | ||
| 93 | if (test._isSkip) { | |
| 94 | this._skip('SKIP', test) | |
| 95 | return false | |
| 96 | } | |
| 97 | ||
| 98 | if (test._isTodo) { | |
| 99 | this._skip('TODO', test) | |
| 100 | return false | |
| 101 | } | |
| 102 | ||
| 103 | if (!this._shouldTest(test)) { | |
| 104 | return false | |
| 105 | } | |
| 106 | ||
| 107 | this.next = test | |
| 108 | test._header() | |
| 109 | ||
| 110 | if (!IS_NODE && !IS_BARE) this._autoExit(test) | |
| 111 | ||
| 112 | return true | |
| 113 | } | |
| 114 | ||
| 115 | return false | |
| 116 | } | |
| 117 | ||
| 118 | _skip (reason, test) { | |
| 119 | if (this._shouldTest(test)) { | |
| 120 | test._header() | |
| 121 | this.tests.pass++ | |
| 122 | this.tests.count++ | |
| 123 | this.assert(false, true, this.tests.count, '- ' + test.name + ' # ' + reason, null) | |
| 124 | } | |
| 125 | } | |
| 126 | ||
| 127 | _shouldTest (test) { | |
| 128 | return test._isHook || (!this.skipAll && (this.solos.size === 0 || this.solos.has(test))) | |
| 129 | } | |
| 130 | ||
| 131 | async _autoExit (test) { | |
| 132 | try { | |
| 133 | await test | |
| 134 | await wait(10) // wait 10 ticks... | |
| 135 | if (this.next === test) { | |
| 136 | this.end() | |
| 137 | } | |
| 138 | } catch {} | |
| 139 | } | |
| 140 | ||
| 141 | log (...message) { | |
| 142 | this._log(...message) | |
| 143 | this.padded = false | |
| 144 | } | |
| 145 | ||
| 146 | padding () { | |
| 147 | if (this.padded) return | |
| 148 | this.padded = true | |
| 149 | this.log() | |
| 150 | } | |
| 151 | ||
| 152 | start () { | |
| 153 | if (this.started) return | |
| 154 | this.started = true | |
| 155 | this.log('TAP version 13') | |
| 156 | } | |
| 157 | ||
| 158 | comment (...message) { | |
| 159 | this.log('#', ...message) | |
| 160 | } | |
| 161 | ||
| 162 | end () { | |
| 163 | if (this.next) { | |
| 164 | if (!this.next._isEnded && !(this.next._hasPlan && this.next._planned === 0)) { | |
| 165 | this.next._onend(prematureEnd(this.next, 'Test did not end (' + this.next.name + ')')) | |
| 166 | return | |
| 167 | } | |
| 168 | ||
| 169 | if (!this.next._isResolved) { | |
| 170 | if (this.next._isDone) { | |
| 171 | this.next._onend(new Error('Teardown did not end (unresolved promise)')) | |
| 172 | return | |
| 173 | } | |
| 174 | this.next._onend(new Error('Test appears deadlocked (unresolved promise)')) | |
| 175 | return | |
| 176 | } | |
| 177 | } | |
| 178 | ||
| 179 | if (this.bail && this.skipAll) { | |
| 180 | this.log('Bail out!') | |
| 181 | } | |
| 182 | ||
| 183 | this.padding() | |
| 184 | this.log('1..' + this.tests.count) | |
| 185 | this.log('# tests = ' + this.tests.pass + '/' + this.tests.count + ' pass') | |
| 186 | this.log('# asserts = ' + this.assertions.pass + '/' + this.assertions.count + ' pass') | |
| 187 | this.log('# time = ' + this._timer() + 'ms') | |
| 188 | this.log() | |
| 189 | ||
| 190 | if (this.tests.count === this.tests.pass && this.assertions.count === this.assertions.pass) this.log('# ok') | |
| 191 | else this.log('# not ok') | |
| 192 | } | |
| 193 | ||
| 194 | assert (indent, ok, number, message, explanation, stealth) { | |
| 195 | const ind = indent ? INDENT : '' | |
| 196 | ||
| 197 | if (ok) { | |
| 198 | if (!stealth || this.unstealth) this.log(ind + 'ok ' + number, message) | |
| 199 | } else { | |
| 200 | if (IS_NODE) process.exitCode = 1 | |
| 201 | if (IS_BARE) global.Bare.exitCode = 1 | |
| 202 | this.log(ind + 'not ok ' + number, message) | |
| 203 | if (explanation) this.log(lazy.errors.stringify(explanation)) | |
| 204 | if (this.bail && !this.skipAll) this.skipAll = true | |
| 205 | if (!this.unstealth && stealth) throw new AssertionError({ message: 'Stealth assertion failed' }) | |
| 206 | } | |
| 207 | } | |
| 208 | } | |
| 209 | ||
| 210 | class Test { | |
| 211 | constructor (name, parent, opts = {}) { | |
| 212 | this._resolve = null | |
| 213 | this._reject = null | |
| 214 | ||
| 215 | this._promise = new Promise((resolve, reject) => { | |
| 216 | this._resolve = resolve | |
| 217 | this._reject = reject | |
| 218 | }) | |
| 219 | ||
| 220 | this._parents = [] | |
| 221 | this._main = parent ? parent._main : this | |
| 222 | this._runner = getRunner() | |
| 223 | this.name = name | |
| 224 | this.passes = 0 | |
| 225 | this.fails = 0 | |
| 226 | this.assertions = 0 | |
| 227 | ||
| 228 | this._isEnded = false | |
| 229 | this._isDone = false | |
| 230 | this._isHook = opts?.hook || false | |
| 231 | this._isSolo = opts?.solo || false | |
| 232 | this._isSkip = opts?.skip || false | |
| 233 | this._isTodo = opts?.todo || false | |
| 234 | this._isResolved = false | |
| 235 | this._isQueued = false | |
| 236 | this._isMain = this._main === this | |
| 237 | this._isStealth = opts?.stealth || parent?._isStealth || false | |
| 238 | this._checkDeadlock = opts?.deadlock !== false | |
| 239 | ||
| 240 | // allow destructuring by binding the functions | |
| 241 | this.comment = this._comment.bind(this) | |
| 242 | this.timeout = this._timeout.bind(this) | |
| 243 | this.teardown = this._teardown.bind(this) | |
| 244 | this.test = this._test.bind(this) | |
| 245 | this.plan = this._plan.bind(this) | |
| 246 | ||
| 247 | this.pass = this._pass.bind(this) | |
| 248 | this.fail = this._fail.bind(this) | |
| 249 | ||
| 250 | this.ok = this._ok.bind(this) | |
| 251 | this.absent = this._absent.bind(this) | |
| 252 | ||
| 253 | this.is = this._is.bind(this, true) | |
| 254 | this.is.coercively = this._is.bind(this, false) | |
| 255 | ||
| 256 | this.not = this._not.bind(this, true) | |
| 257 | this.not.coercively = this._not.bind(this, false) | |
| 258 | ||
| 259 | this.alike = this._alike.bind(this, true) | |
| 260 | this.alike.coercively = this._alike.bind(this, false) | |
| 261 | ||
| 262 | this.unlike = this._unlike.bind(this, true) | |
| 263 | this.unlike.coercively = this._unlike.bind(this, false) | |
| 264 | ||
| 265 | this.exception = this._exception.bind(this, false) | |
| 266 | this.exception.all = this._exception.bind(this, true) | |
| 267 | ||
| 268 | this.execution = this._execution.bind(this) | |
| 269 | ||
| 270 | this.stealth = this._stealth.bind(this) | |
| 271 | ||
| 272 | this.snapshot = this._snapshot.bind(this) | |
| 273 | ||
| 274 | this.end = this._end.bind(this) | |
| 275 | ||
| 276 | this._parent = parent | |
| 277 | this._first = true | |
| 278 | this._wait = false | |
| 279 | this._planned = 0 | |
| 280 | this._hasPlan = false | |
| 281 | this._active = 0 | |
| 282 | this._timer = null | |
| 283 | ||
| 284 | this._headerLogged = false | |
| 285 | this._to = null | |
| 286 | this._teardowns = [] | |
| 287 | this._tickers = new Map() | |
| 288 | ||
| 289 | while (parent) { | |
| 290 | this._parents.push(parent) | |
| 291 | parent = parent._parent | |
| 292 | } | |
| 293 | } | |
| 294 | ||
| 295 | then (...args) { | |
| 296 | return this._promise.then(...args) | |
| 297 | } | |
| 298 | ||
| 299 | catch (...args) { | |
| 300 | return this._promise.catch(...args) | |
| 301 | } | |
| 302 | ||
| 303 | finally (...args) { | |
| 304 | return this._promise.finally(...args) | |
| 305 | } | |
| 306 | ||
| 307 | _header () { | |
| 308 | if (this._headerLogged) return | |
| 309 | this._headerLogged = true | |
| 310 | this._runner.start() | |
| 311 | this._runner.padding() | |
| 312 | this._runner.comment(this.name || 'test') | |
| 313 | } | |
| 314 | ||
| 315 | tmp () { return tmp(this) } | |
| 316 | ||
| 317 | _planDoneOrEnd () { | |
| 318 | return this._isEnded || (this._hasPlan && this._planned === 0) | |
| 319 | } | |
| 320 | ||
| 321 | _timeout (ms) { | |
| 322 | if (!ms) { | |
| 323 | if (this._to) clearTimeout(this._to) | |
| 324 | this._to = null | |
| 325 | return | |
| 326 | } | |
| 327 | ||
| 328 | const ontimeout = () => { | |
| 329 | this._to = null | |
| 330 | this._onend(new Error('Test timed out after ' + ms + ' ms')) | |
| 331 | } | |
| 332 | ||
| 333 | if (this._to) clearTimeout(this._to) | |
| 334 | this._to = setTimeout(ontimeout, ms) | |
| 335 | if (this._to.unref) this._to.unref() | |
| 336 | } | |
| 337 | ||
| 338 | _plan (n) { | |
| 339 | if (typeof n !== 'number' || n < 0) { | |
| 340 | throw new Error('Plan takes a positive whole number only') | |
| 341 | } | |
| 342 | ||
| 343 | this._hasPlan = true | |
| 344 | this._planned = n | |
| 345 | } | |
| 346 | ||
| 347 | _comment (...m) { | |
| 348 | if (this._isResolved) throw new Error('Can\'t comment after end') | |
| 349 | this._runner.log(INDENT + '#', ...m) | |
| 350 | } | |
| 351 | ||
| 352 | _message (message) { | |
| 353 | let m = '- ' | |
| 354 | ||
| 355 | if (!this._isMain) { | |
| 356 | for (let i = this._parents.length - 2; i >= 0; i--) { | |
| 357 | const p = this._parents[i] | |
| 358 | if (!p.name) continue | |
| 359 | m += '(' + p.name + ') - ' | |
| 360 | } | |
| 361 | if (this.name) { | |
| 362 | m += '(' + this.name + ') - ' | |
| 363 | } | |
| 364 | } | |
| 365 | ||
| 366 | if (message) { | |
| 367 | m += message | |
| 368 | } | |
| 369 | ||
| 370 | return m | |
| 371 | } | |
| 372 | ||
| 373 | _tick (ok) { | |
| 374 | if (ok) this.passes++ | |
| 375 | else this.fails++ | |
| 376 | this.assertions++ | |
| 377 | } | |
| 378 | ||
| 379 | _track (topLevel, ok) { | |
| 380 | if (topLevel) { | |
| 381 | this._runner.tests.count++ | |
| 382 | if (ok) this._runner.tests.pass++ | |
| 383 | return this._runner.tests.count | |
| 384 | } | |
| 385 | ||
| 386 | if (this._hasPlan) this._planned-- | |
| 387 | this._tick(ok) | |
| 388 | ||
| 389 | if (!this._isMain) this._main._tick(ok) | |
| 390 | ||
| 391 | this._runner.assertions.count++ | |
| 392 | if (ok) this._runner.assertions.pass++ | |
| 393 | ||
| 394 | return this._main.assertions | |
| 395 | } | |
| 396 | ||
| 397 | _assertion (ok, message, explanation, caller, top, isStealth = this._isStealth) { | |
| 398 | this._runner.assert(!this._main._isResolved, ok, this._track(false, ok), this._message(message), explanation, isStealth) | |
| 399 | ||
| 400 | if (this._isEnded || this._isDone) { | |
| 401 | throw new AssertionError({ message: 'Assertion after end' }) | |
| 402 | } | |
| 403 | ||
| 404 | if (this._hasPlan && this._planned < 0) { | |
| 405 | throw new AssertionError({ message: 'Too many assertions' }) | |
| 406 | } | |
| 407 | ||
| 408 | if (this._hasPlan && this._planned === 0) { | |
| 409 | this._checkEnd() | |
| 410 | } | |
| 411 | } | |
| 412 | ||
| 413 | _fail (message = 'failed') { | |
| 414 | const explanation = explain(false, message, 'fail', this._fail) | |
| 415 | this._assertion(false, message, explanation, this._fail, undefined) | |
| 416 | } | |
| 417 | ||
| 418 | _pass (message = 'passed') { | |
| 419 | this._assertion(true, message, null, this._pass, undefined) | |
| 420 | } | |
| 421 | ||
| 422 | _ok (assertion, message = 'expected truthy value') { | |
| 423 | const ok = assertion | |
| 424 | const explanation = explain(ok, message, 'ok', this._ok) | |
| 425 | this._assertion(ok, message, explanation, this._ok, undefined) | |
| 426 | } | |
| 427 | ||
| 428 | _absent (assertion, message = 'expected falsy value') { | |
| 429 | const ok = !assertion | |
| 430 | const explanation = explain(ok, message, 'absent', this._absent) | |
| 431 | this._assertion(ok, message, explanation, this._absent, undefined) | |
| 432 | } | |
| 433 | ||
| 434 | _is (strict, actual, expected, message = 'should be equal') { | |
| 435 | const ok = strict ? actual === expected : actual == expected // eslint-disable-line | |
| 436 | const explanation = explain(ok, message, 'is', this._is, actual, expected) | |
| 437 | this._assertion(ok, message, explanation, this._is, undefined) | |
| 438 | } | |
| 439 | ||
| 440 | _not (strict, actual, expected, message = 'should not be equal') { | |
| 441 | const ok = strict ? actual !== expected : actual != expected // eslint-disable-line | |
| 442 | const explanation = explain(ok, message, 'not', this._not, actual, expected) | |
| 443 | this._assertion(ok, message, explanation, this._not, undefined) | |
| 444 | } | |
| 445 | ||
| 446 | _alike (strict, actual, expected, message = 'should deep equal') { | |
| 447 | const ok = sameObject(actual, expected, { strict }) | |
| 448 | const explanation = explain(ok, message, 'alike', this._alike, actual, expected) | |
| 449 | this._assertion(ok, message, explanation, this._alike, undefined) | |
| 450 | } | |
| 451 | ||
| 452 | _unlike (strict, actual, expected, message = 'should not deep equal') { | |
| 453 | const ok = sameObject(actual, expected, { strict }) === false | |
| 454 | const explanation = explain(ok, message, 'unlike', this._unlike, actual, expected) | |
| 455 | this._assertion(ok, message, explanation, this._unlike, undefined) | |
| 456 | } | |
| 457 | ||
| 458 | _teardown (fn, opts = {}) { | |
| 459 | if (this._isDone) throw new Error('Can\'t add teardown after end') | |
| 460 | this._teardowns.push([opts.order || 0, !!opts.force, fn]) | |
| 461 | } | |
| 462 | ||
| 463 | async _exception (natives, functionOrPromise, expectedError, message) { | |
| 464 | if (typeof expectedError === 'string') { | |
| 465 | message = expectedError | |
| 466 | expectedError = undefined | |
| 467 | } | |
| 468 | ||
| 469 | const top = originFrame(this._exception) | |
| 470 | const pristineMessage = message === undefined | |
| 471 | ||
| 472 | let ok = null | |
| 473 | let actual = false | |
| 474 | ||
| 475 | if (pristineMessage) message = 'should throw' | |
| 476 | ||
| 477 | this._active++ | |
| 478 | try { | |
| 479 | if (typeof functionOrPromise === 'function') functionOrPromise = functionOrPromise() | |
| 480 | if (isPromise(functionOrPromise)) { | |
| 481 | if (pristineMessage) message = 'should reject' | |
| 482 | await functionOrPromise | |
| 483 | } | |
| 484 | ok = false | |
| 485 | } catch (err) { | |
| 486 | const native = natives === false && isUncaught(err) | |
| 487 | if (native) throw err | |
| 488 | ||
| 489 | if (!expectedError) { | |
| 490 | ok = true | |
| 491 | } else { | |
| 492 | ok = lazy.tmatch(err, expectedError) | |
| 493 | } | |
| 494 | ||
| 495 | actual = err | |
| 496 | } finally { | |
| 497 | this._active-- | |
| 498 | } | |
| 499 | ||
| 500 | const explanation = explain(ok, message, 'exception', this._exception, actual, expectedError, top) | |
| 501 | this._assertion(ok, message, explanation, this._execution, top) | |
| 502 | this._checkEnd() | |
| 503 | } | |
| 504 | ||
| 505 | async _execution (functionOrPromise, message) { | |
| 506 | const top = originFrame(this._execution) | |
| 507 | const pristineMessage = message === undefined | |
| 508 | ||
| 509 | let ok = false | |
| 510 | let error = null | |
| 511 | ||
| 512 | if (pristineMessage) message = 'should return' | |
| 513 | ||
| 514 | const time = highDefTimer() | |
| 515 | ||
| 516 | this._active++ | |
| 517 | try { | |
| 518 | if (typeof functionOrPromise === 'function') functionOrPromise = functionOrPromise() | |
| 519 | if (isPromise(functionOrPromise)) { | |
| 520 | if (pristineMessage) message = 'should resolve' | |
| 521 | await functionOrPromise | |
| 522 | } | |
| 523 | ok = true | |
| 524 | } catch (err) { | |
| 525 | error = err | |
| 526 | } finally { | |
| 527 | this._active-- | |
| 528 | } | |
| 529 | ||
| 530 | const elapsed = time() | |
| 531 | ||
| 532 | const explanation = explain(ok, message, 'execution', this._execution, error, null, top) | |
| 533 | this._assertion(ok, message, explanation, this._execution, top) | |
| 534 | this._checkEnd() | |
| 535 | ||
| 536 | return elapsed | |
| 537 | } | |
| 538 | ||
| 539 | _stealth (name, opts, fn) { | |
| 540 | if (typeof name === 'function') return this.stealth(null, null, name) | |
| 541 | if (typeof opts === 'function') return this.stealth(name, null, opts) | |
| 542 | ||
| 543 | return this.test(name, { ...opts, stealth: true }, fn) | |
| 544 | } | |
| 545 | ||
| 546 | _snapshot (actual, message = 'should match snapshot') { | |
| 547 | const top = originFrame(this._snapshot) | |
| 548 | ||
| 549 | if (!top) { | |
| 550 | this._assertion(true, message, null, this._snapshot, undefined) | |
| 551 | return | |
| 552 | } | |
| 553 | ||
| 554 | if (b4a.isBuffer(actual)) { | |
| 555 | actual = new Uint8Array(actual.buffer, actual.byteOffset, actual.byteLength) | |
| 556 | } | |
| 557 | ||
| 558 | const filename = top.getFileName() | |
| 559 | const key = (this.name || '') + ' ' + this._message(message) | |
| 560 | const expected = getSnapshot(filename, key + ' - ' + this._getTick(key), actual) | |
| 561 | ||
| 562 | const ok = sameObject(actual, expected, { strict: true }) | |
| 563 | const explanation = explain(ok, message, 'snapshot', this._snapshot, actual, expected) | |
| 564 | ||
| 565 | this._assertion(ok, message, explanation, this._snapshot, undefined) | |
| 566 | } | |
| 567 | ||
| 568 | _getTick (key) { | |
| 569 | const tick = this._tickers.get(key) || 0 | |
| 570 | this._tickers.set(key, 1 + tick) | |
| 571 | return tick | |
| 572 | } | |
| 573 | ||
| 574 | _test (name, opts, fn) { | |
| 575 | if (typeof name === 'function') return this.test(null, null, name) | |
| 576 | if (typeof opts === 'function') return this.test(name, null, opts) | |
| 577 | ||
| 578 | const t = new Test(name, this, opts) | |
| 579 | ||
| 580 | if (this._hasPlan) this._planned-- | |
| 581 | this._active++ | |
| 582 | ||
| 583 | return fn ? t._run(fn, opts || {}) : t | |
| 584 | } | |
| 585 | ||
| 586 | async _run (fn, opts) { | |
| 587 | this._isQueued = true | |
| 588 | ||
| 589 | if (!this._parent) { | |
| 590 | if (!(await this._runner.queue(this))) return | |
| 591 | } | |
| 592 | ||
| 593 | this._onstart(opts) | |
| 594 | this._wait = true | |
| 595 | ||
| 596 | try { | |
| 597 | await fn(this) | |
| 598 | } catch (err) { | |
| 599 | if (!(err instanceof AssertionError && err.message === 'ERR_ASSERTION: Stealth assertion failed')) { | |
| 600 | this._wait = false | |
| 601 | await this._runTeardown(err) | |
| 602 | throw err | |
| 603 | } | |
| 604 | } | |
| 605 | ||
| 606 | if (!this._hasPlan) this.end() | |
| 607 | ||
| 608 | this._wait = false | |
| 609 | this._checkEnd() | |
| 610 | ||
| 611 | return await this | |
| 612 | } | |
| 613 | ||
| 614 | _end () { | |
| 615 | this._isEnded = true | |
| 616 | ||
| 617 | if (this._hasPlan && this._planned > 0) { | |
| 618 | throw prematureEnd(this, 'Too few assertions') | |
| 619 | } | |
| 620 | ||
| 621 | this._checkEnd() | |
| 622 | } | |
| 623 | ||
| 624 | _checkEnd () { | |
| 625 | if (this._active || this._wait) return | |
| 626 | if (this._isEnded || (this._hasPlan && this._planned === 0)) this._done() | |
| 627 | } | |
| 628 | ||
| 629 | _done () { | |
| 630 | if (this._isDone) return | |
| 631 | this._isDone = true | |
| 632 | ||
| 633 | if (this._teardowns.length) { | |
| 634 | this._teardowns.sort(cmp) | |
| 635 | this._runTeardown(null) | |
| 636 | } else { | |
| 637 | this._onend(null) | |
| 638 | } | |
| 639 | ||
| 640 | if (this._parent) { | |
| 641 | const p = this._parent | |
| 642 | ||
| 643 | this._parent._active-- | |
| 644 | this._parent = null | |
| 645 | ||
| 646 | p._checkEnd() | |
| 647 | } | |
| 648 | } | |
| 649 | ||
| 650 | async _runTeardown (error) { | |
| 651 | const forced = !!error | |
| 652 | ||
| 653 | let fired = false | |
| 654 | ||
| 655 | const t = setTimeout(() => { | |
| 656 | fired = true | |
| 657 | this.comment('...teardown still running after 250ms') | |
| 658 | }, 250) | |
| 659 | ||
| 660 | if (t.unref) t.unref() | |
| 661 | ||
| 662 | const time = highDefTimer() | |
| 663 | ||
| 664 | for (const [, force, teardown] of this._teardowns) { | |
| 665 | try { | |
| 666 | if (force || !forced) await teardown() | |
| 667 | } catch (err) { | |
| 668 | if (!error) error = err | |
| 669 | } | |
| 670 | } | |
| 671 | ||
| 672 | clearTimeout(t) | |
| 673 | if (fired) this.comment('...teardown time ' + time() + 'ms') | |
| 674 | this._onend(error) | |
| 675 | } | |
| 676 | ||
| 677 | _onstart (opts) { | |
| 678 | const to = this._isMain | |
| 679 | ? (opts && opts.timeout !== undefined) ? opts.timeout : this._runner.defaultTimeout // main tests need a default timeout, unless opt-out | |
| 680 | : opts && opts.timeout // non main ones do not | |
| 681 | ||
| 682 | if (this._isMain) { | |
| 683 | if (!this._isQueued) { | |
| 684 | if (this._runner.next) throw new Error('Only run test can be running at the same time') | |
| 685 | this._runner.next = this | |
| 686 | } | |
| 687 | this._header() | |
| 688 | this._timer = highDefTimer() | |
| 689 | } | |
| 690 | ||
| 691 | if (to) this._timeout(to) | |
| 692 | } | |
| 693 | ||
| 694 | _onend (err) { | |
| 695 | if (this._isResolved) return | |
| 696 | ||
| 697 | this._timeout(0) // just to be sure incase someone ran this during teardown... | |
| 698 | ||
| 699 | const ok = (this.fails === 0) | |
| 700 | ||
| 701 | if (this._isMain && !err) { | |
| 702 | const time = this._timer ? ' # time = ' + this._timer() + 'ms' : '' | |
| 703 | this._runner.assert(false, ok, this._track(true, ok), '- ' + (this.name || '') + time, null) | |
| 704 | } | |
| 705 | ||
| 706 | this._isResolved = true | |
| 707 | this._isDone = true | |
| 708 | ||
| 709 | if (this._isMain && this._runner.next === this) { | |
| 710 | this._runner.next = null | |
| 711 | } | |
| 712 | ||
| 713 | if (err) this._reject(err) | |
| 714 | else this._resolve(ok) | |
| 715 | ||
| 716 | // if test is running without deadlock detection, trigger "io" to rerun it in case idle now | |
| 717 | if (this._checkDeadlock === false) setImmediate(() => {}) | |
| 718 | } | |
| 719 | } | |
| 720 | ||
| 721 | exports = module.exports = test | |
| 722 | ||
| 723 | exports.Test = Test | |
| 724 | exports.test = test | |
| 725 | exports.hook = hook | |
| 726 | exports.solo = solo | |
| 727 | exports.skip = skip | |
| 728 | exports.todo = todo | |
| 729 | exports.configure = configure | |
| 730 | exports.pause = pause | |
| 731 | exports.resume = resume | |
| 732 | exports.stealth = stealth | |
| 733 | ||
| 734 | // Used by snapshots | |
| 735 | exports.createTypedArray = createTypedArray | |
| 736 | ||
| 737 | function configure ({ timeout = DEFAULT_TIMEOUT, bail = false, solo = false, unstealth = false, source = true, coverage = false } = {}) { | |
| 738 | const runner = getRunner() | |
| 739 | ||
| 740 | if (runner.tests.count > 0 || runner.assertions.count > 0) { | |
| 741 | throw new Error('Configuration must happen prior to registering any tests') | |
| 742 | } | |
| 743 | ||
| 744 | if (coverage) require('bare-cov')({ dir: typeof coverage === 'string' ? coverage : undefined }) | |
| 745 | ||
| 746 | runner.defaultTimeout = timeout | |
| 747 | runner.bail = bail | |
| 748 | runner.explicitSolo = solo | |
| 749 | runner.unstealth = unstealth | |
| 750 | runner.source = source | |
| 751 | } | |
| 752 | ||
| 753 | function highDefTimerNode () { | |
| 754 | const then = process.hrtime.bigint() | |
| 755 | return function () { | |
| 756 | const now = process.hrtime.bigint() | |
| 757 | return Number(now - then) / 1e6 | |
| 758 | } | |
| 759 | } | |
| 760 | ||
| 761 | function highDefTimerFallback () { | |
| 762 | const then = Date.now() | |
| 763 | return function () { | |
| 764 | const now = Date.now() | |
| 765 | return now - then | |
| 766 | } | |
| 767 | } | |
| 768 | ||
| 769 | function cmp (a, b) { | |
| 770 | return a[0] - b[0] | |
| 771 | } | |
| 772 | ||
| 773 | function test (name, opts, fn, overrides) { | |
| 774 | if (typeof name === 'function') return test(null, null, name, overrides) | |
| 775 | if (typeof opts === 'function') return test(name, null, opts, overrides) | |
| 776 | ||
| 777 | opts = { ...opts, ...overrides } | |
| 778 | ||
| 779 | const t = new Test(name, null, opts) | |
| 780 | ||
| 781 | if (fn) return t._run(fn, opts) | |
| 782 | if (t._isTodo) return t._run(() => {}, opts) | |
| 783 | ||
| 784 | if (t._isSkip) { | |
| 785 | throw new Error('An inverted test cannot be skipped') | |
| 786 | } | |
| 787 | if (t._isSolo) { | |
| 788 | t._runner.solo = t | |
| 789 | } | |
| 790 | ||
| 791 | t._onstart(opts) | |
| 792 | ||
| 793 | return t | |
| 794 | } | |
| 795 | ||
| 796 | function hook (name, opts, fn) { | |
| 797 | return test(name, opts, fn, { hook: true }) | |
| 798 | } | |
| 799 | ||
| 800 | function solo (name, opts, fn) { | |
| 801 | if (!name && !opts && !fn) return test.configure({ solo: true }) | |
| 802 | return test(name, opts, fn, { solo: true }) | |
| 803 | } | |
| 804 | ||
| 805 | function skip (name, opts, fn) { | |
| 806 | return test(name, opts, fn, { skip: true }) | |
| 807 | } | |
| 808 | ||
| 809 | function todo (name, opts, fn) { | |
| 810 | return test(name, opts, fn, { todo: true }) | |
| 811 | } | |
| 812 | ||
| 813 | function pause () { | |
| 814 | getRunner().pause() | |
| 815 | } | |
| 816 | ||
| 817 | function resume () { | |
| 818 | getRunner().resume() | |
| 819 | } | |
| 820 | ||
| 821 | function wait (ticks = 1) { | |
| 822 | return new Promise(resolve => { | |
| 823 | tickish(function loop () { | |
| 824 | if (--ticks <= 0) return resolve() | |
| 825 | tickish(loop) | |
| 826 | }) | |
| 827 | }) | |
| 828 | } | |
| 829 | ||
| 830 | function tickish (fn) { | |
| 831 | if (IS_NODE) { // do both types of tick in node to flush both queues | |
| 832 | process.nextTick(queueMicrotask, fn) | |
| 833 | } else { | |
| 834 | queueMicrotask(fn) | |
| 835 | } | |
| 836 | } | |
| 837 | ||
| 838 | function explain (ok, message, assert, stackStartFunction, actual, expected, top = !ok && originFrame(stackStartFunction), extra) { | |
| 839 | const runner = getRunner() | |
| 840 | return ok ? null : lazy.errors.explain(ok, message, assert, stackStartFunction, actual, expected, runner.source ? top : null, extra) | |
| 841 | } | |
| 842 | ||
| 843 | function originFrame (stackStartFunction) { | |
| 844 | if (!Error.captureStackTrace) return undefined | |
| 845 | const { prepareStackTrace } = Error | |
| 846 | Error.prepareStackTrace = (_, stack) => { | |
| 847 | if (stack[0].getFunctionName() === '[brittle.error]') return null | |
| 848 | if (stack[0].getMethodName() === 'coercively') return stack[1] | |
| 849 | return stack[0] | |
| 850 | } | |
| 851 | const err = {} | |
| 852 | Error.captureStackTrace(err, stackStartFunction) | |
| 853 | const { stack: top } = err | |
| 854 | Error.prepareStackTrace = prepareStackTrace | |
| 855 | return top | |
| 856 | } | |
| 857 | ||
| 858 | function isPromise (p) { | |
| 859 | return !!(p && typeof p.then === 'function') | |
| 860 | } | |
| 861 | ||
| 862 | function isUncaught (err) { | |
| 863 | return err instanceof SyntaxError || | |
| 864 | err instanceof ReferenceError || | |
| 865 | err instanceof TypeError || | |
| 866 | err instanceof EvalError || | |
| 867 | err instanceof RangeError | |
| 868 | } | |
| 869 | ||
| 870 | function getRunner () { | |
| 871 | if (!global[RUNNER]) global[RUNNER] = new Runner() | |
| 872 | return global[RUNNER] | |
| 873 | } | |
| 874 | ||
| 875 | function prematureEnd (t, message) { | |
| 876 | const details = t._hasPlan | |
| 877 | ? ' [assertion count (' + t.assertions + ') did not reach plan (' + (t.assertions + t._planned) + ')]' | |
| 878 | : '' | |
| 879 | ||
| 880 | return new Error(message + details) | |
| 881 | } | |
| 882 | ||
| 883 | function stealth (name, opts, fn) { | |
| 884 | return test(name, opts, fn, { stealth: true }) | |
| 885 | } |
| … | … | @@ -0,0 +1,12 @@ |
| 1 | const CODE = 'ERR_ASSERTION' | |
| 2 | ||
| 3 | module.exports = class AssertionError extends Error { | |
| 4 | constructor ({ message }) { | |
| 5 | super(`${CODE}: ${message}`) | |
| 6 | this.code = CODE | |
| 7 | ||
| 8 | if (Error.captureStackTrace) { | |
| 9 | Error.captureStackTrace(this, AssertionError) | |
| 10 | } | |
| 11 | } | |
| 12 | } |
| … | … | @@ -0,0 +1,5 @@ |
| 1 | exports.INDENT = ' ' | |
| 2 | exports.RUNNER = Symbol.for('brittle-runner') | |
| 3 | exports.IS_NODE = !!(typeof process === 'object' && process && process.versions && (typeof (process.versions.node || process.versions.pear || process.versions.bare) === 'string') && !process.browser) | |
| 4 | exports.IS_BARE = typeof Bare !== 'undefined' | |
| 5 | exports.DEFAULT_TIMEOUT = 30000 |
| … | … | @@ -0,0 +1,127 @@ |
| 1 | const { AssertionError } = require('assert') | |
| 2 | const fs = require('fs') | |
| 3 | const url = require('url') | |
| 4 | const os = require('os') | |
| 5 | const StackParser = require('error-stack-parser') | |
| 6 | const { INDENT, IS_BARE } = require('./constants') | |
| 7 | ||
| 8 | const parseStack = StackParser.parse.bind(StackParser) | |
| 9 | ||
| 10 | const IGNORE = typeof __filename === 'string' ? [__filename, __filename.replace(/lib(\/|\\)errors\.js$/, 'index.js')] : [] | |
| 11 | ||
| 12 | exports.stringify = stringify | |
| 13 | exports.explain = explain | |
| 14 | ||
| 15 | function explain (ok, message, assert, stackStartFunction, actual, expected, top, extra) { | |
| 16 | if (ok) return null | |
| 17 | ||
| 18 | const cwd = IS_BARE ? os.cwd() : process.cwd() | |
| 19 | ||
| 20 | const err = new AssertionError({ stackStartFunction, message, operator: assert, actual, expected }) | |
| 21 | stackScrub(err) | |
| 22 | ||
| 23 | if (top) { | |
| 24 | const line = top.getLineNumber() | |
| 25 | const column = top.getColumnNumber() | |
| 26 | let file = top.getFileName()?.replace(/\?cacheBust=\d+/g, '') | |
| 27 | ||
| 28 | try { | |
| 29 | try { | |
| 30 | if (url) file = url.fileURLToPath(new URL(file, 'file:')) | |
| 31 | } catch {} | |
| 32 | ||
| 33 | if (file.startsWith(cwd)) { | |
| 34 | file = file.replace(cwd, '.') | |
| 35 | } | |
| 36 | ||
| 37 | if (fs) { | |
| 38 | const code = fs.readFileSync(file, { encoding: 'utf-8' }) | |
| 39 | const split = code.split(/\r?\n/g) | |
| 40 | const point = Array.from({ length: column - 1 }).map(() => '-').join('') + '^' | |
| 41 | const source = [...split.slice(line - 2, line), point, ...split.slice(line, line + 2)] | |
| 42 | err.source = source.join('\n') | |
| 43 | } else { | |
| 44 | err.source = '' | |
| 45 | } | |
| 46 | } /* c8 ignore next */ catch {} | |
| 47 | } | |
| 48 | const { code, generatedMessage, ...info } = err | |
| 49 | err.code = code | |
| 50 | err.generatedMessage = generatedMessage | |
| 51 | Object.defineProperty(info, 'err', { value: err }) | |
| 52 | ||
| 53 | if (err.stack) { | |
| 54 | info.stack = err.stack.slice(err.toString().length + 1).split('\n').map((line) => { | |
| 55 | let match = false | |
| 56 | ||
| 57 | line = line.slice(7).replace(cwd, () => { | |
| 58 | match = true | |
| 59 | return '.' | |
| 60 | }) | |
| 61 | ||
| 62 | if (match) line = line.replace(/file:\/?\/?\/?/, '') | |
| 63 | return line | |
| 64 | }).join('\n').trim() | |
| 65 | } | |
| 66 | ||
| 67 | if (!info.stack || code === 'ERR_TIMEOUT' || code === 'ERR_PREMATURE_END' || actual?.code === 'ERR_TIMEOUT' || actual?.code === 'ERR_PREMATURE_END') delete info.stack | |
| 68 | ||
| 69 | if (actual === undefined && expected === undefined) { | |
| 70 | delete info.actual | |
| 71 | delete info.expected | |
| 72 | } | |
| 73 | ||
| 74 | if (info.diff) delete info.diff | |
| 75 | ||
| 76 | return info | |
| 77 | } | |
| 78 | ||
| 79 | function stackScrub (err) { | |
| 80 | if (err && err.stack) { | |
| 81 | const errString = err.toString() | |
| 82 | const stack = '\n' + err.stack.slice(errString.length + 1) | |
| 83 | const scrubbed = parseStack({ stack }).filter(({ fileName }) => !IGNORE.includes(fileName)) | |
| 84 | if (scrubbed.length > 0) { | |
| 85 | err.stack = `${errString}\n at ${scrubbed.join('\n at ').replace(/\?cacheBust=\d+/g, '')}` | |
| 86 | } | |
| 87 | } | |
| 88 | return err | |
| 89 | } | |
| 90 | ||
| 91 | function indent (src) { | |
| 92 | return src.split('\n').map(s => INDENT + ' ' + s).join('\n') | |
| 93 | } | |
| 94 | ||
| 95 | function stringify (o) { | |
| 96 | return indent('---\n' + toYAML(o)).trimRight() + '\n' + INDENT + ' ...' | |
| 97 | } | |
| 98 | ||
| 99 | // for the life of me can't find a module that does this, that also works in the browser... | |
| 100 | function toYAML (o, maxDepth = 5, indent = '', prev = null) { | |
| 101 | if (maxDepth < 0) return indent + '...' | |
| 102 | ||
| 103 | if (typeof o === 'string') { | |
| 104 | if (o.indexOf('\n') === -1) return o + '\n' | |
| 105 | return '|\n' + o.split('\n').map(s => indent + s).join('\n').trimRight() + '\n' | |
| 106 | } | |
| 107 | ||
| 108 | if (Array.isArray(o) || o instanceof Set) { | |
| 109 | let s = prev ? '\n' : '' | |
| 110 | for (const i of o) { | |
| 111 | s += indent + '- ' + toYAML(i, maxDepth - 1, indent + ' ', 'array') | |
| 112 | } | |
| 113 | return s.trimRight() + '\n' | |
| 114 | } | |
| 115 | ||
| 116 | if (o && typeof o === 'object') { | |
| 117 | const p = prev && prev !== 'array' | |
| 118 | let s = p ? '\n' : '' | |
| 119 | for (const k of Object.keys(o)) { | |
| 120 | const v = o[k] | |
| 121 | s += indent + k + ': ' + toYAML(v, maxDepth - 1, indent + ' ', 'object') | |
| 122 | } | |
| 123 | return (p ? s.trimRight() : s.trim()) + '\n' | |
| 124 | } | |
| 125 | ||
| 126 | return '' + o + '\n' | |
| 127 | } |
| … | … | @@ -0,0 +1,89 @@ |
| 1 | const b4a = require('b4a') | |
| 2 | const { fileURLToPath } = require('url') | |
| 3 | ||
| 4 | exports.createTypedArray = function (TypedArray, src) { | |
| 5 | const b = b4a.from(src, 'base64') | |
| 6 | return new TypedArray(b.buffer, b.byteOffset, b.byteLength / TypedArray.BYTES_PER_ELEMENT) | |
| 7 | } | |
| 8 | ||
| 9 | exports.getSnapshot = function (filename, key, actual) { | |
| 10 | let snap = {} | |
| 11 | ||
| 12 | const p = split(filename) | |
| 13 | if (!p) return actual | |
| 14 | ||
| 15 | try { | |
| 16 | snap = require(p.filename) | |
| 17 | } catch {} | |
| 18 | ||
| 19 | if (snap[key] !== undefined) return snap[key] | |
| 20 | ||
| 21 | snap[key] = actual | |
| 22 | ||
| 23 | const fs = requireMaybe('fs') | |
| 24 | if (!fs) return actual | |
| 25 | ||
| 26 | try { | |
| 27 | fs.mkdirSync(p.dirname) | |
| 28 | } catch {} | |
| 29 | ||
| 30 | let brittle = false | |
| 31 | let s = '' | |
| 32 | ||
| 33 | for (const k of Object.keys(snap)) { | |
| 34 | s += 'exports[' + toString(k) + '] = ' | |
| 35 | ||
| 36 | const v = snap[k] | |
| 37 | ||
| 38 | if (isTypedArray(v)) { | |
| 39 | if (!brittle) { | |
| 40 | brittle = true | |
| 41 | s = 'const { createTypedArray } = require(\'brittle\')\n\n' + s | |
| 42 | } | |
| 43 | const b = b4a.from(v.buffer, v.byteOffset, v.byteLength) | |
| 44 | s += 'createTypedArray(' + v.constructor.name + ', ' + toString(b4a.toString(b, 'base64')) + ')\n\n' | |
| 45 | } else { | |
| 46 | s += toString(v) + '\n\n' | |
| 47 | } | |
| 48 | } | |
| 49 | s = '/* eslint-disable */\n\n' + s + '/* eslint-enable */\n' | |
| 50 | ||
| 51 | fs.writeFileSync(p.filename, s) | |
| 52 | return actual | |
| 53 | } | |
| 54 | ||
| 55 | function isTypedArray (v) { | |
| 56 | return !!(v && v.BYTES_PER_ELEMENT) | |
| 57 | } | |
| 58 | ||
| 59 | function toString (s) { | |
| 60 | if (typeof s === 'string' && s.indexOf('\n') > 0 && s.indexOf('`') === -1) return '`' + s + '`' | |
| 61 | if (typeof s === 'string' && s.indexOf('"') === -1) return '\'' + s + '\'' | |
| 62 | return JSON.stringify(s, null, 2) | |
| 63 | } | |
| 64 | ||
| 65 | function split (filename) { | |
| 66 | if (filename.startsWith('file://')) filename = fileURLToPath(filename) | |
| 67 | ||
| 68 | const a = filename.lastIndexOf('/') | |
| 69 | const b = filename.lastIndexOf('\\') | |
| 70 | const sep = a > b ? '/' : '\\' | |
| 71 | const i = a > b ? a : b | |
| 72 | ||
| 73 | if (i === -1) return null | |
| 74 | ||
| 75 | const dirname = filename.slice(0, i) + sep + 'fixtures' | |
| 76 | ||
| 77 | return { | |
| 78 | dirname, | |
| 79 | filename: dirname + sep + filename.slice(i + 1).replace(/\.[^.]+$/, '') + '.snapshot.cjs' | |
| 80 | } | |
| 81 | } | |
| 82 | ||
| 83 | function requireMaybe (name) { | |
| 84 | try { | |
| 85 | return require(name) | |
| 86 | } catch { | |
| 87 | return null | |
| 88 | } | |
| 89 | } |
| … | … | @@ -0,0 +1,72 @@ |
| 1 | const activePromises = new Map() | |
| 2 | ||
| 3 | class TracingPromise extends Promise { | |
| 4 | constructor (fn) { | |
| 5 | const stack = (new Error()).stack | |
| 6 | let deleted = false | |
| 7 | ||
| 8 | addStack(stack) | |
| 9 | ||
| 10 | super((resolve, reject) => { | |
| 11 | const innerResolve = (value) => { | |
| 12 | if (!deleted) { | |
| 13 | deleted = true | |
| 14 | deleteStack(stack) | |
| 15 | } | |
| 16 | resolve(value) | |
| 17 | } | |
| 18 | const innerReject = (err) => { | |
| 19 | if (!deleted) { | |
| 20 | deleted = true | |
| 21 | deleteStack(stack) | |
| 22 | } | |
| 23 | reject(err) | |
| 24 | } | |
| 25 | ||
| 26 | return fn(innerResolve, innerReject) | |
| 27 | }) | |
| 28 | } | |
| 29 | ||
| 30 | static Untraced = Promise | |
| 31 | ||
| 32 | static enable () { | |
| 33 | global.Promise = TracingPromise | |
| 34 | } | |
| 35 | ||
| 36 | static print () { | |
| 37 | if (activePromises.size === 0) { | |
| 38 | console.error('(All promises are resolved)') | |
| 39 | return | |
| 40 | } | |
| 41 | for (const [stack, count] of activePromises) { | |
| 42 | const lines = stack.split('\n') | |
| 43 | lines.shift() | |
| 44 | lines.shift() | |
| 45 | ||
| 46 | const trace = lines.map((line) => { | |
| 47 | line = line.slice(7) | |
| 48 | if (/\(node:[^(]*\)$/.test(line)) return null | |
| 49 | if (line.startsWith('TracingPromise.then ')) return null | |
| 50 | return ' at ' + line | |
| 51 | }).filter(x => x).join('\n') | |
| 52 | ||
| 53 | if (!trace) continue | |
| 54 | ||
| 55 | console.error(count + ' promise' + (count === 1 ? '' : 's') + ' unresolved') | |
| 56 | console.error(trace) | |
| 57 | console.error() | |
| 58 | } | |
| 59 | } | |
| 60 | } | |
| 61 | ||
| 62 | module.exports = TracingPromise | |
| 63 | ||
| 64 | function addStack (s) { | |
| 65 | activePromises.set(s, (activePromises.get(s) || 0) + 1) | |
| 66 | } | |
| 67 | ||
| 68 | function deleteStack (s) { | |
| 69 | const n = activePromises.get(s) | |
| 70 | if (n === 1) activePromises.delete(s) | |
| 71 | else activePromises.set(s, n - 1) | |
| 72 | } |
| … | … | @@ -0,0 +1,84 @@ |
| 1 | { | |
| 2 | "name": "brittle", | |
| 3 | "version": "3.19.1", | |
| 4 | "description": "A TAP test runner built for modern times", | |
| 5 | "main": "index.js", | |
| 6 | "bin": { | |
| 7 | "brittle": "bin/node.js", | |
| 8 | "brittle-node": "bin/node.js", | |
| 9 | "brittle-bare": "bin/bare.js", | |
| 10 | "brittle-pear": "bin/pear.js" | |
| 11 | }, | |
| 12 | "author": "David Mark Clements (@davidmarkclem)", | |
| 13 | "license": "Apache-2.0", | |
| 14 | "scripts": { | |
| 15 | "test": "standard && node test/all.mjs", | |
| 16 | "test-bare": "bare test/bare-test.mjs" | |
| 17 | }, | |
| 18 | "files": [ | |
| 19 | "index.js", | |
| 20 | "cmd.js", | |
| 21 | "lib/**.js", | |
| 22 | "bin/**.js" | |
| 23 | ], | |
| 24 | "dependencies": { | |
| 25 | "b4a": "^1.6.0", | |
| 26 | "bare-assert": "^1.0.2", | |
| 27 | "bare-cov": "^1.1.0", | |
| 28 | "bare-fs": "^4.1.6", | |
| 29 | "bare-os": "^3.6.1", | |
| 30 | "bare-path": "^3.0.0", | |
| 31 | "bare-process": "^4.2.1", | |
| 32 | "bare-subprocess": "^5.0.0", | |
| 33 | "bare-url": "^2.1.6", | |
| 34 | "error-stack-parser": "^2.1.4", | |
| 35 | "globbie": "^1.0.2", | |
| 36 | "paparam": "^1.6.2", | |
| 37 | "same-object": "^1.0.2", | |
| 38 | "test-tmp": "^1.4.0", | |
| 39 | "tmatch": "^5.0.0" | |
| 40 | }, | |
| 41 | "devDependencies": { | |
| 42 | "chalk": "^4.1.2", | |
| 43 | "safety-catch": "^1.0.2", | |
| 44 | "standard": "^17.0.0" | |
| 45 | }, | |
| 46 | "imports": { | |
| 47 | "assert": { | |
| 48 | "bare": "bare-assert", | |
| 49 | "default": "assert" | |
| 50 | }, | |
| 51 | "child_process": { | |
| 52 | "bare": "bare-subprocess", | |
| 53 | "default": "child_process" | |
| 54 | }, | |
| 55 | "fs": { | |
| 56 | "bare": "bare-fs", | |
| 57 | "default": "fs" | |
| 58 | }, | |
| 59 | "os": { | |
| 60 | "bare": "bare-os", | |
| 61 | "default": "os" | |
| 62 | }, | |
| 63 | "path": { | |
| 64 | "bare": "bare-path", | |
| 65 | "default": "path" | |
| 66 | }, | |
| 67 | "process": { | |
| 68 | "bare": "bare-process", | |
| 69 | "default": "process" | |
| 70 | }, | |
| 71 | "url": { | |
| 72 | "bare": "bare-url", | |
| 73 | "default": "url" | |
| 74 | } | |
| 75 | }, | |
| 76 | "repository": { | |
| 77 | "type": "git", | |
| 78 | "url": "git+https://github.com/holepunchto/brittle.git" | |
| 79 | }, | |
| 80 | "bugs": { | |
| 81 | "url": "https://github.com/holepunchto/brittle/issues" | |
| 82 | }, | |
| 83 | "homepage": "https://github.com/holepunchto/brittle#readme" | |
| 84 | } |
| … | … | @@ -0,0 +1,15 @@ |
| 1 | ISC License | |
| 2 | ||
| 3 | Copyright (c) 2022 Contributors | |
| 4 | ||
| 5 | Permission to use, copy, modify, and/or distribute this software for any | |
| 6 | purpose with or without fee is hereby granted, provided that the above | |
| 7 | copyright notice and this permission notice appear in all copies. | |
| 8 | ||
| 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH | |
| 10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY | |
| 11 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, | |
| 12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM | |
| 13 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR | |
| 14 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR | |
| 15 | PERFORMANCE OF THIS SOFTWARE. |
| … | … | @@ -0,0 +1,39 @@ |
| 1 | # compact-encoding-bitfield | |
| 2 | ||
| 3 | [compact-encoding](https://github.com/compact-encoding/compact-encoding) codec for bitfields. Uses a variable length encoding that is ABI compatible with `cenc.uint`. | |
| 4 | ||
| 5 | ```sh | |
| 6 | npm install compact-encoding-bitfield | |
| 7 | ``` | |
| 8 | ||
| 9 | ## Usage | |
| 10 | ||
| 11 | ```js | |
| 12 | const cenc = require('compact-encoding') | |
| 13 | const bitfield = require('compact-encoding-bitfield') | |
| 14 | ||
| 15 | const buffer = cenc.encode(bitfield(8), 0b1110_1011) | |
| 16 | // <Buffer fd eb 00> | |
| 17 | ||
| 18 | cenc.decode(bitfield(8), buffer) | |
| 19 | // <Buffer eb> | |
| 20 | ``` | |
| 21 | ||
| 22 | Bitfields are represented as buffers. For each byte, the least significant bit denotes the first bit and the most significant bit denotes the last bit. For example, bit `0` will be the least significant bit of the first byte and bit `15` will be the most significant bit of the second byte. | |
| 23 | ||
| 24 | The encoding will convert numbers to buffers. Buffers may also be passed directly and will be returned on decode. | |
| 25 | ||
| 26 | ## ABI | |
| 27 | ||
| 28 | Depending on `length`, the bitfield is stored using a variable number of bytes: | |
| 29 | ||
| 30 | 1. `length < 8`: 1 byte with no prefix. | |
| 31 | 2. `length <= 16`: 2 bytes with a `0xfd` prefix. | |
| 32 | 3. `length <= 32`: 4 bytes with a `0xfe` prefix. | |
| 33 | 4. `length <= 64`: 8 bytes with a `0xff` prefix. | |
| 34 | ||
| 35 | `length` must not exceed 64 bits. | |
| 36 | ||
| 37 | ## License | |
| 38 | ||
| 39 | ISC |
| … | … | @@ -0,0 +1,67 @@ |
| 1 | const c = require('compact-encoding') | |
| 2 | ||
| 3 | module.exports = function bitfield (length) { | |
| 4 | if (length > 64) throw new RangeError('Bitfield cannot be larger than 64 bits') | |
| 5 | ||
| 6 | let byteLength | |
| 7 | if (length < 8) byteLength = 1 | |
| 8 | else if (length <= 16) byteLength = 2 | |
| 9 | else if (length <= 32) byteLength = 4 | |
| 10 | else byteLength = 8 | |
| 11 | ||
| 12 | return { | |
| 13 | preencode (state) { | |
| 14 | state.end++ // Length byte, used for data when byteLength === 1 | |
| 15 | ||
| 16 | if (byteLength === 1) ; | |
| 17 | else if (byteLength === 2) c.uint16.preencode(state) | |
| 18 | else if (byteLength === 4) c.uint32.preencode(state) | |
| 19 | else c.uint64.preencode(state) | |
| 20 | }, | |
| 21 | ||
| 22 | encode (state, b) { | |
| 23 | if (byteLength === 1) ; | |
| 24 | else if (byteLength === 2) c.uint8.encode(state, 0xfd) | |
| 25 | else if (byteLength === 4) c.uint8.encode(state, 0xfe) | |
| 26 | else c.uint8.encode(state, 0xff) | |
| 27 | ||
| 28 | if (typeof b === 'number') { | |
| 29 | if (byteLength === 1) c.uint8.encode(state, b) | |
| 30 | else if (byteLength === 2) c.uint16.encode(state, b) | |
| 31 | else if (byteLength === 4) c.uint32.encode(state, b) | |
| 32 | else c.uint64.encode(state, b) | |
| 33 | } else { | |
| 34 | state.buffer.set(b, state.start) | |
| 35 | ||
| 36 | if (b.byteLength < byteLength) { | |
| 37 | // Zero-fill the rest of the byte length. | |
| 38 | state.buffer.fill( | |
| 39 | 0, | |
| 40 | state.start + b.byteLength, | |
| 41 | state.start + byteLength | |
| 42 | ) | |
| 43 | } | |
| 44 | ||
| 45 | state.start += byteLength | |
| 46 | } | |
| 47 | }, | |
| 48 | ||
| 49 | decode (state) { | |
| 50 | const byte = state.buffer[state.start] | |
| 51 | ||
| 52 | let byteLength | |
| 53 | if (byte <= 0xfc) byteLength = 1 | |
| 54 | else if (byte === 0xfd) byteLength = 2 | |
| 55 | else if (byte === 0xfe) byteLength = 4 | |
| 56 | else byteLength = 8 | |
| 57 | ||
| 58 | if (byteLength > 1) state.start++ // Skip the length byte | |
| 59 | ||
| 60 | if (state.end - state.start < byteLength) throw new Error('Out of bounds') | |
| 61 | ||
| 62 | const b = state.buffer.subarray(state.start, (state.start += byteLength)) | |
| 63 | ||
| 64 | return length <= 8 ? b.subarray(0, 1) : b | |
| 65 | } | |
| 66 | } | |
| 67 | } |
| … | … | @@ -0,0 +1,34 @@ |
| 1 | { | |
| 2 | "name": "compact-encoding-bitfield", | |
| 3 | "version": "1.0.0", | |
| 4 | "description": "Compact codec for bitfields", | |
| 5 | "main": "index.js", | |
| 6 | "files": [ | |
| 7 | "index.js" | |
| 8 | ], | |
| 9 | "scripts": { | |
| 10 | "test": "standard && brittle test.mjs" | |
| 11 | }, | |
| 12 | "repository": { | |
| 13 | "type": "git", | |
| 14 | "url": "git+https://github.com/compact-encoding/compact-encoding-bitfield.git" | |
| 15 | }, | |
| 16 | "author": "Kasper Isager Dalsgarð <kasper@funktionel.co>", | |
| 17 | "license": "ISC", | |
| 18 | "bugs": { | |
| 19 | "url": "https://github.com/compact-encoding/compact-encoding-bitfield/issues" | |
| 20 | }, | |
| 21 | "homepage": "https://github.com/compact-encoding/compact-encoding-bitfield#readme", | |
| 22 | "dependencies": { | |
| 23 | "compact-encoding": "^2.4.1" | |
| 24 | }, | |
| 25 | "devDependencies": { | |
| 26 | "brittle": "^1.3.5", | |
| 27 | "standard": "^16.0.3" | |
| 28 | }, | |
| 29 | "standard": { | |
| 30 | "ignore": [ | |
| 31 | "__snapshots__/**" | |
| 32 | ] | |
| 33 | } | |
| 34 | } |
| … | … | @@ -0,0 +1,23 @@ |
| 1 | name: Build Status | |
| 2 | on: | |
| 3 | push: | |
| 4 | branches: | |
| 5 | - main | |
| 6 | pull_request: | |
| 7 | branches: | |
| 8 | - main | |
| 9 | jobs: | |
| 10 | build: | |
| 11 | strategy: | |
| 12 | matrix: | |
| 13 | node-version: [lts/*] | |
| 14 | os: [ubuntu-latest, macos-latest, windows-latest] | |
| 15 | runs-on: ${{ matrix.os }} | |
| 16 | steps: | |
| 17 | - uses: actions/checkout@v2 | |
| 18 | - name: Use Node.js ${{ matrix.node-version }} | |
| 19 | uses: actions/setup-node@v2 | |
| 20 | with: | |
| 21 | node-version: ${{ matrix.node-version }} | |
| 22 | - run: npm install | |
| 23 | - run: npm test |
| … | … | @@ -0,0 +1,15 @@ |
| 1 | ISC License | |
| 2 | ||
| 3 | Copyright (c) 2021 Contributors | |
| 4 | ||
| 5 | Permission to use, copy, modify, and/or distribute this software for any | |
| 6 | purpose with or without fee is hereby granted, provided that the above | |
| 7 | copyright notice and this permission notice appear in all copies. | |
| 8 | ||
| 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH | |
| 10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY | |
| 11 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, | |
| 12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM | |
| 13 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR | |
| 14 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR | |
| 15 | PERFORMANCE OF THIS SOFTWARE. |
| … | … | @@ -0,0 +1,168 @@ |
| 1 | # compact-encoding-net | |
| 2 | ||
| 3 | [compact-encoding](https://github.com/compact-encoding/compact-encoding) codecs for net types. | |
| 4 | ||
| 5 | ## Installation | |
| 6 | ||
| 7 | ```sh | |
| 8 | npm install compact-encoding-net | |
| 9 | ``` | |
| 10 | ||
| 11 | ## Codecs | |
| 12 | ||
| 13 | ### `port` | |
| 14 | ||
| 15 | Codec for 16 bit port numbers. | |
| 16 | ||
| 17 | ```js | |
| 18 | const { port } = require('compact-encoding-net') | |
| 19 | ``` | |
| 20 | ||
| 21 | #### Encoding | |
| 22 | ||
| 23 | ```js | |
| 24 | const buffer = cenc.encode(port, 8080) | |
| 25 | ``` | |
| 26 | ||
| 27 | #### Decoding | |
| 28 | ||
| 29 | ```js | |
| 30 | cenc.decode(port, buffer) | |
| 31 | // 8080 | |
| 32 | ``` | |
| 33 | ||
| 34 | ### `ipv4` | |
| 35 | ||
| 36 | Codec for IPv4 addresses. | |
| 37 | ||
| 38 | > :warning: The codec is only defined for valid IPv4 addresses. | |
| 39 | ||
| 40 | ```js | |
| 41 | const { ipv4 } = require('compact-encoding-net') | |
| 42 | ``` | |
| 43 | ||
| 44 | #### Encoding | |
| 45 | ||
| 46 | ```js | |
| 47 | const buffer = cenc.encode(ipv4, '127.0.0.1') | |
| 48 | ``` | |
| 49 | ||
| 50 | #### Decoding | |
| 51 | ||
| 52 | ```js | |
| 53 | cenc.decode(ipv4, buffer) | |
| 54 | // '127.0.0.1' | |
| 55 | ``` | |
| 56 | ||
| 57 | ### `ipv4Address` | |
| 58 | ||
| 59 | Codec for IPv4 addresses plus a port. | |
| 60 | ||
| 61 | ```js | |
| 62 | const { ipv4Address } = require('compact-encoding-net') | |
| 63 | ``` | |
| 64 | ||
| 65 | #### Encoding | |
| 66 | ||
| 67 | ```js | |
| 68 | const buffer = cenc.encode(ipv4, { host: '127.0.0.1', port: 8080 }) | |
| 69 | ``` | |
| 70 | ||
| 71 | #### Decoding | |
| 72 | ||
| 73 | ```js | |
| 74 | cenc.decode(ipv4Address, buffer) | |
| 75 | // { host: '127.0.0.1', port: 8080 } | |
| 76 | ``` | |
| 77 | ||
| 78 | ### `ipv6` | |
| 79 | ||
| 80 | Codec for IPv6 addresses. | |
| 81 | ||
| 82 | > :warning: The codec is only defined for valid IPv6 addresses. | |
| 83 | ||
| 84 | ```js | |
| 85 | const { ipv6 } = require('compact-encoding-net') | |
| 86 | ``` | |
| 87 | ||
| 88 | #### Encoding | |
| 89 | ||
| 90 | ```js | |
| 91 | const buffer = cenc.encode(ipv6, '::1') | |
| 92 | ``` | |
| 93 | ||
| 94 | #### Decoding | |
| 95 | ||
| 96 | ```js | |
| 97 | cenc.decode(ipv6, buffer) | |
| 98 | // '0:0:0:0:0:0:0:1' | |
| 99 | ``` | |
| 100 | ||
| 101 | ### `ipv6Address` | |
| 102 | ||
| 103 | Codec for IPv6 addresses plus a port. | |
| 104 | ||
| 105 | ```js | |
| 106 | const { ipv6Address } = require('compact-encoding-net') | |
| 107 | ``` | |
| 108 | ||
| 109 | #### Encoding | |
| 110 | ||
| 111 | ```js | |
| 112 | const buffer = cenc.encode(ipv6Address, { host: '::1', port: 8080 }) | |
| 113 | ``` | |
| 114 | ||
| 115 | #### Decoding | |
| 116 | ||
| 117 | ```js | |
| 118 | cenc.decode(ipv6Address, buffer) | |
| 119 | // { host: '0:0:0:0:0:0:0:1', port: 8080 } | |
| 120 | ``` | |
| 121 | ||
| 122 | ### `ip` | |
| 123 | ||
| 124 | Codec for dual IPv4/6 addresses. | |
| 125 | ||
| 126 | > :warning: The codec is only defined for valid IPv4 and IPv6 addresses. | |
| 127 | ||
| 128 | ```js | |
| 129 | const { ip } = require('compact-encoding-net') | |
| 130 | ``` | |
| 131 | ||
| 132 | #### Encoding | |
| 133 | ||
| 134 | ```js | |
| 135 | const buffer = cenc.encode(ip, '::1') | |
| 136 | ``` | |
| 137 | ||
| 138 | #### Decoding | |
| 139 | ||
| 140 | ```js | |
| 141 | cenc.decode(ip, buffer) | |
| 142 | // '0:0:0:0:0:0:0:1' | |
| 143 | ``` | |
| 144 | ||
| 145 | ### `ipAddress` | |
| 146 | ||
| 147 | Codec for dual IPv4/6 addresses plus a port. | |
| 148 | ||
| 149 | ```js | |
| 150 | const { ipAddress } = require('compact-encoding-net') | |
| 151 | ``` | |
| 152 | ||
| 153 | #### Encoding | |
| 154 | ||
| 155 | ```js | |
| 156 | const buffer = cenc.encode(ipAddress, { host: '::1', port: 8080 }) | |
| 157 | ``` | |
| 158 | ||
| 159 | #### Decoding | |
| 160 | ||
| 161 | ```js | |
| 162 | cenc.decode(ipv6Address, buffer) | |
| 163 | // { host: '0:0:0:0:0:0:0:1', family: 6, port: 8080 } | |
| 164 | ``` | |
| 165 | ||
| 166 | ## License | |
| 167 | ||
| 168 | ISC |
| … | … | @@ -0,0 +1,33 @@ |
| 1 | const bench = require('nanobench') | |
| 2 | const c = require('compact-encoding') | |
| 3 | const { ipv4 } = require('../') | |
| 4 | ||
| 5 | const ip = '1.2.3.4' | |
| 6 | ||
| 7 | bench('ipv4 encode', function (b) { | |
| 8 | b.start() | |
| 9 | ||
| 10 | let result | |
| 11 | ||
| 12 | for (let i = 0; i < 10000000; i++) { | |
| 13 | result = c.encode(ipv4, ip) | |
| 14 | } | |
| 15 | ||
| 16 | b.log(result.toString('hex')) | |
| 17 | b.end() | |
| 18 | }) | |
| 19 | ||
| 20 | bench('ipv4 encode', function (b) { | |
| 21 | const buffer = c.encode(ipv4, ip) | |
| 22 | ||
| 23 | b.start() | |
| 24 | ||
| 25 | let result | |
| 26 | ||
| 27 | for (let i = 0; i < 10000000; i++) { | |
| 28 | result = c.decode(ipv4, buffer) | |
| 29 | } | |
| 30 | ||
| 31 | b.log(JSON.stringify(result)) | |
| 32 | b.end() | |
| 33 | }) |
| … | … | @@ -0,0 +1,33 @@ |
| 1 | const bench = require('nanobench') | |
| 2 | const c = require('compact-encoding') | |
| 3 | const { ipv6 } = require('../') | |
| 4 | ||
| 5 | const ip = '1:2:3:4:5:6:7:8' | |
| 6 | ||
| 7 | bench('ipv6 encode', function (b) { | |
| 8 | b.start() | |
| 9 | ||
| 10 | let result | |
| 11 | ||
| 12 | for (let i = 0; i < 10000000; i++) { | |
| 13 | result = c.encode(ipv6, ip) | |
| 14 | } | |
| 15 | ||
| 16 | b.log(result.toString('hex')) | |
| 17 | b.end() | |
| 18 | }) | |
| 19 | ||
| 20 | bench('ipv6 encode', function (b) { | |
| 21 | const buffer = c.encode(ipv6, ip) | |
| 22 | ||
| 23 | b.start() | |
| 24 | ||
| 25 | let result | |
| 26 | ||
| 27 | for (let i = 0; i < 10000000; i++) { | |
| 28 | result = c.decode(ipv6, buffer) | |
| 29 | } | |
| 30 | ||
| 31 | b.log(JSON.stringify(result)) | |
| 32 | b.end() | |
| 33 | }) |
| … | … | @@ -0,0 +1,164 @@ |
| 1 | const c = require('compact-encoding') | |
| 2 | ||
| 3 | const port = c.uint16 | |
| 4 | ||
| 5 | const address = (host, family) => { | |
| 6 | return { | |
| 7 | preencode (state, m) { | |
| 8 | host.preencode(state, m.host) | |
| 9 | port.preencode(state, m.port) | |
| 10 | }, | |
| 11 | encode (state, m) { | |
| 12 | host.encode(state, m.host) | |
| 13 | port.encode(state, m.port) | |
| 14 | }, | |
| 15 | decode (state) { | |
| 16 | return { | |
| 17 | host: host.decode(state), | |
| 18 | family, | |
| 19 | port: port.decode(state) | |
| 20 | } | |
| 21 | } | |
| 22 | } | |
| 23 | } | |
| 24 | ||
| 25 | const ipv4 = { | |
| 26 | preencode (state) { | |
| 27 | state.end += 4 | |
| 28 | }, | |
| 29 | encode (state, string) { | |
| 30 | const start = state.start | |
| 31 | const end = start + 4 | |
| 32 | ||
| 33 | let i = 0 | |
| 34 | ||
| 35 | while (i < string.length) { | |
| 36 | let n = 0 | |
| 37 | let c | |
| 38 | ||
| 39 | while (i < string.length && (c = string.charCodeAt(i++)) !== /* . */ 0x2e) { | |
| 40 | n = n * 10 + (c - /* 0 */ 0x30) | |
| 41 | } | |
| 42 | ||
| 43 | state.buffer[state.start++] = n | |
| 44 | } | |
| 45 | ||
| 46 | state.start = end | |
| 47 | }, | |
| 48 | decode (state) { | |
| 49 | if (state.end - state.start < 4) throw new Error('Out of bounds') | |
| 50 | return ( | |
| 51 | state.buffer[state.start++] + '.' + | |
| 52 | state.buffer[state.start++] + '.' + | |
| 53 | state.buffer[state.start++] + '.' + | |
| 54 | state.buffer[state.start++] | |
| 55 | ) | |
| 56 | } | |
| 57 | } | |
| 58 | ||
| 59 | const ipv4Address = address(ipv4, 4) | |
| 60 | ||
| 61 | const ipv6 = { | |
| 62 | preencode (state) { | |
| 63 | state.end += 16 | |
| 64 | }, | |
| 65 | encode (state, string) { | |
| 66 | const start = state.start | |
| 67 | const end = start + 16 | |
| 68 | ||
| 69 | let i = 0 | |
| 70 | let split = null | |
| 71 | ||
| 72 | while (i < string.length) { | |
| 73 | let n = 0 | |
| 74 | let c | |
| 75 | ||
| 76 | while (i < string.length && (c = string.charCodeAt(i++)) !== /* : */ 0x3a) { | |
| 77 | if (c >= 0x30 && c <= 0x39) n = n * 0x10 + (c - /* 0 */ 0x30) | |
| 78 | else if (c >= 0x41 && c <= 0x46) n = n * 0x10 + (c - /* A */ 0x41 + 10) | |
| 79 | else if (c >= 0x61 && c <= 0x66) n = n * 0x10 + (c - /* a */ 0x61 + 10) | |
| 80 | } | |
| 81 | ||
| 82 | state.buffer[state.start++] = n >>> 8 | |
| 83 | state.buffer[state.start++] = n | |
| 84 | ||
| 85 | if (i < string.length && string.charCodeAt(i) === /* : */ 0x3a) { | |
| 86 | i++ | |
| 87 | split = state.start | |
| 88 | } | |
| 89 | } | |
| 90 | ||
| 91 | if (split !== null) { | |
| 92 | const offset = end - state.start | |
| 93 | state.buffer | |
| 94 | .copyWithin(split + offset, split) | |
| 95 | .fill(0, split, split + offset) | |
| 96 | } | |
| 97 | ||
| 98 | state.start = end | |
| 99 | }, | |
| 100 | decode (state) { | |
| 101 | if (state.end - state.start < 16) throw new Error('Out of bounds') | |
| 102 | return ( | |
| 103 | (state.buffer[state.start++] * 256 + state.buffer[state.start++]).toString(16) + ':' + | |
| 104 | (state.buffer[state.start++] * 256 + state.buffer[state.start++]).toString(16) + ':' + | |
| 105 | (state.buffer[state.start++] * 256 + state.buffer[state.start++]).toString(16) + ':' + | |
| 106 | (state.buffer[state.start++] * 256 + state.buffer[state.start++]).toString(16) + ':' + | |
| 107 | (state.buffer[state.start++] * 256 + state.buffer[state.start++]).toString(16) + ':' + | |
| 108 | (state.buffer[state.start++] * 256 + state.buffer[state.start++]).toString(16) + ':' + | |
| 109 | (state.buffer[state.start++] * 256 + state.buffer[state.start++]).toString(16) + ':' + | |
| 110 | (state.buffer[state.start++] * 256 + state.buffer[state.start++]).toString(16) | |
| 111 | ) | |
| 112 | } | |
| 113 | } | |
| 114 | ||
| 115 | const ipv6Address = address(ipv6, 6) | |
| 116 | ||
| 117 | const ip = { | |
| 118 | preencode (state, string) { | |
| 119 | const family = string.includes(':') ? 6 : 4 | |
| 120 | c.uint8.preencode(state, family) | |
| 121 | if (family === 4) ipv4.preencode(state) | |
| 122 | else ipv6.preencode(state) | |
| 123 | }, | |
| 124 | encode (state, string) { | |
| 125 | const family = string.includes(':') ? 6 : 4 | |
| 126 | c.uint8.encode(state, family) | |
| 127 | if (family === 4) ipv4.encode(state, string) | |
| 128 | else ipv6.encode(state, string) | |
| 129 | }, | |
| 130 | decode (state) { | |
| 131 | const family = c.uint8.decode(state) | |
| 132 | if (family === 4) return ipv4.decode(state) | |
| 133 | else return ipv6.decode(state) | |
| 134 | } | |
| 135 | } | |
| 136 | ||
| 137 | const ipAddress = { | |
| 138 | preencode (state, m) { | |
| 139 | ip.preencode(state, m.host) | |
| 140 | port.preencode(state, m.port) | |
| 141 | }, | |
| 142 | encode (state, m) { | |
| 143 | ip.encode(state, m.host) | |
| 144 | port.encode(state, m.port) | |
| 145 | }, | |
| 146 | decode (state) { | |
| 147 | const family = c.uint8.decode(state) | |
| 148 | return { | |
| 149 | host: family === 4 ? ipv4.decode(state) : ipv6.decode(state), | |
| 150 | family, | |
| 151 | port: port.decode(state) | |
| 152 | } | |
| 153 | } | |
| 154 | } | |
| 155 | ||
| 156 | module.exports = { | |
| 157 | port, | |
| 158 | ipv4, | |
| 159 | ipv4Address, | |
| 160 | ipv6, | |
| 161 | ipv6Address, | |
| 162 | ip, | |
| 163 | ipAddress | |
| 164 | } |
| … | … | @@ -0,0 +1,27 @@ |
| 1 | { | |
| 2 | "name": "compact-encoding-net", | |
| 3 | "version": "1.2.0", | |
| 4 | "description": "Compact codecs for net types", | |
| 5 | "main": "index.js", | |
| 6 | "scripts": { | |
| 7 | "test": "standard && brittle test.mjs" | |
| 8 | }, | |
| 9 | "repository": { | |
| 10 | "type": "git", | |
| 11 | "url": "git+https://github.com/compact-encoding/compact-encoding-net.git" | |
| 12 | }, | |
| 13 | "author": "Kasper Isager Dalsgarð <kasper@funktionel.co>", | |
| 14 | "license": "ISC", | |
| 15 | "bugs": { | |
| 16 | "url": "https://github.com/compact-encoding/compact-encoding-net/issues" | |
| 17 | }, | |
| 18 | "homepage": "https://github.com/compact-encoding/compact-encoding-net#readme", | |
| 19 | "dependencies": { | |
| 20 | "compact-encoding": "^2.4.1" | |
| 21 | }, | |
| 22 | "devDependencies": { | |
| 23 | "brittle": "^1.3.5", | |
| 24 | "nanobench": "^2.1.1", | |
| 25 | "standard": "^16.0.3" | |
| 26 | } | |
| 27 | } |
| … | … | @@ -0,0 +1,128 @@ |
| 1 | import test from 'brittle' | |
| 2 | import c from 'compact-encoding' | |
| 3 | ||
| 4 | import { port, ipv4, ipv6, ipv4Address, ipv6Address, ip, ipAddress } from './index.js' | |
| 5 | ||
| 6 | test('port', (t) => { | |
| 7 | const p = 0x1234 | |
| 8 | const buf = Buffer.from([0x34, 0x12]) | |
| 9 | ||
| 10 | t.alike(c.encode(port, p), buf) | |
| 11 | t.alike(c.decode(port, buf), p) | |
| 12 | }) | |
| 13 | ||
| 14 | test('ipv4', (t) => { | |
| 15 | const ip = '1.2.3.4' | |
| 16 | const buf = Buffer.from([1, 2, 3, 4]) | |
| 17 | ||
| 18 | t.alike(c.encode(ipv4, ip), buf) | |
| 19 | t.alike(c.decode(ipv4, buf), ip) | |
| 20 | }) | |
| 21 | ||
| 22 | test('ipv4 + port', (t) => { | |
| 23 | const host = '1.2.3.4' | |
| 24 | const port = 1234 | |
| 25 | ||
| 26 | t.alike(c.decode(ipv4Address, c.encode(ipv4Address, { host, port })), { | |
| 27 | host, | |
| 28 | family: 4, | |
| 29 | port | |
| 30 | }) | |
| 31 | }) | |
| 32 | ||
| 33 | test('ipv6', (t) => { | |
| 34 | const ip = '1:2:3:4:5:6:7:8' | |
| 35 | const buf = Buffer.from([0, 1, 0, 2, 0, 3, 0, 4, 0, 5, 0, 6, 0, 7, 0, 8]) | |
| 36 | ||
| 37 | t.alike(c.encode(ipv6, ip), buf) | |
| 38 | t.alike(c.decode(ipv6, buf), ip) | |
| 39 | ||
| 40 | t.test('abbreviated', (t) => { | |
| 41 | const buf = Buffer.from([0, 1, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 8]) | |
| 42 | ||
| 43 | t.alike(c.encode(ipv6, '1:2::7:8'), buf) | |
| 44 | t.alike(c.decode(ipv6, buf), '1:2:0:0:0:0:7:8') | |
| 45 | }) | |
| 46 | ||
| 47 | t.test('prefix abbreviated', (t) => { | |
| 48 | const buf = Buffer.from([0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 6, 0, 7, 0, 8]) | |
| 49 | ||
| 50 | t.alike(c.encode(ipv6, '::5:6:7:8'), buf) | |
| 51 | t.alike(c.decode(ipv6, buf), '0:0:0:0:5:6:7:8') | |
| 52 | }) | |
| 53 | ||
| 54 | t.test('suffix abbreviated', (t) => { | |
| 55 | const buf = Buffer.from([0, 1, 0, 2, 0, 3, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0]) | |
| 56 | ||
| 57 | t.alike(c.encode(ipv6, '1:2:3:4::'), buf) | |
| 58 | t.alike(c.decode(ipv6, buf), '1:2:3:4:0:0:0:0') | |
| 59 | }) | |
| 60 | ||
| 61 | t.test('any', (t) => { | |
| 62 | const buf = Buffer.from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) | |
| 63 | ||
| 64 | t.alike(c.encode(ipv6, '::'), buf) | |
| 65 | t.alike(c.decode(ipv6, buf), '0:0:0:0:0:0:0:0') | |
| 66 | }) | |
| 67 | ||
| 68 | t.test('lowercase hex', (t) => { | |
| 69 | const buf = Buffer.from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xab, 0xcd]) | |
| 70 | ||
| 71 | t.alike(c.encode(ipv6, '::abcd'), buf) | |
| 72 | t.alike(c.decode(ipv6, buf), '0:0:0:0:0:0:0:abcd') | |
| 73 | }) | |
| 74 | ||
| 75 | t.test('uppercase hex', (t) => { | |
| 76 | const buf = Buffer.from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xab, 0xcd]) | |
| 77 | ||
| 78 | t.alike(c.encode(ipv6, '::ABCD'), buf) | |
| 79 | t.alike(c.decode(ipv6, buf), '0:0:0:0:0:0:0:abcd') | |
| 80 | }) | |
| 81 | }) | |
| 82 | ||
| 83 | test('ipv6 + port', (t) => { | |
| 84 | const host = '1:2:3:4:5:6:7:8' | |
| 85 | const port = 1234 | |
| 86 | ||
| 87 | t.alike(c.decode(ipv6Address, c.encode(ipv6Address, { host, port })), { | |
| 88 | host, | |
| 89 | family: 6, | |
| 90 | port | |
| 91 | }) | |
| 92 | }) | |
| 93 | ||
| 94 | test('dual ip', (t) => { | |
| 95 | { | |
| 96 | const host = '1.2.3.4' | |
| 97 | ||
| 98 | t.alike(c.decode(ip, c.encode(ip, host)), host, 'ipv4') | |
| 99 | } | |
| 100 | { | |
| 101 | const host = '1:2:3:4:5:6:7:8' | |
| 102 | ||
| 103 | t.alike(c.decode(ip, c.encode(ip, host)), host, 'ipv6') | |
| 104 | } | |
| 105 | }) | |
| 106 | ||
| 107 | test('dual ip + port', (t) => { | |
| 108 | const port = 1234 | |
| 109 | ||
| 110 | { | |
| 111 | const host = '1.2.3.4' | |
| 112 | ||
| 113 | t.alike(c.decode(ipAddress, c.encode(ipAddress, { host, port })), { | |
| 114 | host, | |
| 115 | family: 4, | |
| 116 | port | |
| 117 | }, 'ipv4') | |
| 118 | } | |
| 119 | { | |
| 120 | const host = '1:2:3:4:5:6:7:8' | |
| 121 | ||
| 122 | t.alike(c.decode(ipAddress, c.encode(ipAddress, { host, port })), { | |
| 123 | host, | |
| 124 | family: 6, | |
| 125 | port | |
| 126 | }, 'ipv6') | |
| 127 | } | |
| 128 | }) |
| … | … | @@ -0,0 +1,23 @@ |
| 1 | Copyright 2013 Thorsten Lorenz. | |
| 2 | All rights reserved. | |
| 3 | ||
| 4 | Permission is hereby granted, free of charge, to any person | |
| 5 | obtaining a copy of this software and associated documentation | |
| 6 | files (the "Software"), to deal in the Software without | |
| 7 | restriction, including without limitation the rights to use, | |
| 8 | copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| 9 | copies of the Software, and to permit persons to whom the | |
| 10 | Software is furnished to do so, subject to the following | |
| 11 | conditions: | |
| 12 | ||
| 13 | The above copyright notice and this permission notice shall be | |
| 14 | included in all copies or substantial portions of the Software. | |
| 15 | ||
| 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |
| 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES | |
| 18 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | |
| 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT | |
| 20 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, | |
| 21 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | |
| 22 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR | |
| 23 | OTHER DEALINGS IN THE SOFTWARE. |
| … | … | @@ -0,0 +1,206 @@ |
| 1 | # convert-source-map [![Build Status][ci-image]][ci-url] | |
| 2 | ||
| 3 | Converts a source-map from/to different formats and allows adding/changing properties. | |
| 4 | ||
| 5 | ```js | |
| 6 | var convert = require('convert-source-map'); | |
| 7 | ||
| 8 | var json = convert | |
| 9 | .fromComment('//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYnVpbGQvZm9vLm1pbi5qcyIsInNvdXJjZXMiOlsic3JjL2Zvby5qcyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSIsInNvdXJjZVJvb3QiOiIvIn0=') | |
| 10 | .toJSON(); | |
| 11 | ||
| 12 | var modified = convert | |
| 13 | .fromComment('//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYnVpbGQvZm9vLm1pbi5qcyIsInNvdXJjZXMiOlsic3JjL2Zvby5qcyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSIsInNvdXJjZVJvb3QiOiIvIn0=') | |
| 14 | .setProperty('sources', [ 'SRC/FOO.JS' ]) | |
| 15 | .toJSON(); | |
| 16 | ||
| 17 | console.log(json); | |
| 18 | console.log(modified); | |
| 19 | ``` | |
| 20 | ||
| 21 | ```json | |
| 22 | {"version":3,"file":"build/foo.min.js","sources":["src/foo.js"],"names":[],"mappings":"AAAA","sourceRoot":"/"} | |
| 23 | {"version":3,"file":"build/foo.min.js","sources":["SRC/FOO.JS"],"names":[],"mappings":"AAAA","sourceRoot":"/"} | |
| 24 | ``` | |
| 25 | ||
| 26 | ## Upgrading | |
| 27 | ||
| 28 | Prior to v2.0.0, the `fromMapFileComment` and `fromMapFileSource` functions took a String directory path and used that to resolve & read the source map file from the filesystem. However, this made the library limited to nodejs environments and broke on sources with querystrings. | |
| 29 | ||
| 30 | In v2.0.0, you now need to pass a function that does the file reading. It will receive the source filename as a String that you can resolve to a filesystem path, URL, or anything else. | |
| 31 | ||
| 32 | If you are using `convert-source-map` in nodejs and want the previous behavior, you'll use a function like such: | |
| 33 | ||
| 34 | ```diff | |
| 35 | + var fs = require('fs'); // Import the fs module to read a file | |
| 36 | + var path = require('path'); // Import the path module to resolve a path against your directory | |
| 37 | - var conv = convert.fromMapFileSource(css, '../my-dir'); | |
| 38 | + var conv = convert.fromMapFileSource(css, function (filename) { | |
| 39 | + return fs.readFileSync(path.resolve('../my-dir', filename), 'utf-8'); | |
| 40 | + }); | |
| 41 | ``` | |
| 42 | ||
| 43 | ## API | |
| 44 | ||
| 45 | ### fromObject(obj) | |
| 46 | ||
| 47 | Returns source map converter from given object. | |
| 48 | ||
| 49 | ### fromJSON(json) | |
| 50 | ||
| 51 | Returns source map converter from given json string. | |
| 52 | ||
| 53 | ### fromURI(uri) | |
| 54 | ||
| 55 | Returns source map converter from given uri encoded json string. | |
| 56 | ||
| 57 | ### fromBase64(base64) | |
| 58 | ||
| 59 | Returns source map converter from given base64 encoded json string. | |
| 60 | ||
| 61 | ### fromComment(comment) | |
| 62 | ||
| 63 | Returns source map converter from given base64 or uri encoded json string prefixed with `//# sourceMappingURL=...`. | |
| 64 | ||
| 65 | ### fromMapFileComment(comment, readMap) | |
| 66 | ||
| 67 | Returns source map converter from given `filename` by parsing `//# sourceMappingURL=filename`. | |
| 68 | ||
| 69 | `readMap` must be a function which receives the source map filename and returns either a String or Buffer of the source map (if read synchronously), or a `Promise` containing a String or Buffer of the source map (if read asynchronously). | |
| 70 | ||
| 71 | If `readMap` doesn't return a `Promise`, `fromMapFileComment` will return a source map converter synchronously. | |
| 72 | ||
| 73 | If `readMap` returns a `Promise`, `fromMapFileComment` will also return `Promise`. The `Promise` will be either resolved with the source map converter or rejected with an error. | |
| 74 | ||
| 75 | #### Examples | |
| 76 | ||
| 77 | **Synchronous read in Node.js:** | |
| 78 | ||
| 79 | ```js | |
| 80 | var convert = require('convert-source-map'); | |
| 81 | var fs = require('fs'); | |
| 82 | ||
| 83 | function readMap(filename) { | |
| 84 | return fs.readFileSync(filename, 'utf8'); | |
| 85 | } | |
| 86 | ||
| 87 | var json = convert | |
| 88 | .fromMapFileComment('//# sourceMappingURL=map-file-comment.css.map', readMap) | |
| 89 | .toJSON(); | |
| 90 | console.log(json); | |
| 91 | ``` | |
| 92 | ||
| 93 | ||
| 94 | **Asynchronous read in Node.js:** | |
| 95 | ||
| 96 | ```js | |
| 97 | var convert = require('convert-source-map'); | |
| 98 | var { promises: fs } = require('fs'); // Notice the `promises` import | |
| 99 | ||
| 100 | function readMap(filename) { | |
| 101 | return fs.readFile(filename, 'utf8'); | |
| 102 | } | |
| 103 | ||
| 104 | var converter = await convert.fromMapFileComment('//# sourceMappingURL=map-file-comment.css.map', readMap) | |
| 105 | var json = converter.toJSON(); | |
| 106 | console.log(json); | |
| 107 | ``` | |
| 108 | ||
| 109 | **Asynchronous read in the browser:** | |
| 110 | ||
| 111 | ```js | |
| 112 | var convert = require('convert-source-map'); | |
| 113 | ||
| 114 | async function readMap(url) { | |
| 115 | const res = await fetch(url); | |
| 116 | return res.text(); | |
| 117 | } | |
| 118 | ||
| 119 | const converter = await convert.fromMapFileComment('//# sourceMappingURL=map-file-comment.css.map', readMap) | |
| 120 | var json = converter.toJSON(); | |
| 121 | console.log(json); | |
| 122 | ``` | |
| 123 | ||
| 124 | ### fromSource(source) | |
| 125 | ||
| 126 | Finds last sourcemap comment in file and returns source map converter or returns `null` if no source map comment was found. | |
| 127 | ||
| 128 | ### fromMapFileSource(source, readMap) | |
| 129 | ||
| 130 | Finds last sourcemap comment in file and returns source map converter or returns `null` if no source map comment was found. | |
| 131 | ||
| 132 | `readMap` must be a function which receives the source map filename and returns either a String or Buffer of the source map (if read synchronously), or a `Promise` containing a String or Buffer of the source map (if read asynchronously). | |
| 133 | ||
| 134 | If `readMap` doesn't return a `Promise`, `fromMapFileSource` will return a source map converter synchronously. | |
| 135 | ||
| 136 | If `readMap` returns a `Promise`, `fromMapFileSource` will also return `Promise`. The `Promise` will be either resolved with the source map converter or rejected with an error. | |
| 137 | ||
| 138 | ### toObject() | |
| 139 | ||
| 140 | Returns a copy of the underlying source map. | |
| 141 | ||
| 142 | ### toJSON([space]) | |
| 143 | ||
| 144 | Converts source map to json string. If `space` is given (optional), this will be passed to | |
| 145 | [JSON.stringify](https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/JSON/stringify) when the | |
| 146 | JSON string is generated. | |
| 147 | ||
| 148 | ### toURI() | |
| 149 | ||
| 150 | Converts source map to uri encoded json string. | |
| 151 | ||
| 152 | ### toBase64() | |
| 153 | ||
| 154 | Converts source map to base64 encoded json string. | |
| 155 | ||
| 156 | ### toComment([options]) | |
| 157 | ||
| 158 | Converts source map to an inline comment that can be appended to the source-file. | |
| 159 | ||
| 160 | By default, the comment is formatted like: `//# sourceMappingURL=...`, which you would | |
| 161 | normally see in a JS source file. | |
| 162 | ||
| 163 | When `options.encoding == 'uri'`, the data will be uri encoded, otherwise they will be base64 encoded. | |
| 164 | ||
| 165 | When `options.multiline == true`, the comment is formatted like: `/*# sourceMappingURL=... */`, which you would find in a CSS source file. | |
| 166 | ||
| 167 | ### addProperty(key, value) | |
| 168 | ||
| 169 | Adds given property to the source map. Throws an error if property already exists. | |
| 170 | ||
| 171 | ### setProperty(key, value) | |
| 172 | ||
| 173 | Sets given property to the source map. If property doesn't exist it is added, otherwise its value is updated. | |
| 174 | ||
| 175 | ### getProperty(key) | |
| 176 | ||
| 177 | Gets given property of the source map. | |
| 178 | ||
| 179 | ### removeComments(src) | |
| 180 | ||
| 181 | Returns `src` with all source map comments removed | |
| 182 | ||
| 183 | ### removeMapFileComments(src) | |
| 184 | ||
| 185 | Returns `src` with all source map comments pointing to map files removed. | |
| 186 | ||
| 187 | ### commentRegex | |
| 188 | ||
| 189 | Provides __a fresh__ RegExp each time it is accessed. Can be used to find source map comments. | |
| 190 | ||
| 191 | Breaks down a source map comment into groups: Groups: 1: media type, 2: MIME type, 3: charset, 4: encoding, 5: data. | |
| 192 | ||
| 193 | ### mapFileCommentRegex | |
| 194 | ||
| 195 | Provides __a fresh__ RegExp each time it is accessed. Can be used to find source map comments pointing to map files. | |
| 196 | ||
| 197 | ### generateMapFileComment(file, [options]) | |
| 198 | ||
| 199 | Returns a comment that links to an external source map via `file`. | |
| 200 | ||
| 201 | By default, the comment is formatted like: `//# sourceMappingURL=...`, which you would normally see in a JS source file. | |
| 202 | ||
| 203 | When `options.multiline == true`, the comment is formatted like: `/*# sourceMappingURL=... */`, which you would find in a CSS source file. | |
| 204 | ||
| 205 | [ci-url]: https://github.com/thlorenz/convert-source-map/actions?query=workflow:ci | |
| 206 | [ci-image]: https://img.shields.io/github/workflow/status/thlorenz/convert-source-map/CI?style=flat-square |
| … | … | @@ -0,0 +1,233 @@ |
| 1 | 'use strict'; | |
| 2 | ||
| 3 | Object.defineProperty(exports, 'commentRegex', { | |
| 4 | get: function getCommentRegex () { | |
| 5 | // Groups: 1: media type, 2: MIME type, 3: charset, 4: encoding, 5: data. | |
| 6 | return /^\s*?\/[\/\*][@#]\s+?sourceMappingURL=data:(((?:application|text)\/json)(?:;charset=([^;,]+?)?)?)?(?:;(base64))?,(.*?)$/mg; | |
| 7 | } | |
| 8 | }); | |
| 9 | ||
| 10 | ||
| 11 | Object.defineProperty(exports, 'mapFileCommentRegex', { | |
| 12 | get: function getMapFileCommentRegex () { | |
| 13 | // Matches sourceMappingURL in either // or /* comment styles. | |
| 14 | return /(?:\/\/[@#][ \t]+?sourceMappingURL=([^\s'"`]+?)[ \t]*?$)|(?:\/\*[@#][ \t]+sourceMappingURL=([^*]+?)[ \t]*?(?:\*\/){1}[ \t]*?$)/mg; | |
| 15 | } | |
| 16 | }); | |
| 17 | ||
| 18 | var decodeBase64; | |
| 19 | if (typeof Buffer !== 'undefined') { | |
| 20 | if (typeof Buffer.from === 'function') { | |
| 21 | decodeBase64 = decodeBase64WithBufferFrom; | |
| 22 | } else { | |
| 23 | decodeBase64 = decodeBase64WithNewBuffer; | |
| 24 | } | |
| 25 | } else { | |
| 26 | decodeBase64 = decodeBase64WithAtob; | |
| 27 | } | |
| 28 | ||
| 29 | function decodeBase64WithBufferFrom(base64) { | |
| 30 | return Buffer.from(base64, 'base64').toString(); | |
| 31 | } | |
| 32 | ||
| 33 | function decodeBase64WithNewBuffer(base64) { | |
| 34 | if (typeof value === 'number') { | |
| 35 | throw new TypeError('The value to decode must not be of type number.'); | |
| 36 | } | |
| 37 | return new Buffer(base64, 'base64').toString(); | |
| 38 | } | |
| 39 | ||
| 40 | function decodeBase64WithAtob(base64) { | |
| 41 | return decodeURIComponent(escape(atob(base64))); | |
| 42 | } | |
| 43 | ||
| 44 | function stripComment(sm) { | |
| 45 | return sm.split(',').pop(); | |
| 46 | } | |
| 47 | ||
| 48 | function readFromFileMap(sm, read) { | |
| 49 | var r = exports.mapFileCommentRegex.exec(sm); | |
| 50 | // for some odd reason //# .. captures in 1 and /* .. */ in 2 | |
| 51 | var filename = r[1] || r[2]; | |
| 52 | ||
| 53 | try { | |
| 54 | var sm = read(filename); | |
| 55 | if (sm != null && typeof sm.catch === 'function') { | |
| 56 | return sm.catch(throwError); | |
| 57 | } else { | |
| 58 | return sm; | |
| 59 | } | |
| 60 | } catch (e) { | |
| 61 | throwError(e); | |
| 62 | } | |
| 63 | ||
| 64 | function throwError(e) { | |
| 65 | throw new Error('An error occurred while trying to read the map file at ' + filename + '\n' + e.stack); | |
| 66 | } | |
| 67 | } | |
| 68 | ||
| 69 | function Converter (sm, opts) { | |
| 70 | opts = opts || {}; | |
| 71 | ||
| 72 | if (opts.hasComment) { | |
| 73 | sm = stripComment(sm); | |
| 74 | } | |
| 75 | ||
| 76 | if (opts.encoding === 'base64') { | |
| 77 | sm = decodeBase64(sm); | |
| 78 | } else if (opts.encoding === 'uri') { | |
| 79 | sm = decodeURIComponent(sm); | |
| 80 | } | |
| 81 | ||
| 82 | if (opts.isJSON || opts.encoding) { | |
| 83 | sm = JSON.parse(sm); | |
| 84 | } | |
| 85 | ||
| 86 | this.sourcemap = sm; | |
| 87 | } | |
| 88 | ||
| 89 | Converter.prototype.toJSON = function (space) { | |
| 90 | return JSON.stringify(this.sourcemap, null, space); | |
| 91 | }; | |
| 92 | ||
| 93 | if (typeof Buffer !== 'undefined') { | |
| 94 | if (typeof Buffer.from === 'function') { | |
| 95 | Converter.prototype.toBase64 = encodeBase64WithBufferFrom; | |
| 96 | } else { | |
| 97 | Converter.prototype.toBase64 = encodeBase64WithNewBuffer; | |
| 98 | } | |
| 99 | } else { | |
| 100 | Converter.prototype.toBase64 = encodeBase64WithBtoa; | |
| 101 | } | |
| 102 | ||
| 103 | function encodeBase64WithBufferFrom() { | |
| 104 | var json = this.toJSON(); | |
| 105 | return Buffer.from(json, 'utf8').toString('base64'); | |
| 106 | } | |
| 107 | ||
| 108 | function encodeBase64WithNewBuffer() { | |
| 109 | var json = this.toJSON(); | |
| 110 | if (typeof json === 'number') { | |
| 111 | throw new TypeError('The json to encode must not be of type number.'); | |
| 112 | } | |
| 113 | return new Buffer(json, 'utf8').toString('base64'); | |
| 114 | } | |
| 115 | ||
| 116 | function encodeBase64WithBtoa() { | |
| 117 | var json = this.toJSON(); | |
| 118 | return btoa(unescape(encodeURIComponent(json))); | |
| 119 | } | |
| 120 | ||
| 121 | Converter.prototype.toURI = function () { | |
| 122 | var json = this.toJSON(); | |
| 123 | return encodeURIComponent(json); | |
| 124 | }; | |
| 125 | ||
| 126 | Converter.prototype.toComment = function (options) { | |
| 127 | var encoding, content, data; | |
| 128 | if (options != null && options.encoding === 'uri') { | |
| 129 | encoding = ''; | |
| 130 | content = this.toURI(); | |
| 131 | } else { | |
| 132 | encoding = ';base64'; | |
| 133 | content = this.toBase64(); | |
| 134 | } | |
| 135 | data = 'sourceMappingURL=data:application/json;charset=utf-8' + encoding + ',' + content; | |
| 136 | return options != null && options.multiline ? '/*# ' + data + ' */' : '//# ' + data; | |
| 137 | }; | |
| 138 | ||
| 139 | // returns copy instead of original | |
| 140 | Converter.prototype.toObject = function () { | |
| 141 | return JSON.parse(this.toJSON()); | |
| 142 | }; | |
| 143 | ||
| 144 | Converter.prototype.addProperty = function (key, value) { | |
| 145 | if (this.sourcemap.hasOwnProperty(key)) throw new Error('property "' + key + '" already exists on the sourcemap, use set property instead'); | |
| 146 | return this.setProperty(key, value); | |
| 147 | }; | |
| 148 | ||
| 149 | Converter.prototype.setProperty = function (key, value) { | |
| 150 | this.sourcemap[key] = value; | |
| 151 | return this; | |
| 152 | }; | |
| 153 | ||
| 154 | Converter.prototype.getProperty = function (key) { | |
| 155 | return this.sourcemap[key]; | |
| 156 | }; | |
| 157 | ||
| 158 | exports.fromObject = function (obj) { | |
| 159 | return new Converter(obj); | |
| 160 | }; | |
| 161 | ||
| 162 | exports.fromJSON = function (json) { | |
| 163 | return new Converter(json, { isJSON: true }); | |
| 164 | }; | |
| 165 | ||
| 166 | exports.fromURI = function (uri) { | |
| 167 | return new Converter(uri, { encoding: 'uri' }); | |
| 168 | }; | |
| 169 | ||
| 170 | exports.fromBase64 = function (base64) { | |
| 171 | return new Converter(base64, { encoding: 'base64' }); | |
| 172 | }; | |
| 173 | ||
| 174 | exports.fromComment = function (comment) { | |
| 175 | var m, encoding; | |
| 176 | comment = comment | |
| 177 | .replace(/^\/\*/g, '//') | |
| 178 | .replace(/\*\/$/g, ''); | |
| 179 | m = exports.commentRegex.exec(comment); | |
| 180 | encoding = m && m[4] || 'uri'; | |
| 181 | return new Converter(comment, { encoding: encoding, hasComment: true }); | |
| 182 | }; | |
| 183 | ||
| 184 | function makeConverter(sm) { | |
| 185 | return new Converter(sm, { isJSON: true }); | |
| 186 | } | |
| 187 | ||
| 188 | exports.fromMapFileComment = function (comment, read) { | |
| 189 | if (typeof read === 'string') { | |
| 190 | throw new Error( | |
| 191 | 'String directory paths are no longer supported with `fromMapFileComment`\n' + | |
| 192 | 'Please review the Upgrading documentation at https://github.com/thlorenz/convert-source-map#upgrading' | |
| 193 | ) | |
| 194 | } | |
| 195 | ||
| 196 | var sm = readFromFileMap(comment, read); | |
| 197 | if (sm != null && typeof sm.then === 'function') { | |
| 198 | return sm.then(makeConverter); | |
| 199 | } else { | |
| 200 | return makeConverter(sm); | |
| 201 | } | |
| 202 | }; | |
| 203 | ||
| 204 | // Finds last sourcemap comment in file or returns null if none was found | |
| 205 | exports.fromSource = function (content) { | |
| 206 | var m = content.match(exports.commentRegex); | |
| 207 | return m ? exports.fromComment(m.pop()) : null; | |
| 208 | }; | |
| 209 | ||
| 210 | // Finds last sourcemap comment in file or returns null if none was found | |
| 211 | exports.fromMapFileSource = function (content, read) { | |
| 212 | if (typeof read === 'string') { | |
| 213 | throw new Error( | |
| 214 | 'String directory paths are no longer supported with `fromMapFileSource`\n' + | |
| 215 | 'Please review the Upgrading documentation at https://github.com/thlorenz/convert-source-map#upgrading' | |
| 216 | ) | |
| 217 | } | |
| 218 | var m = content.match(exports.mapFileCommentRegex); | |
| 219 | return m ? exports.fromMapFileComment(m.pop(), read) : null; | |
| 220 | }; | |
| 221 | ||
| 222 | exports.removeComments = function (src) { | |
| 223 | return src.replace(exports.commentRegex, ''); | |
| 224 | }; | |
| 225 | ||
| 226 | exports.removeMapFileComments = function (src) { | |
| 227 | return src.replace(exports.mapFileCommentRegex, ''); | |
| 228 | }; | |
| 229 | ||
| 230 | exports.generateMapFileComment = function (file, options) { | |
| 231 | var data = 'sourceMappingURL=' + file; | |
| 232 | return options && options.multiline ? '/*# ' + data + ' */' : '//# ' + data; | |
| 233 | }; |
| … | … | @@ -0,0 +1,38 @@ |
| 1 | { | |
| 2 | "name": "convert-source-map", | |
| 3 | "version": "2.0.0", | |
| 4 | "description": "Converts a source-map from/to different formats and allows adding/changing properties.", | |
| 5 | "main": "index.js", | |
| 6 | "scripts": { | |
| 7 | "test": "tap test/*.js --color" | |
| 8 | }, | |
| 9 | "repository": { | |
| 10 | "type": "git", | |
| 11 | "url": "git://github.com/thlorenz/convert-source-map.git" | |
| 12 | }, | |
| 13 | "homepage": "https://github.com/thlorenz/convert-source-map", | |
| 14 | "devDependencies": { | |
| 15 | "inline-source-map": "~0.6.2", | |
| 16 | "tap": "~9.0.0" | |
| 17 | }, | |
| 18 | "keywords": [ | |
| 19 | "convert", | |
| 20 | "sourcemap", | |
| 21 | "source", | |
| 22 | "map", | |
| 23 | "browser", | |
| 24 | "debug" | |
| 25 | ], | |
| 26 | "author": { | |
| 27 | "name": "Thorsten Lorenz", | |
| 28 | "email": "thlorenz@gmx.de", | |
| 29 | "url": "http://thlorenz.com" | |
| 30 | }, | |
| 31 | "license": "MIT", | |
| 32 | "engine": { | |
| 33 | "node": ">=4" | |
| 34 | }, | |
| 35 | "files": [ | |
| 36 | "index.js" | |
| 37 | ] | |
| 38 | } |
| … | … | @@ -0,0 +1,21 @@ |
| 1 | The MIT License (MIT) | |
| 2 | ||
| 3 | Copyright (c) 2021 Mathias Buus | |
| 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 | |
| 13 | all 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 | |
| 21 | THE SOFTWARE. |
| … | … | @@ -0,0 +1,384 @@ |
| 1 | # dht-rpc | |
| 2 | ||
| 3 | Make RPC calls over a [Kademlia](https://pdos.csail.mit.edu/~petar/papers/maymounkov-kademlia-lncs.pdf) based DHT. | |
| 4 | ||
| 5 | ``` | |
| 6 | npm install dht-rpc | |
| 7 | ``` | |
| 8 | ||
| 9 | ## Key Features | |
| 10 | ||
| 11 | - Remote IP / firewall detection | |
| 12 | - Easily add any command to your DHT | |
| 13 | - Streaming queries and updates | |
| 14 | ||
| 15 | Note that internally V5 of dht-rpc differs significantly from V4, due to a series | |
| 16 | of improvements to NAT detection, secure routing IDs and more. | |
| 17 | ||
| 18 | ## Usage | |
| 19 | ||
| 20 | Here is an example implementing a simple key value store | |
| 21 | ||
| 22 | First spin up a bootstrap node. You can make multiple if you want for redundancy. | |
| 23 | There is nothing special about a bootstrap node, except it needs to know it's own host and port, | |
| 24 | since it knows no other nodes to infer it from. | |
| 25 | ||
| 26 | ```js | |
| 27 | import DHT from 'dht-rpc' | |
| 28 | ||
| 29 | const bootstrap = DHT.bootstrapper(10001, '127.0.0.1') | |
| 30 | ``` | |
| 31 | ||
| 32 | Now lets make some dht nodes that can store values in our key value store. | |
| 33 | ||
| 34 | ```js | |
| 35 | import DHT from 'dht-rpc' | |
| 36 | import crypto from 'crypto' | |
| 37 | ||
| 38 | // Let's create 100 dht nodes for our example. | |
| 39 | for (var i = 0; i < 100; i++) createNode() | |
| 40 | ||
| 41 | function createNode() { | |
| 42 | const node = new DHT({ | |
| 43 | bootstrap: ['localhost:10001'] | |
| 44 | }) | |
| 45 | ||
| 46 | const values = new Map() | |
| 47 | const VALUES = 0 // define a command enum | |
| 48 | ||
| 49 | node.on('request', function (req) { | |
| 50 | if (req.command === VALUES) { | |
| 51 | if (req.token) { | |
| 52 | // if we are the closest node store the value (ie the node sent a valid roundtrip token) | |
| 53 | const key = hash(req.value).toString('hex') | |
| 54 | values.set(key, req.value) | |
| 55 | console.log('Storing', key, '-->', req.value.toString()) | |
| 56 | return req.reply(null) | |
| 57 | } | |
| 58 | ||
| 59 | const value = values.get(req.target.toString('hex')) | |
| 60 | req.reply(value) | |
| 61 | } | |
| 62 | }) | |
| 63 | } | |
| 64 | ||
| 65 | function hash(value) { | |
| 66 | return crypto.createHash('sha256').update(value).digest() | |
| 67 | } | |
| 68 | ``` | |
| 69 | ||
| 70 | To insert a value into this dht make another script that does this following | |
| 71 | ||
| 72 | ```js | |
| 73 | const node = new DHT() | |
| 74 | ||
| 75 | const q = node.query( | |
| 76 | { | |
| 77 | target: hash(val), | |
| 78 | command: VALUES, | |
| 79 | value | |
| 80 | }, | |
| 81 | { | |
| 82 | // commit true will make the query re-request the 20 closest | |
| 83 | // nodes with a valid round trip token to update the values | |
| 84 | commit: true | |
| 85 | } | |
| 86 | ) | |
| 87 | ||
| 88 | await q.finished() | |
| 89 | ``` | |
| 90 | ||
| 91 | Then after inserting run this script to query for a value | |
| 92 | ||
| 93 | ```js | |
| 94 | const target = Buffer.from(hexFromAbove, 'hex') | |
| 95 | for await (const data of node.query({ target, command: VALUES })) { | |
| 96 | if (data.value && hash(data.value).toString('hex') === hexFromAbove) { | |
| 97 | // We found the value! Destroy the query stream as there is no need to continue. | |
| 98 | console.log(val, '-->', data.value.toString()) | |
| 99 | break | |
| 100 | } | |
| 101 | } | |
| 102 | console.log('(query finished)') | |
| 103 | ``` | |
| 104 | ||
| 105 | ## API | |
| 106 | ||
| 107 | #### `const node = new DHT([options])` | |
| 108 | ||
| 109 | Create a new DHT node. | |
| 110 | ||
| 111 | Options include: | |
| 112 | ||
| 113 | ```js | |
| 114 | { | |
| 115 | // A list of bootstrap nodes. Optionally prefix a suggested-IP, i.e. '192.168.1.10@bootstrap-node.com:24242' | |
| 116 | bootstrap: [ 'bootstrap-node.com:24242', ... ], | |
| 117 | // Optionally pass in array of { host, port } to add to the routing table if you know any peers | |
| 118 | nodes: [{ host, port }, ...], | |
| 119 | // Optionally pass a port you prefer to bind to instead of a random one, or a range of ports it will try to bind to in order: [startPort, endPort], endPort exclusive | |
| 120 | port: 0, | |
| 121 | // Optionally pass a host you prefer to bind to instead of all networks | |
| 122 | host: '0.0.0.0', | |
| 123 | // Optionally pass a UDX instance on which sockets will be created. | |
| 124 | udx, | |
| 125 | // dht-rpc will automatically detect if you are firewalled. If you know that you are not set this to false | |
| 126 | firewalled: true, | |
| 127 | // dht-rpc will hint when a node is down if a request times out. Setting to false disables sending hints | |
| 128 | sendDownHints: true, | |
| 129 | // How many down hints can be sent per tick. -1 is unlimited | |
| 130 | downHintsRateLimit: 50 | |
| 131 | } | |
| 132 | ``` | |
| 133 | ||
| 134 | Nodes per default use something called adaptive mode to decide whether or not they want to join other nodes' routing table. | |
| 135 | This includes things like node uptime, if the node is firewalled etc. Adaptive mode is conservative, so it might take ~20-30 mins for the node to turn persistent. If you are making a test case with your own bootstrap network you'd usually want to turn this off to make sure your test finishes in a timely manner. You can do this by passing `ephemeral: false` in the constructor. | |
| 136 | For the vast majority of use-cases you should always use adaptive mode to ensure good DHT health, ie the defaults. | |
| 137 | ||
| 138 | Your DHT routing id is `hash(publicIp + publicPort)` and will be autoconfigured internally. | |
| 139 | ||
| 140 | #### `const node = DHT.bootstrapper(port, host, [options])` | |
| 141 | ||
| 142 | Make a bootstrap node for your DHT. The port and host needs to be its globally accessible port and host. | |
| 143 | Note: `port` and `host` parameters are used to create the node id. Use `options.host` if you want to bind to i.e. 127.0.0.1. | |
| 144 | DHT nodes can use any other DHT node to bootstrap, but a bootstrap node can bootstrap itself, by itself. | |
| 145 | ||
| 146 | #### `await node.fullyBootstrapped()` | |
| 147 | ||
| 148 | Wait for the node to be fully bootstrapped etc. | |
| 149 | You don't have to wait for this method, but can be useful during testing. | |
| 150 | ||
| 151 | #### `node.id` | |
| 152 | ||
| 153 | Get your own routing ID. Only available when the node is not ephemeral. | |
| 154 | ||
| 155 | #### `node.ephemeral` | |
| 156 | ||
| 157 | A boolean indicating if you are currently ephemeral or not | |
| 158 | ||
| 159 | #### `node.on('bootstrap')` | |
| 160 | ||
| 161 | Emitted when the routing table is fully bootstrapped. Emitted as a conveinience. | |
| 162 | ||
| 163 | #### `node.on('listening')` | |
| 164 | ||
| 165 | Emitted when the underlying UDX socket is listening. Emitted as a conveinience. | |
| 166 | ||
| 167 | #### `node.on('ready')` | |
| 168 | ||
| 169 | Emitted when the node is fully bootstrapped etc. | |
| 170 | ||
| 171 | #### `node.on('persistent')` | |
| 172 | ||
| 173 | Emitted when the node is no longer in ephemeral mode. | |
| 174 | All nodes start in ephemeral mode, as they figure out their NAT settings. | |
| 175 | If you set `ephemeral: false` then this is emitted during the bootstrap phase, assuming | |
| 176 | you are on an open NAT. | |
| 177 | ||
| 178 | #### `node.on('wake-up')` | |
| 179 | ||
| 180 | Emitted when the node has detected that the computer has gone to sleep. If this happens, | |
| 181 | it will switch from persistent mode to ephemeral again. | |
| 182 | ||
| 183 | #### `node.on('network-change', interfaces)` | |
| 184 | ||
| 185 | Emitted when the network interfaces of the computer change. | |
| 186 | ||
| 187 | #### `node.on('nat-update', (host, port) => {})` | |
| 188 | ||
| 189 | Emitted when `node.host` or `node.port` were changed. | |
| 190 | ||
| 191 | #### `node.on('close')` | |
| 192 | ||
| 193 | Will be emitted after `node.destroy()` is completed. | |
| 194 | ||
| 195 | #### `node.refresh()` | |
| 196 | ||
| 197 | Refresh the routing table by looking up a random node in the background. | |
| 198 | This is called internally periodically, but exposed in-case you want to force a refresh. | |
| 199 | ||
| 200 | #### `node.host` | |
| 201 | ||
| 202 | Get your node's public ip, inferred from other nodes in the DHT. | |
| 203 | If the ip cannot be determined, this is set to `null`. | |
| 204 | ||
| 205 | #### `node.port` | |
| 206 | ||
| 207 | Get your node's public port, inferred from other nodes in the DHT. | |
| 208 | If your node does not have a consistent port, this is set to 0. | |
| 209 | ||
| 210 | #### `node.firewalled` | |
| 211 | ||
| 212 | Boolean indicated if your node is behind a firewall. | |
| 213 | ||
| 214 | This is auto detected by having other nodes trying to do a PING to you | |
| 215 | without you contacting them first. | |
| 216 | ||
| 217 | #### `node.randomized` | |
| 218 | ||
| 219 | Boolean indicating if your node is likely behind a randomizing NAT. | |
| 220 | ||
| 221 | #### `const addr = node.address()` | |
| 222 | ||
| 223 | Get the local address of the UDP socket bound. | |
| 224 | ||
| 225 | Note that if you are in ephemeral mode, this will return a different | |
| 226 | port than the one you provided in the constructor (under `port`), as ephemeral | |
| 227 | mode always uses a random port. | |
| 228 | ||
| 229 | #### `node.on('request', req)` | |
| 230 | ||
| 231 | Emitted when an incoming DHT request is received. This is where you can add your own RPC methods. | |
| 232 | ||
| 233 | - `req.target` - the dht target the peer is looking (routing is handled behind the scene) | |
| 234 | - `req.command` - the RPC command enum | |
| 235 | - `req.value` - the RPC value buffer | |
| 236 | - `req.token` - If the remote peer echoed back a valid roundtrip token, proving their "from address" this is set | |
| 237 | - `req.from` - who sent this request (host, port) | |
| 238 | ||
| 239 | To reply to a request use the `req.reply(value)` method and to reply with an error code use `req.error(errorCode)`. | |
| 240 | ||
| 241 | In general error codes are up to the user to define, with the general suggestion to start application specific errors | |
| 242 | from error code `16` and up, to avoid future clashes with `dht-rpc` internals. | |
| 243 | ||
| 244 | Currently dht-rpc defines the following errors | |
| 245 | ||
| 246 | ```js | |
| 247 | DHT.OK = 0 // ie no error | |
| 248 | DHT.ERROR_UNKNOWN_COMMAND = 1 // the command requested does not exist | |
| 249 | DHT.ERROR_INVALID_TOKEN = 2 // the round trip token sent is invalid | |
| 250 | ``` | |
| 251 | ||
| 252 | #### `reply = await node.request({ token, target, command, value }, to, [options])` | |
| 253 | ||
| 254 | Send a request to a specific node specified by the `to` address (`{ host, port }`). | |
| 255 | See the query API for more info on the arguments. | |
| 256 | ||
| 257 | Options include: | |
| 258 | ||
| 259 | ```js | |
| 260 | { | |
| 261 | retry: true, // whether the request should retry on timeout | |
| 262 | socket: udxSocket // request on this specific socket | |
| 263 | } | |
| 264 | ``` | |
| 265 | ||
| 266 | Normally you'd set the token when committing to the dht in the query's commit hook. | |
| 267 | ||
| 268 | #### `reply = await node.ping(to, [options])` | |
| 269 | ||
| 270 | Ping a remote node described by `to`. The return promise resolves if the remote node replies. `to` is an object with the shape `{ host, port }`. | |
| 271 | ||
| 272 | Additional options include: | |
| 273 | ||
| 274 | ```js | |
| 275 | { | |
| 276 | size: 0, // size of the value buffer, filled with zeroes | |
| 277 | ttl: 0 // Set the ttl for the packet sent. `0` is the default ttl. | |
| 278 | } | |
| 279 | ``` | |
| 280 | ||
| 281 | #### `reply = await node.delayedPing(to, delayMs, [options])` | |
| 282 | ||
| 283 | Send a ping that intentionally waits before replying. Useful for testing latency/timeout behavior and scheduling a delayed response. | |
| 284 | ||
| 285 | - `to` - `{ host, port }` of the target peer | |
| 286 | - `delayMs` - integer milliseconds to delay the reply (0–10_000). Values above 10_000 ms are rejected and no reply is sent (the request will time out). | |
| 287 | - `options` - same options as `node.request()`: | |
| 288 | ||
| 289 | Example: | |
| 290 | ||
| 291 | ```js | |
| 292 | const start = Date.now() | |
| 293 | await node.delayedPing({ host: peer.host, port: peer.port }, 1000) | |
| 294 | console.log('elapsed >=', Date.now() - start, 'ms') // ~1s + network overhead | |
| 295 | ``` | |
| 296 | ||
| 297 | #### `stream = node.query({ target, command, value }, [options])` | |
| 298 | ||
| 299 | Query the DHT. Will move as close as possible to the `target` provided, which should be a 32-byte uniformly distributed buffer (ie a hash). | |
| 300 | ||
| 301 | - `target` - find nodes close to this (should be a 32 byte buffer like a hash) | |
| 302 | - `command` - an enum (uint) indicating the method you want to invoke | |
| 303 | - `value` - optional binary payload to send with it | |
| 304 | ||
| 305 | If you want to modify state stored in the dht, you can use the commit flag to signal the closest | |
| 306 | nodes. | |
| 307 | ||
| 308 | ```js | |
| 309 | { | |
| 310 | // "commit" the query to the 20 closest nodes so they can modify/update their state | |
| 311 | commit: true | |
| 312 | } | |
| 313 | ``` | |
| 314 | ||
| 315 | Commiting a query will just re-request your command to the closest nodes once those are verified. | |
| 316 | If you want to do some more specific logic with the closest nodes you can specify a function instead, | |
| 317 | that is called for each close reply. | |
| 318 | ||
| 319 | ```js | |
| 320 | { | |
| 321 | async commit (reply, dht, query) { | |
| 322 | // normally you'd send back the roundtrip token here, to prove to the remote that you own | |
| 323 | // your ip/port | |
| 324 | await dht.request({ token: reply.token, target, command, value }, reply.from) | |
| 325 | } | |
| 326 | } | |
| 327 | ``` | |
| 328 | ||
| 329 | Other options include: | |
| 330 | ||
| 331 | ```js | |
| 332 | { | |
| 333 | nodes: [ | |
| 334 | // start the query by querying these nodes | |
| 335 | // useful if you are re-doing a query from a set of closest nodes. | |
| 336 | ], | |
| 337 | replies: [ | |
| 338 | // similar to nodes, but useful if you have an array of closest replies instead | |
| 339 | // from a previous query. | |
| 340 | ], | |
| 341 | map (reply) { | |
| 342 | // map the reply into what you want returned on the stream | |
| 343 | return { onlyValue: reply.value } | |
| 344 | } | |
| 345 | } | |
| 346 | ``` | |
| 347 | ||
| 348 | The query method returns a stream encapsulating the query, that is also an async iterator. Each `data` event contain a DHT reply. | |
| 349 | If you just want to wait for the query to finish, you can use the `await stream.finished()` helper. After completion the closest | |
| 350 | nodes are stored in `stream.closestNodes` array. | |
| 351 | ||
| 352 | If you want to access the closest replies to your provided target you can see those at `stream.closestReplies`. | |
| 353 | ||
| 354 | #### `stream = node.findNode(target, [options])` | |
| 355 | ||
| 356 | Find the node closest to the node with id `target`. Returns a stream encapsulating the query (see `node.query()`). `options` are the same as `node.query()`. | |
| 357 | ||
| 358 | #### `node.destroy()` | |
| 359 | ||
| 360 | Shutdown the DHT node. | |
| 361 | ||
| 362 | #### `node.destroyed` | |
| 363 | ||
| 364 | Boolean indicating if this has been destroyed. | |
| 365 | ||
| 366 | #### `node.toArray([options])` | |
| 367 | ||
| 368 | Get the routing table peers out as an array of `{ host, port }`. Use `options.limit` to get a subset. | |
| 369 | ||
| 370 | #### `node.addNode({ host, port })` | |
| 371 | ||
| 372 | Manually add a node to the routing table. | |
| 373 | ||
| 374 | #### `await node.suspend()` | |
| 375 | ||
| 376 | Tell the DHT you are going to background (ie suspend and allow it to make preperations for that) | |
| 377 | ||
| 378 | #### `await node.resume()` | |
| 379 | ||
| 380 | Tell the DHT you are resuming from suspension. | |
| 381 | ||
| 382 | ## License | |
| 383 | ||
| 384 | MIT |
| … | … | @@ -0,0 +1,1081 @@ |
| 1 | const { EventEmitter } = require('events') | |
| 2 | const Table = require('kademlia-routing-table') | |
| 3 | const TOS = require('time-ordered-set') | |
| 4 | const UDX = require('udx-native') | |
| 5 | const sodium = require('sodium-universal') | |
| 6 | const c = require('compact-encoding') | |
| 7 | const NatSampler = require('nat-sampler') | |
| 8 | const b4a = require('b4a') | |
| 9 | const NetworkHealth = require('./lib/health') | |
| 10 | const IO = require('./lib/io') | |
| 11 | const Query = require('./lib/query') | |
| 12 | const Session = require('./lib/session') | |
| 13 | const peer = require('./lib/peer') | |
| 14 | const { UNKNOWN_COMMAND, INVALID_TOKEN } = require('./lib/errors') | |
| 15 | const { PING, PING_NAT, FIND_NODE, DOWN_HINT, DELAYED_PING } = require('./lib/commands') | |
| 16 | ||
| 17 | const TMP = b4a.allocUnsafe(32) | |
| 18 | const TICK_INTERVAL = 5000 | |
| 19 | const SLEEPING_INTERVAL = 3 * TICK_INTERVAL | |
| 20 | const STABLE_TICKS = 240 // if nothing major bad happens in ~20mins we can consider this node stable (if nat is friendly) | |
| 21 | const MORE_STABLE_TICKS = 3 * STABLE_TICKS | |
| 22 | const REFRESH_TICKS = 60 // refresh every ~5min when idle | |
| 23 | const RECENT_NODE = 12 // we've heard from a node less than 1min ago | |
| 24 | const OLD_NODE = 360 // if an node has been around more than 30 min we consider it old | |
| 25 | ||
| 26 | const DEFAULTS = { | |
| 27 | concurrency: 10, | |
| 28 | maxWindow: IO.DEFAULT_MAX_WINDOW, | |
| 29 | maxPingDelay: 10_000 | |
| 30 | } | |
| 31 | ||
| 32 | class DHT extends EventEmitter { | |
| 33 | constructor(opts = {}) { | |
| 34 | super() | |
| 35 | ||
| 36 | this.bootstrapNodes = opts.bootstrap === false ? [] : (opts.bootstrap || []).map(parseNode) | |
| 37 | this.table = new Table(randomBytes(32)) | |
| 38 | this.nodes = new TOS() | |
| 39 | this.udx = opts.udx || new UDX() | |
| 40 | this.io = new IO(this.table, this.udx, { | |
| 41 | ...opts, | |
| 42 | onrequest: this._onrequest.bind(this), | |
| 43 | onresponse: this._onresponse.bind(this), | |
| 44 | ontimeout: this._ontimeout.bind(this) | |
| 45 | }) | |
| 46 | this.health = new NetworkHealth(this) | |
| 47 | ||
| 48 | this.concurrency = opts.concurrency || DEFAULTS.concurrency | |
| 49 | this.maxPingDelay = opts.maxPingDelay || DEFAULTS.maxPingDelay | |
| 50 | this.bootstrapped = false | |
| 51 | this.ephemeral = true | |
| 52 | this.firewalled = this.io.firewalled | |
| 53 | this.adaptive = typeof opts.ephemeral !== 'boolean' && opts.adaptive !== false | |
| 54 | this.destroyed = false | |
| 55 | this.suspended = false | |
| 56 | this.online = true | |
| 57 | this.degraded = false | |
| 58 | this.stats = { | |
| 59 | queries: { active: 0, total: 0 }, | |
| 60 | requests: this.io.stats.requests, | |
| 61 | commands: { | |
| 62 | ping: this.io.stats.commands[PING], | |
| 63 | pingNat: this.io.stats.commands[PING_NAT], | |
| 64 | findNode: this.io.stats.commands[FIND_NODE], | |
| 65 | downHint: this.io.stats.commands[DOWN_HINT] | |
| 66 | } | |
| 67 | } | |
| 68 | ||
| 69 | this._nat = new NatSampler() | |
| 70 | this._quickFirewall = opts.quickFirewall !== false | |
| 71 | this._forcePersistent = opts.ephemeral === false | |
| 72 | this._repinging = 0 | |
| 73 | this._checks = 0 | |
| 74 | this._tick = randomOffset(100) // make sure to random offset all the network ticks | |
| 75 | this._refreshTicks = randomOffset(REFRESH_TICKS) | |
| 76 | this._stableTicks = this.adaptive ? STABLE_TICKS : 0 | |
| 77 | this._tickInterval = setInterval(this._ontick.bind(this), TICK_INTERVAL) | |
| 78 | this._lastTick = Date.now() | |
| 79 | this._lastHost = null | |
| 80 | this._filterNode = opts.filterNode || opts.addNode || null // opts.addNode is deprecating, use opts.filterNode instead | |
| 81 | this._onrow = (row) => row.on('full', (node) => this._onfullrow(node, row)) | |
| 82 | this._nonePersistentSamples = [] | |
| 83 | this._bootstrapping = this._bootstrap() | |
| 84 | this._bootstrapping.catch(noop) | |
| 85 | this._sendDownHints = opts.sendDownHints !== false | |
| 86 | this._downHintsRateLimit = | |
| 87 | opts.downHintsRateLimit !== undefined ? opts.downHintsRateLimit : 10 * 5 | |
| 88 | this._downHintsSentPerTick = 0 | |
| 89 | this._pendingTimers = new Set() | |
| 90 | ||
| 91 | this.table.on('row', this._onrow) | |
| 92 | ||
| 93 | this.io.networkInterfaces.on('change', (interfaces) => this._onnetworkchange(interfaces)) | |
| 94 | ||
| 95 | if (opts.nodes) { | |
| 96 | for (let i = opts.nodes.length - 1; i >= 0; i--) { | |
| 97 | this.addNode(opts.nodes[i]) | |
| 98 | } | |
| 99 | } | |
| 100 | } | |
| 101 | ||
| 102 | static DEFAULTS = DEFAULTS | |
| 103 | ||
| 104 | static bootstrapper(port, host, opts) { | |
| 105 | if (!port) throw new Error('Port is required') | |
| 106 | if (!host) throw new Error('Host is required') | |
| 107 | if (host === '0.0.0.0' || host === '::') throw new Error('Invalid host') | |
| 108 | if (!UDX.isIPv4(host)) throw new Error('Host must be a IPv4 address') | |
| 109 | ||
| 110 | const dht = new this({ | |
| 111 | port, | |
| 112 | ephemeral: false, | |
| 113 | firewalled: false, | |
| 114 | anyPort: false, | |
| 115 | bootstrap: [], | |
| 116 | ...opts | |
| 117 | }) | |
| 118 | dht._nat.add(host, port) | |
| 119 | return dht | |
| 120 | } | |
| 121 | ||
| 122 | get id() { | |
| 123 | return this.ephemeral ? null : this.table.id | |
| 124 | } | |
| 125 | ||
| 126 | get host() { | |
| 127 | return this._nat.host | |
| 128 | } | |
| 129 | ||
| 130 | get port() { | |
| 131 | return this._nat.port | |
| 132 | } | |
| 133 | ||
| 134 | get randomized() { | |
| 135 | return this._nat.host !== null && this._nat.port === 0 | |
| 136 | } | |
| 137 | ||
| 138 | get socket() { | |
| 139 | return this.firewalled ? this.io.clientSocket : this.io.serverSocket | |
| 140 | } | |
| 141 | ||
| 142 | get config() { | |
| 143 | return { | |
| 144 | concurrency: this.concurrency, | |
| 145 | maxWindow: this.io.congestion._maxWindow, | |
| 146 | randomPunchInterval: this._randomPunchInterval, | |
| 147 | connectionKeepAlive: this.connectionKeepAlive, | |
| 148 | sendDownHints: this._sendDownHints, | |
| 149 | downHintsRateLimit: this._downHintsRateLimit | |
| 150 | } | |
| 151 | } | |
| 152 | ||
| 153 | onmessage(socket, buf, rinfo) { | |
| 154 | if (buf.byteLength > 1) this.io.onmessage(socket, buf, rinfo) | |
| 155 | } | |
| 156 | ||
| 157 | bind() { | |
| 158 | return this.io.bind() | |
| 159 | } | |
| 160 | ||
| 161 | async suspend({ log = noop } = {}) { | |
| 162 | log('Suspending waiting for io bind...') | |
| 163 | await this.io.bind() | |
| 164 | log('Done, continuing') | |
| 165 | if (this.suspended || this.destroyed) return | |
| 166 | this.suspended = true | |
| 167 | clearInterval(this._tickInterval) | |
| 168 | log('Done, suspending io') | |
| 169 | await this.io.suspend({ log }) | |
| 170 | log('Done, dht suspended') | |
| 171 | this.emit('suspend') | |
| 172 | } | |
| 173 | ||
| 174 | async resume({ log = noop } = {}) { | |
| 175 | if (!this.suspended || this.destroyed) return | |
| 176 | this.suspended = false | |
| 177 | this._tickInterval = setInterval(this._ontick.bind(this), TICK_INTERVAL) | |
| 178 | this._onwakeup() | |
| 179 | log('Resuming io') | |
| 180 | await this.io.resume() | |
| 181 | log('Done, dht resumed') | |
| 182 | this.io.networkInterfaces.on('change', (interfaces) => this._onnetworkchange(interfaces)) | |
| 183 | this.refresh() | |
| 184 | this.emit('resume') | |
| 185 | } | |
| 186 | ||
| 187 | address() { | |
| 188 | const socket = this.socket | |
| 189 | return socket ? socket.address() : null | |
| 190 | } | |
| 191 | ||
| 192 | localAddress() { | |
| 193 | if (!this.io.serverSocket) return null | |
| 194 | ||
| 195 | return { | |
| 196 | host: localIP(this.udx), | |
| 197 | port: this.io.serverSocket.address().port | |
| 198 | } | |
| 199 | } | |
| 200 | ||
| 201 | remoteAddress() { | |
| 202 | if (!this.host) return null | |
| 203 | if (!this.port) return null | |
| 204 | if (this.firewalled) return null | |
| 205 | if (!this.io.serverSocket) return null | |
| 206 | ||
| 207 | const port = this.io.serverSocket.address().port | |
| 208 | if (port !== this.port) return null | |
| 209 | ||
| 210 | return { | |
| 211 | host: this.host, | |
| 212 | port | |
| 213 | } | |
| 214 | } | |
| 215 | ||
| 216 | addNode({ host, port }) { | |
| 217 | this._addNode({ | |
| 218 | id: peer.id(host, port), | |
| 219 | port, | |
| 220 | host, | |
| 221 | token: null, | |
| 222 | to: null, | |
| 223 | sampled: 0, | |
| 224 | added: this._tick, | |
| 225 | pinged: 0, | |
| 226 | seen: 0, | |
| 227 | downHints: 0, | |
| 228 | prev: null, | |
| 229 | next: null | |
| 230 | }) | |
| 231 | } | |
| 232 | ||
| 233 | toArray(opts) { | |
| 234 | const limit = opts && opts.limit | |
| 235 | if (limit === 0) return [] | |
| 236 | return this.nodes.toArray({ limit, reverse: true }).map(({ host, port }) => ({ host, port })) | |
| 237 | } | |
| 238 | ||
| 239 | async fullyBootstrapped() { | |
| 240 | return this._bootstrapping | |
| 241 | } | |
| 242 | ||
| 243 | ready() { | |
| 244 | // Deprecating, use fullyBootstrapped instead (removed on next major) | |
| 245 | return this.fullyBootstrapped() | |
| 246 | } | |
| 247 | ||
| 248 | findNode(target, opts) { | |
| 249 | if (this.destroyed) throw new Error('Node destroyed') | |
| 250 | this._refreshTicks = REFRESH_TICKS | |
| 251 | return new Query(this, target, true, FIND_NODE, null, opts) | |
| 252 | } | |
| 253 | ||
| 254 | query({ target, command, value }, opts) { | |
| 255 | if (this.destroyed) throw new Error('Node destroyed') | |
| 256 | this._refreshTicks = REFRESH_TICKS | |
| 257 | return new Query(this, target, false, command, value || null, opts) | |
| 258 | } | |
| 259 | ||
| 260 | ping({ host, port }, opts) { | |
| 261 | let value = null | |
| 262 | ||
| 263 | if (opts && opts.size && opts.size > 0) value = b4a.alloc(opts.size) | |
| 264 | ||
| 265 | const req = this.io.createRequest( | |
| 266 | { id: null, host, port }, | |
| 267 | null, | |
| 268 | true, | |
| 269 | PING, | |
| 270 | null, | |
| 271 | value, | |
| 272 | (opts && opts.session) || null, | |
| 273 | opts && opts.ttl | |
| 274 | ) | |
| 275 | return this._requestToPromise(req, opts) | |
| 276 | } | |
| 277 | ||
| 278 | delayedPing({ host, port }, delayMs, opts) { | |
| 279 | if (delayMs > this.maxPingDelay) { | |
| 280 | throw new Error(`Delay exceeds max delay: ${this.maxPingDelay}ms`) | |
| 281 | } | |
| 282 | ||
| 283 | const value = b4a.allocUnsafe(4) | |
| 284 | c.uint32.encode({ start: 0, end: 4, buffer: value }, delayMs) | |
| 285 | ||
| 286 | const req = this.io.createRequest( | |
| 287 | { id: null, host, port }, | |
| 288 | null, | |
| 289 | true, | |
| 290 | DELAYED_PING, | |
| 291 | null, | |
| 292 | value, | |
| 293 | (opts && opts.session) || null, | |
| 294 | opts && opts.ttl | |
| 295 | ) | |
| 296 | // add 1 second to the timeout to account for network overhead | |
| 297 | req.timeout = delayMs + 1_000 | |
| 298 | return this._requestToPromise(req, opts) | |
| 299 | } | |
| 300 | ||
| 301 | async rttStats() { | |
| 302 | const stats = { | |
| 303 | successes: 0, | |
| 304 | errors: 0, | |
| 305 | responses: { | |
| 306 | avgRtt: 0, | |
| 307 | errors: 0, | |
| 308 | avgCloserNodes: 0 | |
| 309 | }, | |
| 310 | closestReplies: { | |
| 311 | avgRtt: 0, | |
| 312 | errors: 0, | |
| 313 | avgCloserNodes: 0 | |
| 314 | } | |
| 315 | } | |
| 316 | ||
| 317 | if (this.nodes.latest) { | |
| 318 | const q = this.findNode(this.nodes.latest.id) | |
| 319 | ||
| 320 | let responseCount = 0 | |
| 321 | let closestCount = 0 | |
| 322 | ||
| 323 | for await (const msg of q) { | |
| 324 | stats.responses.avgRtt += msg.rtt | |
| 325 | stats.responses.errors += msg.error | |
| 326 | stats.responses.avgCloserNodes += msg.closerNodes?.length || 0 | |
| 327 | responseCount++ | |
| 328 | } | |
| 329 | stats.responses.avgRtt /= responseCount | |
| 330 | stats.responses.avgCloserNodes /= responseCount | |
| 331 | ||
| 332 | for await (const msg of q.closestReplies) { | |
| 333 | stats.closestReplies.avgRtt += msg.rtt | |
| 334 | stats.closestReplies.errors += msg.error | |
| 335 | stats.closestReplies.avgCloserNodes += msg.closerNodes?.length || 0 | |
| 336 | closestCount++ | |
| 337 | } | |
| 338 | stats.closestReplies.avgRtt /= closestCount | |
| 339 | stats.closestReplies.avgCloserNodes /= closestCount | |
| 340 | ||
| 341 | stats.successes = q.successes | |
| 342 | stats.errors = q.errors | |
| 343 | } | |
| 344 | ||
| 345 | return stats | |
| 346 | } | |
| 347 | ||
| 348 | request({ token = null, command, target = null, value = null }, { host, port }, opts) { | |
| 349 | const req = this.io.createRequest( | |
| 350 | { id: null, host, port }, | |
| 351 | token, | |
| 352 | false, | |
| 353 | command, | |
| 354 | target, | |
| 355 | value, | |
| 356 | (opts && opts.session) || null, | |
| 357 | opts && opts.ttl | |
| 358 | ) | |
| 359 | return this._requestToPromise(req, opts) | |
| 360 | } | |
| 361 | ||
| 362 | session() { | |
| 363 | return new Session(this) | |
| 364 | } | |
| 365 | ||
| 366 | _requestToPromise(req, opts) { | |
| 367 | if (req === null) return Promise.reject(new Error('Node destroyed')) | |
| 368 | ||
| 369 | if (opts && opts.socket) req.socket = opts.socket | |
| 370 | if (opts && opts.retry === false) req.retries = 0 | |
| 371 | ||
| 372 | return new Promise((resolve, reject) => { | |
| 373 | req.onresponse = resolve | |
| 374 | req.onerror = reject | |
| 375 | req.send() | |
| 376 | }) | |
| 377 | } | |
| 378 | ||
| 379 | async _bootstrap() { | |
| 380 | const self = this | |
| 381 | ||
| 382 | await Promise.resolve() // wait a tick, so apis can be used from the outside | |
| 383 | await this.io.bind() | |
| 384 | ||
| 385 | this.emit('listening') | |
| 386 | ||
| 387 | // TODO: some papers describe more advanced ways of bootstrapping - we should prob look into that | |
| 388 | ||
| 389 | let first = this.firewalled && this._quickFirewall && !this._forcePersistent | |
| 390 | let testNat = false | |
| 391 | ||
| 392 | const onlyFirewall = !this._forcePersistent | |
| 393 | ||
| 394 | for (let i = 0; i < 2; i++) { | |
| 395 | await this._backgroundQuery(this.table.id).on('data', ondata).finished() | |
| 396 | ||
| 397 | if (this.bootstrapped || (!testNat && !this._forcePersistent)) break | |
| 398 | if (!(await this._updateNetworkState(onlyFirewall))) break | |
| 399 | } | |
| 400 | ||
| 401 | if (this.bootstrapped) return | |
| 402 | this.bootstrapped = true | |
| 403 | ||
| 404 | this.emit('ready') | |
| 405 | ||
| 406 | function ondata(data) { | |
| 407 | // Simple QUICK nat heuristic. | |
| 408 | // If we get ONE positive nat ping before the bootstrap query finishes | |
| 409 | // then we always to a nat test, no matter if we are adaptive... | |
| 410 | // This should be expanded in the future to try more than one node etc, not always hit the first etc | |
| 411 | // If this fails, then nbd, as the onstable hook will pick it up later. | |
| 412 | ||
| 413 | if (!first) return | |
| 414 | first = false | |
| 415 | ||
| 416 | const value = b4a.allocUnsafe(2) | |
| 417 | c.uint16.encode({ start: 0, end: 2, buffer: value }, self.io.serverSocket.address().port) | |
| 418 | ||
| 419 | self._request( | |
| 420 | data.from, | |
| 421 | false, | |
| 422 | true, | |
| 423 | PING_NAT, | |
| 424 | null, | |
| 425 | value, | |
| 426 | null, | |
| 427 | () => { | |
| 428 | testNat = true | |
| 429 | }, | |
| 430 | noop | |
| 431 | ) | |
| 432 | } | |
| 433 | } | |
| 434 | ||
| 435 | refresh() { | |
| 436 | const node = this.table.random() | |
| 437 | this._backgroundQuery(node ? node.id : this.table.id).on('error', noop) | |
| 438 | } | |
| 439 | ||
| 440 | async destroy() { | |
| 441 | const emitClose = !this.destroyed | |
| 442 | this.destroyed = true | |
| 443 | clearInterval(this._tickInterval) | |
| 444 | for (const timer of this._pendingTimers) { | |
| 445 | clearTimeout(timer) | |
| 446 | } | |
| 447 | await this.io.destroy() | |
| 448 | if (emitClose) this.emit('close') | |
| 449 | } | |
| 450 | ||
| 451 | _request(to, force, internal, command, target, value, session, onresponse, onerror) { | |
| 452 | if (internal && !this._sendDownHints && command === DOWN_HINT) return null | |
| 453 | const req = this.io.createRequest(to, null, internal, command, target, value, session) | |
| 454 | if (req === null) return null | |
| 455 | ||
| 456 | req.onresponse = onresponse | |
| 457 | req.onerror = onerror | |
| 458 | req.send(force) | |
| 459 | ||
| 460 | return req | |
| 461 | } | |
| 462 | ||
| 463 | _natAdd(host, port) { | |
| 464 | const prevHost = this._nat.host | |
| 465 | const prevPort = this._nat.port | |
| 466 | ||
| 467 | this._nat.add(host, port) | |
| 468 | ||
| 469 | if (prevHost === this._nat.host && prevPort === this._nat.port) return | |
| 470 | ||
| 471 | this.emit('nat-update', this._nat.host, this._nat.port) | |
| 472 | } | |
| 473 | ||
| 474 | // we don't check that this is a bootstrap node but we limit the sample size to very few nodes, so fine | |
| 475 | _sampleBootstrapMaybe(from, to) { | |
| 476 | if (this._nonePersistentSamples.length >= Math.max(1, this.bootstrapNodes.length)) return | |
| 477 | const id = from.host + ':' + from.port | |
| 478 | if (this._nonePersistentSamples.indexOf(id) > -1) return | |
| 479 | this._nonePersistentSamples.push(id) | |
| 480 | this._natAdd(to.host, to.port) | |
| 481 | } | |
| 482 | ||
| 483 | _addNodeFromNetwork(sample, from, to) { | |
| 484 | if (this._filterNode !== null && !this._filterNode(from)) { | |
| 485 | return | |
| 486 | } | |
| 487 | ||
| 488 | if (from.id === null) { | |
| 489 | this._sampleBootstrapMaybe(from, to) | |
| 490 | return | |
| 491 | } | |
| 492 | ||
| 493 | const oldNode = this.table.get(from.id) | |
| 494 | ||
| 495 | // refresh it, if we've seen this before | |
| 496 | if (oldNode) { | |
| 497 | if (sample && (oldNode.sampled === 0 || this._tick - oldNode.sampled >= OLD_NODE)) { | |
| 498 | oldNode.to = to | |
| 499 | oldNode.sampled = this._tick | |
| 500 | this._natAdd(to.host, to.port) | |
| 501 | } | |
| 502 | ||
| 503 | oldNode.pinged = oldNode.seen = this._tick | |
| 504 | this.nodes.add(oldNode) | |
| 505 | return | |
| 506 | } | |
| 507 | ||
| 508 | this._addNode({ | |
| 509 | id: from.id, | |
| 510 | port: from.port, | |
| 511 | host: from.host, | |
| 512 | to, | |
| 513 | sampled: 0, | |
| 514 | added: this._tick, | |
| 515 | pinged: this._tick, // last time we interacted with them | |
| 516 | seen: this._tick, // last time we heard from them | |
| 517 | downHints: 0, | |
| 518 | prev: null, | |
| 519 | next: null | |
| 520 | }) | |
| 521 | } | |
| 522 | ||
| 523 | _addNode(node) { | |
| 524 | if (this.nodes.has(node) || b4a.equals(node.id, this.table.id)) return | |
| 525 | ||
| 526 | node.added = node.pinged = node.seen = this._tick | |
| 527 | ||
| 528 | if (!this.table.add(node)) return | |
| 529 | this.nodes.add(node) | |
| 530 | ||
| 531 | if (node.to && node.sampled === 0) { | |
| 532 | node.sampled = this._tick | |
| 533 | this._natAdd(node.to.host, node.to.port) | |
| 534 | } | |
| 535 | ||
| 536 | this.emit('add-node', node) | |
| 537 | } | |
| 538 | ||
| 539 | _removeStaleNode(node, lastSeen) { | |
| 540 | if (node.seen <= lastSeen) this._removeNode(node) | |
| 541 | } | |
| 542 | ||
| 543 | _removeNode(node) { | |
| 544 | if (!this.nodes.has(node)) return | |
| 545 | ||
| 546 | this.table.remove(node.id) | |
| 547 | this.nodes.remove(node) | |
| 548 | ||
| 549 | this.emit('remove-node', node) | |
| 550 | } | |
| 551 | ||
| 552 | _onwakeup() { | |
| 553 | this._tick += 2 * OLD_NODE // bump the tick enough that everything appears old. | |
| 554 | this._tick += 8 - (this._tick & 7) - 2 // triggers a series of pings in two ticks | |
| 555 | this._stableTicks = MORE_STABLE_TICKS | |
| 556 | this._refreshTicks = 1 // triggers a refresh next tick (allow network time to wake up also) | |
| 557 | this._lastHost = null // clear network cache check | |
| 558 | this.health.reset() | |
| 559 | ||
| 560 | if (this.adaptive) { | |
| 561 | // TODO: re-enable this as soon as we find out why this is over triggering in some edge cases | |
| 562 | // this.firewalled = true | |
| 563 | // this.io.firewalled = true | |
| 564 | ||
| 565 | if (!this.ephemeral) { | |
| 566 | this.ephemeral = true | |
| 567 | this.io.ephemeral = true | |
| 568 | this.emit('ephemeral') | |
| 569 | } | |
| 570 | } | |
| 571 | ||
| 572 | this.emit('wakeup') | |
| 573 | } | |
| 574 | ||
| 575 | _onfullrow(newNode, row) { | |
| 576 | if (!this.bootstrapped || this._repinging >= 3) return | |
| 577 | ||
| 578 | let oldest = null | |
| 579 | for (const node of row.nodes) { | |
| 580 | if (node.pinged === this._tick) continue | |
| 581 | if ( | |
| 582 | oldest === null || | |
| 583 | oldest.pinged > node.pinged || | |
| 584 | (oldest.pinged === node.pinged && oldest.added > node.added) | |
| 585 | ) { | |
| 586 | oldest = node | |
| 587 | } | |
| 588 | } | |
| 589 | ||
| 590 | if (oldest === null) return | |
| 591 | if (this._tick - oldest.pinged < RECENT_NODE && this._tick - oldest.added > OLD_NODE) return | |
| 592 | ||
| 593 | this._repingAndSwap(newNode, oldest) | |
| 594 | } | |
| 595 | ||
| 596 | _onnetworkchange(interfaces) { | |
| 597 | this.emit('network-change', interfaces) | |
| 598 | this.emit('network-update') | |
| 599 | } | |
| 600 | ||
| 601 | _repingAndSwap(newNode, oldNode) { | |
| 602 | const self = this | |
| 603 | const lastSeen = oldNode.seen | |
| 604 | ||
| 605 | oldNode.pinged = this._tick | |
| 606 | ||
| 607 | this._repinging++ | |
| 608 | this._request( | |
| 609 | { id: null, host: oldNode.host, port: oldNode.port }, | |
| 610 | false, | |
| 611 | true, | |
| 612 | PING, | |
| 613 | null, | |
| 614 | null, | |
| 615 | null, | |
| 616 | onsuccess, | |
| 617 | onswap | |
| 618 | ) | |
| 619 | ||
| 620 | function onsuccess(m) { | |
| 621 | if (oldNode.seen <= lastSeen) return onswap() | |
| 622 | self._repinging-- | |
| 623 | } | |
| 624 | ||
| 625 | function onswap(e) { | |
| 626 | self._repinging-- | |
| 627 | self._removeNode(oldNode) | |
| 628 | self._addNode(newNode) | |
| 629 | } | |
| 630 | } | |
| 631 | ||
| 632 | _onrequest(req, external) { | |
| 633 | if (req.from.id !== null) { | |
| 634 | this._addNodeFromNetwork(!external, req.from, req.to) | |
| 635 | } | |
| 636 | ||
| 637 | if (req.internal) { | |
| 638 | switch (req.command) { | |
| 639 | // standard keep alive call | |
| 640 | case PING: { | |
| 641 | req.sendReply(0, null, false, false) | |
| 642 | return | |
| 643 | } | |
| 644 | case DELAYED_PING: { | |
| 645 | this._ondelayedping(req) | |
| 646 | return | |
| 647 | } | |
| 648 | // check if the other side can receive a message to their other socket | |
| 649 | case PING_NAT: { | |
| 650 | if (req.value === null || req.value.byteLength < 2) return | |
| 651 | const port = c.uint16.decode({ start: 0, end: 2, buffer: req.value }) | |
| 652 | if (port === 0) return | |
| 653 | req.from.port = port | |
| 654 | req.sendReply(0, null, false, false) | |
| 655 | return | |
| 656 | } | |
| 657 | // empty dht reply back | |
| 658 | case FIND_NODE: { | |
| 659 | if (!req.target) return | |
| 660 | req.sendReply(0, null, false, true) | |
| 661 | return | |
| 662 | } | |
| 663 | // "this is node you sent me is down" - let's try to ping it | |
| 664 | case DOWN_HINT: { | |
| 665 | if (req.value === null || req.value.byteLength < 6) return | |
| 666 | if (this._checks < 10) { | |
| 667 | sodium.crypto_generichash(TMP, req.value.subarray(0, 6)) | |
| 668 | const node = this.table.get(TMP) | |
| 669 | if (node && (node.pinged < this._tick || node.downHints === 0)) { | |
| 670 | node.downHints++ | |
| 671 | this._check(node) | |
| 672 | } | |
| 673 | } | |
| 674 | req.sendReply(0, null, false, false) | |
| 675 | return | |
| 676 | } | |
| 677 | } | |
| 678 | ||
| 679 | req.sendReply(UNKNOWN_COMMAND, null, false, req.target !== null) | |
| 680 | return | |
| 681 | } | |
| 682 | ||
| 683 | // ask the user to handle it or reply back with a bad command | |
| 684 | if (this.onrequest(req) === false) { | |
| 685 | req.sendReply(UNKNOWN_COMMAND, null, false, req.target !== null) | |
| 686 | } | |
| 687 | } | |
| 688 | ||
| 689 | onrequest(req) { | |
| 690 | return this.emit('request', req) | |
| 691 | } | |
| 692 | ||
| 693 | _ondelayedping(req) { | |
| 694 | if (req.value === null || req.value.byteLength < 4) return | |
| 695 | const delayMs = c.uint32.decode({ start: 0, end: 4, buffer: req.value }) | |
| 696 | if (delayMs > this.maxPingDelay) return | |
| 697 | const timer = setTimeout(() => { | |
| 698 | if (this.destroyed) return | |
| 699 | this._pendingTimers.delete(timer) | |
| 700 | req.sendReply(0, null, false, false) | |
| 701 | }, delayMs) | |
| 702 | this._pendingTimers.add(timer) | |
| 703 | } | |
| 704 | ||
| 705 | _onresponse(res, external) { | |
| 706 | this._addNodeFromNetwork(!external, res.from, res.to) | |
| 707 | } | |
| 708 | ||
| 709 | _ontimeout(req) { | |
| 710 | if (!req.to.id) return | |
| 711 | const node = this.table.get(req.to.id) | |
| 712 | if (node) this._removeNode(node) | |
| 713 | } | |
| 714 | ||
| 715 | _pingSome() { | |
| 716 | let cnt = this.io.inflight.length > 2 ? 3 : 5 | |
| 717 | let oldest = this.nodes.oldest | |
| 718 | ||
| 719 | // tiny dht, pinged the bootstrap again | |
| 720 | if (!oldest) { | |
| 721 | this.refresh() | |
| 722 | return | |
| 723 | } | |
| 724 | ||
| 725 | // we've recently pinged the oldest one, so only trigger a couple of repings | |
| 726 | if (this._tick - oldest.pinged < RECENT_NODE) { | |
| 727 | cnt = 2 | |
| 728 | } | |
| 729 | ||
| 730 | while (cnt--) { | |
| 731 | if (!oldest || this._tick === oldest.pinged) continue | |
| 732 | this._check(oldest) | |
| 733 | oldest = oldest.next | |
| 734 | } | |
| 735 | } | |
| 736 | ||
| 737 | _check(node) { | |
| 738 | node.pinged = this._tick | |
| 739 | ||
| 740 | const lastSeen = node.seen | |
| 741 | const onresponse = () => { | |
| 742 | this._checks-- | |
| 743 | this._removeStaleNode(node, lastSeen) | |
| 744 | } | |
| 745 | const onerror = () => { | |
| 746 | this._checks-- | |
| 747 | this._removeNode(node) | |
| 748 | } | |
| 749 | ||
| 750 | this._checks++ | |
| 751 | this._request( | |
| 752 | { id: null, host: node.host, port: node.port }, | |
| 753 | false, | |
| 754 | true, | |
| 755 | PING, | |
| 756 | null, | |
| 757 | null, | |
| 758 | null, | |
| 759 | onresponse, | |
| 760 | onerror | |
| 761 | ) | |
| 762 | } | |
| 763 | ||
| 764 | _ontick() { | |
| 765 | const time = Date.now() | |
| 766 | ||
| 767 | if (time - this._lastTick > SLEEPING_INTERVAL && this.suspended === false) { | |
| 768 | this._onwakeup() | |
| 769 | } else { | |
| 770 | this._tick++ | |
| 771 | } | |
| 772 | ||
| 773 | this._lastTick = time | |
| 774 | ||
| 775 | if (!this.bootstrapped || this.suspended) return | |
| 776 | ||
| 777 | if (this.adaptive && this.ephemeral && --this._stableTicks <= 0) { | |
| 778 | if (this._lastHost === this._nat.host) { | |
| 779 | // do not recheck the same network... | |
| 780 | this._stableTicks = MORE_STABLE_TICKS | |
| 781 | } else { | |
| 782 | this._updateNetworkState() // the promise returned here never fails so just ignore it | |
| 783 | } | |
| 784 | } | |
| 785 | ||
| 786 | if ((this._tick & 7) === 0) { | |
| 787 | this._pingSome() | |
| 788 | } | |
| 789 | ||
| 790 | if ( | |
| 791 | ((this._tick & 63) === 0 && this.nodes.length < this.table.k) || | |
| 792 | --this._refreshTicks <= 0 | |
| 793 | ) { | |
| 794 | this.refresh() | |
| 795 | } | |
| 796 | ||
| 797 | this._downHintsSentPerTick = 0 | |
| 798 | this.health.update() | |
| 799 | } | |
| 800 | ||
| 801 | async _updateNetworkState(onlyFirewall = false) { | |
| 802 | if (!this.ephemeral) return false | |
| 803 | if (onlyFirewall && !this.firewalled) return false | |
| 804 | ||
| 805 | const { host, port } = this._nat | |
| 806 | ||
| 807 | if (!onlyFirewall) { | |
| 808 | // remember what host we checked and reset the counter | |
| 809 | this._stableTicks = MORE_STABLE_TICKS | |
| 810 | this._lastHost = host | |
| 811 | } | |
| 812 | ||
| 813 | // check if we have a consistent host and port | |
| 814 | if (host === null || port === 0) { | |
| 815 | return false | |
| 816 | } | |
| 817 | ||
| 818 | const natSampler = this.firewalled ? new NatSampler() : this._nat | |
| 819 | ||
| 820 | // ask remote nodes to ping us on our server socket to see if we have the port open | |
| 821 | const firewalled = this.firewalled && (await this._checkIfFirewalled(natSampler)) | |
| 822 | if (firewalled) return false | |
| 823 | ||
| 824 | this.firewalled = this.io.firewalled = false | |
| 825 | ||
| 826 | // incase it's called in parallel for some reason, or if our nat status somehow changed | |
| 827 | if (!this.ephemeral || host !== this._nat.host || port !== this._nat.port) return false | |
| 828 | // if the firewall probe returned a different host / non consistent port, bail as well | |
| 829 | if (natSampler.host !== host || natSampler.port === 0) return false | |
| 830 | ||
| 831 | const id = peer.id(natSampler.host, natSampler.port) | |
| 832 | ||
| 833 | if (!onlyFirewall) { | |
| 834 | this.ephemeral = this.io.ephemeral = false | |
| 835 | } | |
| 836 | ||
| 837 | if (natSampler !== this._nat) { | |
| 838 | const prevHost = this._nat.host | |
| 839 | const prevPort = this._nat.port | |
| 840 | ||
| 841 | this._nonePersistentSamples = [] | |
| 842 | this._nat = natSampler | |
| 843 | ||
| 844 | if (prevHost !== this._nat.host || prevPort !== this._nat.port) { | |
| 845 | this.emit('nat-update', this._nat.host, this._nat.port) | |
| 846 | } | |
| 847 | } | |
| 848 | ||
| 849 | // TODO: we should make this a bit more defensive in terms of using more | |
| 850 | // resources to make sure that the new routing table contains as many alive nodes | |
| 851 | // as possible, vs blindly copying them over... | |
| 852 | ||
| 853 | // all good! copy over the old routing table to the new one | |
| 854 | if (!b4a.equals(this.table.id, id)) { | |
| 855 | const nodes = this.table.toArray() | |
| 856 | ||
| 857 | this.table = this.io.table = new Table(id) | |
| 858 | ||
| 859 | for (const node of nodes) { | |
| 860 | if (b4a.equals(node.id, id)) continue | |
| 861 | if (!this.table.add(node)) this.nodes.remove(node) | |
| 862 | } | |
| 863 | ||
| 864 | this.table.on('row', this._onrow) | |
| 865 | ||
| 866 | // we need to rebootstrap/refresh since we updated our id | |
| 867 | if (this.bootstrapped) this.refresh() | |
| 868 | } | |
| 869 | ||
| 870 | if (!this.ephemeral) { | |
| 871 | this.emit('persistent') | |
| 872 | } | |
| 873 | ||
| 874 | return true | |
| 875 | } | |
| 876 | ||
| 877 | async *_resolveBootstrapNodes() { | |
| 878 | for (let { host, port } of this.bootstrapNodes) { | |
| 879 | let doLookup = false | |
| 880 | ||
| 881 | if (host.indexOf('@') === -1) { | |
| 882 | doLookup = true | |
| 883 | } else { | |
| 884 | const [suggestedIP, fallbackHost] = host.split('@') | |
| 885 | try { | |
| 886 | await this.ping({ host: suggestedIP, port }) | |
| 887 | host = suggestedIP | |
| 888 | } catch { | |
| 889 | host = fallbackHost | |
| 890 | doLookup = true | |
| 891 | } | |
| 892 | } | |
| 893 | ||
| 894 | if (doLookup) { | |
| 895 | try { | |
| 896 | host = UDX.isIPv4(host) ? host : (await this.udx.lookup(host, { family: 4 })).host | |
| 897 | } catch { | |
| 898 | continue | |
| 899 | } | |
| 900 | } | |
| 901 | ||
| 902 | yield { | |
| 903 | id: peer.id(host, port), | |
| 904 | host, | |
| 905 | port | |
| 906 | } | |
| 907 | } | |
| 908 | } | |
| 909 | ||
| 910 | async _addBootstrapNodes(nodes) { | |
| 911 | for await (const node of this._resolveBootstrapNodes()) { | |
| 912 | nodes.push(node) | |
| 913 | } | |
| 914 | } | |
| 915 | ||
| 916 | async _checkIfFirewalled(natSampler = new NatSampler()) { | |
| 917 | const nodes = [] | |
| 918 | for (let node = this.nodes.latest; node && nodes.length < 5; node = node.prev) { | |
| 919 | nodes.push(node) | |
| 920 | } | |
| 921 | ||
| 922 | if (nodes.length < 5) await this._addBootstrapNodes(nodes) | |
| 923 | // if no nodes are available, including bootstrappers - bail | |
| 924 | if (nodes.length === 0) return true | |
| 925 | ||
| 926 | const hosts = new Set() | |
| 927 | const value = b4a.allocUnsafe(2) | |
| 928 | ||
| 929 | c.uint16.encode({ start: 0, end: 2, buffer: value }, this.io.serverSocket.address().port) | |
| 930 | ||
| 931 | // double check they actually came on the server socket... | |
| 932 | this.io.serverSocket.on('message', onmessage) | |
| 933 | ||
| 934 | const pongs = await requestAll(this, true, PING_NAT, value, nodes) | |
| 935 | ||
| 936 | let count = 0 | |
| 937 | for (const res of pongs) { | |
| 938 | if (hosts.has(res.from.host)) { | |
| 939 | count++ | |
| 940 | natSampler.add(res.to.host, res.to.port) | |
| 941 | } | |
| 942 | } | |
| 943 | ||
| 944 | this.io.serverSocket.removeListener('message', onmessage) | |
| 945 | ||
| 946 | // if we got no or very few replies, consider it a fluke | |
| 947 | if (count < (nodes.length >= 5 ? 3 : 1)) return true | |
| 948 | ||
| 949 | // check that the server socket has the same ip as the client socket | |
| 950 | if (natSampler.host === null || this._nat.host !== natSampler.host) return true | |
| 951 | ||
| 952 | // check that the local port of the server socket is the same as the remote port | |
| 953 | // TODO: we might want a flag to opt out of this heuristic for specific remapped port servers | |
| 954 | if (natSampler.port === 0 || natSampler.port !== this.io.serverSocket.address().port) { | |
| 955 | return true | |
| 956 | } | |
| 957 | ||
| 958 | return false | |
| 959 | ||
| 960 | function onmessage(_, { host }) { | |
| 961 | hosts.add(host) | |
| 962 | } | |
| 963 | } | |
| 964 | ||
| 965 | _backgroundQuery(target) { | |
| 966 | this._refreshTicks = REFRESH_TICKS | |
| 967 | ||
| 968 | const backgroundCon = Math.min(this.concurrency, Math.max(2, (this.concurrency / 8) | 0)) | |
| 969 | const q = new Query(this, target, true, FIND_NODE, null, { | |
| 970 | concurrency: backgroundCon | |
| 971 | }) | |
| 972 | ||
| 973 | q.on('data', () => { | |
| 974 | // yield to other traffic | |
| 975 | q.concurrency = this.io.inflight.length < 3 ? this.concurrency : backgroundCon | |
| 976 | }) | |
| 977 | ||
| 978 | return q | |
| 979 | } | |
| 980 | ||
| 981 | // called by health | |
| 982 | _online() { | |
| 983 | if (this.online && !this.degraded) return | |
| 984 | this.online = true | |
| 985 | this.degraded = false | |
| 986 | this.emit('network-update') | |
| 987 | } | |
| 988 | ||
| 989 | // called by health | |
| 990 | _degraded() { | |
| 991 | if (this.degraded) return | |
| 992 | this.online = true | |
| 993 | this.degraded = true | |
| 994 | this.emit('network-update') | |
| 995 | } | |
| 996 | ||
| 997 | // called by health | |
| 998 | _offline() { | |
| 999 | if (!this.online) return | |
| 1000 | this.online = false | |
| 1001 | this.degraded = false | |
| 1002 | this.emit('network-update') | |
| 1003 | } | |
| 1004 | } | |
| 1005 | ||
| 1006 | DHT.OK = 0 | |
| 1007 | DHT.ERROR_UNKNOWN_COMMAND = UNKNOWN_COMMAND | |
| 1008 | DHT.ERROR_INVALID_TOKEN = INVALID_TOKEN | |
| 1009 | ||
| 1010 | module.exports = DHT | |
| 1011 | ||
| 1012 | function localIP(udx, family = 4) { | |
| 1013 | let host = null | |
| 1014 | ||
| 1015 | for (const n of udx.networkInterfaces()) { | |
| 1016 | if (n.family !== family || n.internal) continue | |
| 1017 | ||
| 1018 | // mac really likes en0, mb a better way but this shouldnt be bad anywhere so return now | |
| 1019 | if (n.name === 'en0') return n.host | |
| 1020 | ||
| 1021 | // otherwise pick the first non internal host (let the loop continue in case we see en0) | |
| 1022 | if (host === null) host = n.host | |
| 1023 | } | |
| 1024 | ||
| 1025 | return host || (family === 4 ? '127.0.0.1' : '::1') | |
| 1026 | } | |
| 1027 | ||
| 1028 | function parseNode(s) { | |
| 1029 | if (typeof s === 'object') return s | |
| 1030 | if (typeof s === 'number') return { host: '127.0.0.1', port: s } | |
| 1031 | const [host, port] = s.split(':') | |
| 1032 | if (!port) throw new Error('Bootstrap node format is host:port') | |
| 1033 | ||
| 1034 | return { | |
| 1035 | host, | |
| 1036 | port: Number(port) | |
| 1037 | } | |
| 1038 | } | |
| 1039 | ||
| 1040 | function randomBytes(n) { | |
| 1041 | const b = b4a.alloc(n) | |
| 1042 | sodium.randombytes_buf(b) | |
| 1043 | return b | |
| 1044 | } | |
| 1045 | ||
| 1046 | function randomOffset(n) { | |
| 1047 | return n - ((Math.random() * 0.5 * n) | 0) | |
| 1048 | } | |
| 1049 | ||
| 1050 | function requestAll(dht, internal, command, value, nodes) { | |
| 1051 | let missing = nodes.length | |
| 1052 | const replies = [] | |
| 1053 | ||
| 1054 | return new Promise((resolve) => { | |
| 1055 | for (const node of nodes) { | |
| 1056 | const req = dht._request( | |
| 1057 | node, | |
| 1058 | false, | |
| 1059 | internal, | |
| 1060 | command, | |
| 1061 | null, | |
| 1062 | value, | |
| 1063 | null, | |
| 1064 | onsuccess, | |
| 1065 | onerror | |
| 1066 | ) | |
| 1067 | if (!req) return resolve(replies) | |
| 1068 | } | |
| 1069 | ||
| 1070 | function onsuccess(res) { | |
| 1071 | replies.push(res) | |
| 1072 | if (--missing === 0) resolve(replies) | |
| 1073 | } | |
| 1074 | ||
| 1075 | function onerror() { | |
| 1076 | if (--missing === 0) resolve(replies) | |
| 1077 | } | |
| 1078 | }) | |
| 1079 | } | |
| 1080 | ||
| 1081 | function noop() {} |
| … | … | @@ -0,0 +1,5 @@ |
| 1 | exports.PING = 0 | |
| 2 | exports.PING_NAT = 1 | |
| 3 | exports.FIND_NODE = 2 | |
| 4 | exports.DOWN_HINT = 3 | |
| 5 | exports.DELAYED_PING = 4 |
| … | … | @@ -0,0 +1,29 @@ |
| 1 | module.exports = class DHTError extends Error { | |
| 2 | constructor(msg, code, fn = DHTError) { | |
| 3 | super(`${code}: ${msg}`) | |
| 4 | this.code = code | |
| 5 | ||
| 6 | if (Error.captureStackTrace) { | |
| 7 | Error.captureStackTrace(this, fn) | |
| 8 | } | |
| 9 | } | |
| 10 | ||
| 11 | get name() { | |
| 12 | return 'DHTError' | |
| 13 | } | |
| 14 | ||
| 15 | static UNKNOWN_COMMAND = 1 | |
| 16 | static INVALID_TOKEN = 2 | |
| 17 | ||
| 18 | static REQUEST_TIMEOUT(msg = 'Request timed out') { | |
| 19 | return new DHTError(msg, 'REQUEST_TIMEOUT', DHTError.REQUEST_TIMEOUT) | |
| 20 | } | |
| 21 | ||
| 22 | static REQUEST_DESTROYED(msg = 'Request destroyed') { | |
| 23 | return new DHTError(msg, 'REQUEST_DESTROYED', DHTError.REQUEST_DESTROYED) | |
| 24 | } | |
| 25 | ||
| 26 | static IO_SUSPENDED(msg = 'I/O suspended') { | |
| 27 | return new DHTError(msg, 'IO_SUSPENDED', DHTError.IO_SUSPENDED) | |
| 28 | } | |
| 29 | } |
| … | … | @@ -0,0 +1,110 @@ |
| 1 | const MAX_HEALTH_WINDOW = 4 | |
| 2 | const IDLE_THRESHOLD = 4 | |
| 3 | const DEGRADED_TIMEOUT_RATE_THRESHOLD = 0.5 | |
| 4 | ||
| 5 | module.exports = class NetworkHealth { | |
| 6 | constructor(dht) { | |
| 7 | this._dht = dht | |
| 8 | this._window = [] | |
| 9 | this._head = -1 | |
| 10 | this._degradedTicks = 0 | |
| 11 | this._healthyTicks = 0 | |
| 12 | this.online = true | |
| 13 | this.degraded = false | |
| 14 | } | |
| 15 | ||
| 16 | get oldest() { | |
| 17 | return this._window[this._tail] | |
| 18 | } | |
| 19 | ||
| 20 | get previous() { | |
| 21 | return this._window[(this._head - 1 + MAX_HEALTH_WINDOW) % MAX_HEALTH_WINDOW] | |
| 22 | } | |
| 23 | ||
| 24 | get newest() { | |
| 25 | return this._window[this._head] | |
| 26 | } | |
| 27 | ||
| 28 | get responses() { | |
| 29 | if (!this.newest || !this.previous) return 0 | |
| 30 | return this.newest.responses - this.previous.responses | |
| 31 | } | |
| 32 | ||
| 33 | get timeouts() { | |
| 34 | if (!this.newest || !this.previous) return 0 | |
| 35 | return this.newest.timeouts - this.previous.timeouts | |
| 36 | } | |
| 37 | ||
| 38 | get timeoutsRate() { | |
| 39 | if (this.timeouts === 0) return 0 | |
| 40 | return this.timeouts / (this.responses + this.timeouts) | |
| 41 | } | |
| 42 | ||
| 43 | get cold() { | |
| 44 | return this._window.length < MAX_HEALTH_WINDOW | |
| 45 | } | |
| 46 | ||
| 47 | get idle() { | |
| 48 | return this.responses + this.timeouts < IDLE_THRESHOLD | |
| 49 | } | |
| 50 | ||
| 51 | get allDegraded() { | |
| 52 | return this._degradedTicks === MAX_HEALTH_WINDOW | |
| 53 | } | |
| 54 | ||
| 55 | get allHealthy() { | |
| 56 | return this._healthyTicks === MAX_HEALTH_WINDOW | |
| 57 | } | |
| 58 | ||
| 59 | get stats() { | |
| 60 | return { | |
| 61 | online: this.online, | |
| 62 | degraded: this.degraded, | |
| 63 | cold: this.cold, | |
| 64 | idle: this.idle, | |
| 65 | responses: this.responses, | |
| 66 | timeouts: this.timeouts, | |
| 67 | timeoutsRate: this.timeoutsRate | |
| 68 | } | |
| 69 | } | |
| 70 | ||
| 71 | get _tail() { | |
| 72 | return (this._head + 1) % MAX_HEALTH_WINDOW | |
| 73 | } | |
| 74 | ||
| 75 | reset() { | |
| 76 | this._window = [] | |
| 77 | this._head = -1 | |
| 78 | this._degradedTicks = 0 | |
| 79 | this._healthyTicks = 0 | |
| 80 | this.online = true | |
| 81 | this.degraded = false | |
| 82 | this._dht._online() | |
| 83 | } | |
| 84 | ||
| 85 | update() { | |
| 86 | if (this.oldest?.degraded) this._degradedTicks-- | |
| 87 | else if (this.oldest?.degraded === false) this._healthyTicks-- | |
| 88 | ||
| 89 | this._head = this._tail | |
| 90 | this._window[this._head] = { | |
| 91 | responses: this._dht.stats.requests.responses, | |
| 92 | timeouts: this._dht.stats.requests.timeouts | |
| 93 | } | |
| 94 | ||
| 95 | if (this.cold || this.idle) return | |
| 96 | ||
| 97 | this.newest.degraded = this.timeoutsRate > DEGRADED_TIMEOUT_RATE_THRESHOLD | |
| 98 | ||
| 99 | if (this.newest.degraded) this._degradedTicks++ | |
| 100 | else this._healthyTicks++ | |
| 101 | ||
| 102 | this.online = this.responses > 0 | |
| 103 | if (this.online && this.allDegraded) this.degraded = true | |
| 104 | if (!this.online || this.allHealthy) this.degraded = false | |
| 105 | ||
| 106 | if (this.online && !this.degraded) this._dht._online() | |
| 107 | else if (this.degraded) this._dht._degraded() | |
| 108 | else this._dht._offline() | |
| 109 | } | |
| 110 | } |
| … | … | @@ -0,0 +1,630 @@ |
| 1 | const FIFO = require('fast-fifo') | |
| 2 | const sodium = require('sodium-universal') | |
| 3 | const c = require('compact-encoding') | |
| 4 | const b4a = require('b4a') | |
| 5 | const AdaptiveTimeout = require('adaptive-timeout') | |
| 6 | const peer = require('./peer') | |
| 7 | const { INVALID_TOKEN, REQUEST_TIMEOUT, REQUEST_DESTROYED, IO_SUSPENDED } = require('./errors') | |
| 8 | ||
| 9 | const VERSION = 0b11 | |
| 10 | const RESPONSE_ID = (0b0001 << 4) | VERSION | |
| 11 | const REQUEST_ID = (0b0000 << 4) | VERSION | |
| 12 | const EMPTY_ARRAY = [] | |
| 13 | const MAX_WINDOW = 80 | |
| 14 | ||
| 15 | module.exports = class IO { | |
| 16 | constructor( | |
| 17 | table, | |
| 18 | udx, | |
| 19 | { | |
| 20 | maxWindow = MAX_WINDOW, | |
| 21 | port = 0, | |
| 22 | host = '0.0.0.0', | |
| 23 | anyPort = true, | |
| 24 | firewalled = true, | |
| 25 | onrequest, | |
| 26 | onresponse = noop, | |
| 27 | ontimeout = noop, | |
| 28 | adaptiveTimeout | |
| 29 | } = {} | |
| 30 | ) { | |
| 31 | this.table = table | |
| 32 | this.udx = udx | |
| 33 | this.inflight = [] | |
| 34 | this.clientSocket = null | |
| 35 | this.serverSocket = null | |
| 36 | this.firewalled = firewalled !== false | |
| 37 | this.ephemeral = true | |
| 38 | this.congestion = new CongestionWindow(maxWindow) | |
| 39 | this.networkInterfaces = udx.watchNetworkInterfaces() | |
| 40 | this.suspended = false | |
| 41 | ||
| 42 | this.stats = { | |
| 43 | requests: { | |
| 44 | active: 0, | |
| 45 | total: 0, | |
| 46 | responses: 0, | |
| 47 | timeouts: 0, | |
| 48 | retries: 0 | |
| 49 | }, | |
| 50 | commands: [ | |
| 51 | { tx: 0, rx: 0 }, // tx = transmitted, rx = received | |
| 52 | { tx: 0, rx: 0 }, | |
| 53 | { tx: 0, rx: 0 }, | |
| 54 | { tx: 0, rx: 0 } | |
| 55 | ] | |
| 56 | } | |
| 57 | ||
| 58 | this.onrequest = onrequest | |
| 59 | this.onresponse = onresponse | |
| 60 | this.ontimeout = ontimeout | |
| 61 | ||
| 62 | this._pending = new FIFO() | |
| 63 | this._rotateSecrets = 10 | |
| 64 | this._tid = (Math.random() * 65536) | 0 | |
| 65 | this._secrets = null | |
| 66 | this._drainInterval = null | |
| 67 | this._destroying = null | |
| 68 | this._binding = null | |
| 69 | ||
| 70 | // port can be a number or a range [start, to] | |
| 71 | this.portRange = port.length ? port : port === 0 ? [0, 0] : [port, port + 5] | |
| 72 | ||
| 73 | this._host = host | |
| 74 | this._anyPort = anyPort !== false | |
| 75 | this._boundServerPort = 0 | |
| 76 | this._boundClientPort = 0 | |
| 77 | ||
| 78 | this._adt = adaptiveTimeout ? new AdaptiveTimeout(adaptiveTimeout) : null | |
| 79 | } | |
| 80 | ||
| 81 | static DEFAULT_MAX_WINDOW = MAX_WINDOW | |
| 82 | ||
| 83 | onmessage(socket, buffer, { host, port }) { | |
| 84 | if (buffer.byteLength < 2 || !(port > 0 && port < 65536) || this.suspended === true) return | |
| 85 | ||
| 86 | const from = { id: null, host, port } | |
| 87 | const state = { start: 1, end: buffer.byteLength, buffer } | |
| 88 | const expectedSocket = this.firewalled ? this.clientSocket : this.serverSocket | |
| 89 | const external = socket !== expectedSocket | |
| 90 | ||
| 91 | if (buffer[0] === REQUEST_ID) { | |
| 92 | const req = Request.decode(this, socket, from, state) | |
| 93 | if (req === null) return | |
| 94 | if ( | |
| 95 | req.token !== null && | |
| 96 | !b4a.equals(req.token, this.token(req.from, 1)) && | |
| 97 | !b4a.equals(req.token, this.token(req.from, 0)) | |
| 98 | ) { | |
| 99 | req.error(INVALID_TOKEN, { token: true }) | |
| 100 | return | |
| 101 | } | |
| 102 | this.onrequest(req, external) | |
| 103 | return | |
| 104 | } | |
| 105 | ||
| 106 | if (buffer[0] === RESPONSE_ID) { | |
| 107 | const res = decodeReply(from, state) | |
| 108 | if (res === null) return | |
| 109 | ||
| 110 | for (let i = 0; i < this.inflight.length; i++) { | |
| 111 | const req = this.inflight[i] | |
| 112 | if (req.tid !== res.tid) continue | |
| 113 | ||
| 114 | res.rtt = Date.now() - req._timestamp | |
| 115 | ||
| 116 | if (this._adt && req.sent <= 2) { | |
| 117 | this._adt.put(`${req.to.host}:${req.to.port}`, res.rtt) | |
| 118 | } | |
| 119 | ||
| 120 | if (i === this.inflight.length - 1) this.inflight.pop() | |
| 121 | else this.inflight[i] = this.inflight.pop() | |
| 122 | ||
| 123 | if (req.session) req.session._detach(req) | |
| 124 | ||
| 125 | // TODO: Auto retry here if errors.INVALID_TOKEN is returned? | |
| 126 | ||
| 127 | if (req._timeout) { | |
| 128 | clearTimeout(req._timeout) | |
| 129 | req._timeout = null | |
| 130 | } | |
| 131 | ||
| 132 | this.congestion.recv() | |
| 133 | ||
| 134 | if (req.internal && req.command < this.stats.commands.length) { | |
| 135 | this.stats.commands[req.command].rx++ | |
| 136 | } | |
| 137 | ||
| 138 | this.stats.requests.active-- | |
| 139 | this.stats.requests.responses++ | |
| 140 | ||
| 141 | this.onresponse(res, external) | |
| 142 | req.onresponse(res, req) | |
| 143 | break | |
| 144 | } | |
| 145 | } | |
| 146 | } | |
| 147 | ||
| 148 | token(addr, i) { | |
| 149 | if (this._secrets === null) { | |
| 150 | const buf = b4a.alloc(64) | |
| 151 | this._secrets = [buf.subarray(0, 32), buf.subarray(32, 64)] | |
| 152 | sodium.randombytes_buf(this._secrets[0]) | |
| 153 | sodium.randombytes_buf(this._secrets[1]) | |
| 154 | } | |
| 155 | ||
| 156 | const token = b4a.allocUnsafe(32) | |
| 157 | sodium.crypto_generichash(token, b4a.from(addr.host), this._secrets[i]) | |
| 158 | return token | |
| 159 | } | |
| 160 | ||
| 161 | async destroy() { | |
| 162 | if (this._destroying) return this._destroying | |
| 163 | this._destroying = this._destroy() | |
| 164 | return this._destroying | |
| 165 | } | |
| 166 | ||
| 167 | async _destroy() { | |
| 168 | // simplifies timing to await the bind here also, although it might be unneeded | |
| 169 | await this.bind() | |
| 170 | await this._clear(false) | |
| 171 | } | |
| 172 | ||
| 173 | async _clear(suspended) { | |
| 174 | if (this._drainInterval) { | |
| 175 | clearInterval(this._drainInterval) | |
| 176 | this._drainInterval = null | |
| 177 | } | |
| 178 | ||
| 179 | while (this.inflight.length) { | |
| 180 | const req = this.inflight.pop() | |
| 181 | if (req._timeout) clearTimeout(req._timeout) | |
| 182 | req._timeout = null | |
| 183 | req.destroyed = true | |
| 184 | ||
| 185 | if (req.session) req.session._detach(req) | |
| 186 | ||
| 187 | this.congestion.recv() | |
| 188 | this.stats.requests.active-- | |
| 189 | ||
| 190 | req.onerror(suspended ? IO_SUSPENDED() : REQUEST_DESTROYED(), req) | |
| 191 | } | |
| 192 | ||
| 193 | await Promise.allSettled([this.serverSocket.close(), this.clientSocket.close()]) | |
| 194 | ||
| 195 | this.networkInterfaces.destroy() | |
| 196 | } | |
| 197 | ||
| 198 | async suspend() { | |
| 199 | this.suspended = true | |
| 200 | await this._clear(true) | |
| 201 | ||
| 202 | this.congestion.clear() | |
| 203 | ||
| 204 | if (this._drainInterval) { | |
| 205 | clearInterval(this._drainInterval) | |
| 206 | this._drainInterval = null | |
| 207 | } | |
| 208 | } | |
| 209 | ||
| 210 | async _rebind(binding) { | |
| 211 | if (binding) await binding | |
| 212 | if (this._destroying) return this._destroying | |
| 213 | await this._bindSockets() | |
| 214 | this.networkInterfaces = this.udx.watchNetworkInterfaces() | |
| 215 | } | |
| 216 | ||
| 217 | resume() { | |
| 218 | this.suspended = false | |
| 219 | const binding = this._binding | |
| 220 | this._binding = this._rebind(binding) | |
| 221 | return this._binding | |
| 222 | } | |
| 223 | ||
| 224 | bind() { | |
| 225 | if (this._binding) return this._binding | |
| 226 | this._binding = this._bindSockets() | |
| 227 | return this._binding | |
| 228 | } | |
| 229 | ||
| 230 | async _bindSockets() { | |
| 231 | const serverSocket = this.udx.createSocket() | |
| 232 | ||
| 233 | const candidatePorts = [] | |
| 234 | ||
| 235 | // Retrying previous port always has precedence | |
| 236 | if (this._boundServerPort) candidatePorts.push(this._boundServerPort) | |
| 237 | ||
| 238 | for (let i = this.portRange[0]; i < this.portRange[1]; i++) candidatePorts.push(i) | |
| 239 | ||
| 240 | for (const port of candidatePorts) { | |
| 241 | if (serverSocket.bound) break | |
| 242 | ||
| 243 | try { | |
| 244 | serverSocket.bind(port, this._host) | |
| 245 | } catch (err) { | |
| 246 | if (!this._anyPort) { | |
| 247 | await serverSocket.close() | |
| 248 | throw err | |
| 249 | } | |
| 250 | } | |
| 251 | } | |
| 252 | ||
| 253 | if (!serverSocket.bound) { | |
| 254 | try { | |
| 255 | serverSocket.bind(0, this._host) | |
| 256 | } catch (err) { | |
| 257 | await serverSocket.close() | |
| 258 | throw err | |
| 259 | } | |
| 260 | } | |
| 261 | ||
| 262 | const clientSocket = this.udx.createSocket() | |
| 263 | ||
| 264 | try { | |
| 265 | clientSocket.bind(this._boundClientPort || 0, this._host) | |
| 266 | } catch { | |
| 267 | try { | |
| 268 | clientSocket.bind(0, this._host) | |
| 269 | } catch (err) { | |
| 270 | await serverSocket.close() | |
| 271 | await clientSocket.close() | |
| 272 | throw err | |
| 273 | } | |
| 274 | } | |
| 275 | ||
| 276 | this._boundServerPort = serverSocket.address().port | |
| 277 | this._boundClientPort = clientSocket.address().port | |
| 278 | ||
| 279 | this.clientSocket = clientSocket | |
| 280 | this.serverSocket = serverSocket | |
| 281 | ||
| 282 | this.serverSocket.on('message', this.onmessage.bind(this, this.serverSocket)) | |
| 283 | this.clientSocket.on('message', this.onmessage.bind(this, this.clientSocket)) | |
| 284 | ||
| 285 | if (this._drainInterval === null) { | |
| 286 | this._drainInterval = setInterval(this._drain.bind(this), 750) | |
| 287 | if (this._drainInterval.unref) this._drainInterval.unref() | |
| 288 | } | |
| 289 | ||
| 290 | for (const req of this.inflight) { | |
| 291 | if (!req.socket) req.socket = this.firewalled ? this.clientSocket : this.serverSocket | |
| 292 | req.sent = 0 | |
| 293 | req.send(false) | |
| 294 | } | |
| 295 | } | |
| 296 | ||
| 297 | _drain() { | |
| 298 | if (this._secrets !== null && --this._rotateSecrets === 0) { | |
| 299 | this._rotateSecrets = 10 | |
| 300 | const tmp = this._secrets[0] | |
| 301 | this._secrets[0] = this._secrets[1] | |
| 302 | this._secrets[1] = tmp | |
| 303 | sodium.crypto_generichash(tmp, tmp) | |
| 304 | } | |
| 305 | ||
| 306 | this.congestion.drain() | |
| 307 | ||
| 308 | while (!this.congestion.isFull()) { | |
| 309 | const p = this._pending.shift() | |
| 310 | if (p === undefined) return | |
| 311 | p._sendNow() | |
| 312 | } | |
| 313 | } | |
| 314 | ||
| 315 | createRequest(to, token, internal, command, target, value, session, ttl) { | |
| 316 | if (this._destroying !== null) return null | |
| 317 | ||
| 318 | if (this._tid === 65536) this._tid = 0 | |
| 319 | ||
| 320 | const tid = this._tid++ | |
| 321 | const socket = this.firewalled ? this.clientSocket : this.serverSocket | |
| 322 | ||
| 323 | const req = new Request( | |
| 324 | this, | |
| 325 | socket, | |
| 326 | tid, | |
| 327 | null, | |
| 328 | to, | |
| 329 | token, | |
| 330 | internal, | |
| 331 | command, | |
| 332 | target, | |
| 333 | value, | |
| 334 | session, | |
| 335 | ttl || 0 | |
| 336 | ) | |
| 337 | this.inflight.push(req) | |
| 338 | if (session) session._attach(req) | |
| 339 | ||
| 340 | if (internal && command < this.stats.commands.length) { | |
| 341 | this.stats.commands[command].tx++ | |
| 342 | } | |
| 343 | ||
| 344 | this.stats.requests.active++ | |
| 345 | this.stats.requests.total++ | |
| 346 | ||
| 347 | return req | |
| 348 | } | |
| 349 | } | |
| 350 | ||
| 351 | class Request { | |
| 352 | constructor(io, socket, tid, from, to, token, internal, command, target, value, session, ttl) { | |
| 353 | this.socket = socket | |
| 354 | this.tid = tid | |
| 355 | this.from = from | |
| 356 | this.to = to | |
| 357 | this.token = token | |
| 358 | this.command = command | |
| 359 | this.target = target | |
| 360 | this.value = value | |
| 361 | this.internal = internal | |
| 362 | this.session = session | |
| 363 | this.ttl = ttl | |
| 364 | this.index = -1 | |
| 365 | this.sent = 0 | |
| 366 | this.retries = 3 | |
| 367 | this.destroyed = false | |
| 368 | this.timeout = 0 | |
| 369 | ||
| 370 | this.oncycle = noop | |
| 371 | this.onerror = noop | |
| 372 | this.onresponse = noop | |
| 373 | ||
| 374 | this._buffer = null | |
| 375 | this._io = io | |
| 376 | this._timeout = null | |
| 377 | this._timestamp = Date.now() | |
| 378 | } | |
| 379 | ||
| 380 | static decode(io, socket, from, state) { | |
| 381 | try { | |
| 382 | const flags = c.uint.decode(state) | |
| 383 | const tid = c.uint16.decode(state) | |
| 384 | const to = peer.ipv4.decode(state) | |
| 385 | const id = flags & 1 ? c.fixed32.decode(state) : null | |
| 386 | const token = flags & 2 ? c.fixed32.decode(state) : null | |
| 387 | const internal = (flags & 4) !== 0 | |
| 388 | const command = c.uint.decode(state) | |
| 389 | const target = flags & 8 ? c.fixed32.decode(state) : null | |
| 390 | const value = flags & 16 ? c.buffer.decode(state) : null | |
| 391 | ||
| 392 | if (id !== null) from.id = validateId(id, from) | |
| 393 | ||
| 394 | return new Request( | |
| 395 | io, | |
| 396 | socket, | |
| 397 | tid, | |
| 398 | from, | |
| 399 | to, | |
| 400 | token, | |
| 401 | internal, | |
| 402 | command, | |
| 403 | target, | |
| 404 | value, | |
| 405 | null, | |
| 406 | 0 | |
| 407 | ) | |
| 408 | } catch { | |
| 409 | return null | |
| 410 | } | |
| 411 | } | |
| 412 | ||
| 413 | reply(value, opts = {}) { | |
| 414 | const socket = opts.socket || this.socket | |
| 415 | const to = opts.to || this.from | |
| 416 | this._sendReply(0, value || null, opts.token !== false, opts.closerNodes !== false, to, socket) | |
| 417 | } | |
| 418 | ||
| 419 | error(code, opts = {}) { | |
| 420 | const socket = opts.socket || this.socket | |
| 421 | const to = opts.to || this.from | |
| 422 | this._sendReply(code, null, opts.token === true, opts.closerNodes !== false, to, socket) | |
| 423 | } | |
| 424 | ||
| 425 | relay(value, to, opts) { | |
| 426 | const socket = (opts && opts.socket) || this.socket | |
| 427 | const buffer = this._encodeRequest(null, value, to, socket) | |
| 428 | socket.trySend(buffer, to.port, to.host, this.ttl) | |
| 429 | } | |
| 430 | ||
| 431 | send(force = false) { | |
| 432 | if (this.destroyed) return | |
| 433 | ||
| 434 | if (this.socket === null) return | |
| 435 | if (this._buffer === null) { | |
| 436 | this._buffer = this._encodeRequest(this.token, this.value, this.to, this.socket) | |
| 437 | } | |
| 438 | ||
| 439 | if (!force && this._io.congestion.isFull()) { | |
| 440 | this._io._pending.push(this) | |
| 441 | return | |
| 442 | } | |
| 443 | ||
| 444 | this._sendNow() | |
| 445 | } | |
| 446 | ||
| 447 | sendReply(error, value, token, hasCloserNodes) { | |
| 448 | this._sendReply(error, value, token, hasCloserNodes, this.from, this.socket, null) | |
| 449 | } | |
| 450 | ||
| 451 | _sendNow() { | |
| 452 | if (this.destroyed) return | |
| 453 | this.sent++ | |
| 454 | this._io.congestion.send() | |
| 455 | this.socket.trySend(this._buffer, this.to.port, this.to.host, this.ttl) | |
| 456 | if (this._timeout) clearTimeout(this._timeout) | |
| 457 | const value = | |
| 458 | this.timeout || this._io._adt?.get(`${this.to.host}:${this.to.port}`, this.sent) || 1000 | |
| 459 | this._timeout = setTimeout(oncycle, value, this) | |
| 460 | } | |
| 461 | ||
| 462 | destroy(err) { | |
| 463 | if (this.destroyed) return | |
| 464 | this.destroyed = true | |
| 465 | ||
| 466 | if (this._timeout) { | |
| 467 | clearTimeout(this._timeout) | |
| 468 | this._timeout = null | |
| 469 | } | |
| 470 | ||
| 471 | const i = this._io.inflight.indexOf(this) | |
| 472 | if (i === -1) return | |
| 473 | ||
| 474 | if (i === this._io.inflight.length - 1) this._io.inflight.pop() | |
| 475 | else this._io.inflight[i] = this._io.inflight.pop() | |
| 476 | ||
| 477 | if (this.session) this.session._detach(this) | |
| 478 | ||
| 479 | this._io.stats.requests.active-- | |
| 480 | this._io.congestion.recv() | |
| 481 | ||
| 482 | this.onerror(err || REQUEST_DESTROYED(), this) | |
| 483 | } | |
| 484 | ||
| 485 | _sendReply(error, value, token, hasCloserNodes, from, socket) { | |
| 486 | if (socket === null || this.destroyed) return | |
| 487 | ||
| 488 | const id = this._io.ephemeral === false && socket === this._io.serverSocket | |
| 489 | const closerNodes = | |
| 490 | this.target !== null && hasCloserNodes ? this._io.table.closest(this.target) : EMPTY_ARRAY | |
| 491 | const state = { start: 0, end: 1 + 1 + 6 + 2, buffer: null } // (type | version) + flags + to + tid | |
| 492 | ||
| 493 | if (id) state.end += 32 | |
| 494 | if (token) state.end += 32 | |
| 495 | if (closerNodes.length > 0) peer.ipv4Array.preencode(state, closerNodes) | |
| 496 | if (error > 0) c.uint.preencode(state, error) | |
| 497 | if (value) c.buffer.preencode(state, value) | |
| 498 | ||
| 499 | state.buffer = b4a.allocUnsafe(state.end) | |
| 500 | state.buffer[state.start++] = RESPONSE_ID | |
| 501 | state.buffer[state.start++] = | |
| 502 | (id ? 1 : 0) | | |
| 503 | (token ? 2 : 0) | | |
| 504 | (closerNodes.length > 0 ? 4 : 0) | | |
| 505 | (error > 0 ? 8 : 0) | | |
| 506 | (value ? 16 : 0) | |
| 507 | ||
| 508 | c.uint16.encode(state, this.tid) | |
| 509 | peer.ipv4.encode(state, from) | |
| 510 | ||
| 511 | if (id) c.fixed32.encode(state, this._io.table.id) | |
| 512 | if (token) c.fixed32.encode(state, this._io.token(from, 1)) | |
| 513 | if (closerNodes.length > 0) peer.ipv4Array.encode(state, closerNodes) | |
| 514 | if (error > 0) c.uint.encode(state, error) | |
| 515 | if (value) c.buffer.encode(state, value) | |
| 516 | ||
| 517 | socket.trySend(state.buffer, from.port, from.host, this.ttl) | |
| 518 | } | |
| 519 | ||
| 520 | _encodeRequest(token, value, to, socket) { | |
| 521 | const id = this._io.ephemeral === false && socket === this._io.serverSocket | |
| 522 | const state = { start: 0, end: 1 + 1 + 6 + 2, buffer: null } // (type | version) + flags + to + tid | |
| 523 | ||
| 524 | if (id) state.end += 32 | |
| 525 | if (token) state.end += 32 | |
| 526 | ||
| 527 | c.uint.preencode(state, this.command) | |
| 528 | ||
| 529 | if (this.target) state.end += 32 | |
| 530 | if (value) c.buffer.preencode(state, value) | |
| 531 | ||
| 532 | state.buffer = b4a.allocUnsafe(state.end) | |
| 533 | state.buffer[state.start++] = REQUEST_ID | |
| 534 | state.buffer[state.start++] = | |
| 535 | (id ? 1 : 0) | | |
| 536 | (token ? 2 : 0) | | |
| 537 | (this.internal ? 4 : 0) | | |
| 538 | (this.target ? 8 : 0) | | |
| 539 | (value ? 16 : 0) | |
| 540 | ||
| 541 | c.uint16.encode(state, this.tid) | |
| 542 | peer.ipv4.encode(state, to) | |
| 543 | ||
| 544 | if (id) c.fixed32.encode(state, this._io.table.id) | |
| 545 | if (token) c.fixed32.encode(state, token) | |
| 546 | ||
| 547 | c.uint.encode(state, this.command) | |
| 548 | ||
| 549 | if (this.target) c.fixed32.encode(state, this.target) | |
| 550 | if (value) c.buffer.encode(state, value) | |
| 551 | ||
| 552 | return state.buffer | |
| 553 | } | |
| 554 | } | |
| 555 | ||
| 556 | class CongestionWindow { | |
| 557 | constructor(maxWindow) { | |
| 558 | this._i = 0 | |
| 559 | this._total = 0 | |
| 560 | this._window = [0, 0, 0, 0] | |
| 561 | this._maxWindow = maxWindow | |
| 562 | } | |
| 563 | ||
| 564 | clear() { | |
| 565 | this._i = 0 | |
| 566 | this._total = 0 | |
| 567 | this._window = [0, 0, 0, 0] | |
| 568 | } | |
| 569 | ||
| 570 | isFull() { | |
| 571 | return this._total >= 2 * this._maxWindow || this._window[this._i] >= this._maxWindow | |
| 572 | } | |
| 573 | ||
| 574 | recv() { | |
| 575 | if (this._window[this._i] > 0) { | |
| 576 | this._window[this._i]-- | |
| 577 | this._total-- | |
| 578 | } | |
| 579 | } | |
| 580 | ||
| 581 | send() { | |
| 582 | this._total++ | |
| 583 | this._window[this._i]++ | |
| 584 | } | |
| 585 | ||
| 586 | drain() { | |
| 587 | this._i = (this._i + 1) & 3 | |
| 588 | this._total -= this._window[this._i] | |
| 589 | this._window[this._i] = 0 // clear oldest | |
| 590 | } | |
| 591 | } | |
| 592 | ||
| 593 | function noop() {} | |
| 594 | ||
| 595 | function oncycle(req) { | |
| 596 | req._timeout = null | |
| 597 | req.oncycle(req) | |
| 598 | if (req.sent > req.retries) { | |
| 599 | req._io.stats.requests.timeouts++ | |
| 600 | req.destroy(REQUEST_TIMEOUT()) | |
| 601 | req._io.ontimeout(req) | |
| 602 | } else { | |
| 603 | req._io.stats.requests.retries++ | |
| 604 | req.send() | |
| 605 | } | |
| 606 | } | |
| 607 | ||
| 608 | function decodeReply(from, state) { | |
| 609 | try { | |
| 610 | const flags = c.uint.decode(state) | |
| 611 | const tid = c.uint16.decode(state) | |
| 612 | const to = peer.ipv4.decode(state) | |
| 613 | const id = flags & 1 ? c.fixed32.decode(state) : null | |
| 614 | const token = flags & 2 ? c.fixed32.decode(state) : null | |
| 615 | const closerNodes = flags & 4 ? peer.ipv4Array.decode(state) : null | |
| 616 | const error = flags & 8 ? c.uint.decode(state) : 0 | |
| 617 | const value = flags & 16 ? c.buffer.decode(state) : null | |
| 618 | ||
| 619 | if (id !== null) from.id = validateId(id, from) | |
| 620 | ||
| 621 | return { tid, rtt: 0, from, to, token, closerNodes, error, value } | |
| 622 | } catch { | |
| 623 | return null | |
| 624 | } | |
| 625 | } | |
| 626 | ||
| 627 | function validateId(id, from) { | |
| 628 | const expected = peer.id(from.host, from.port) | |
| 629 | return b4a.equals(expected, id) ? expected : null | |
| 630 | } |
| … | … | @@ -0,0 +1,25 @@ |
| 1 | const sodium = require('sodium-universal') | |
| 2 | const c = require('compact-encoding') | |
| 3 | const net = require('compact-encoding-net') | |
| 4 | const b4a = require('b4a') | |
| 5 | ||
| 6 | const ipv4 = { | |
| 7 | ...net.ipv4Address, | |
| 8 | decode(state) { | |
| 9 | const ip = net.ipv4Address.decode(state) | |
| 10 | return { | |
| 11 | id: null, // populated by the callee | |
| 12 | host: ip.host, | |
| 13 | port: ip.port | |
| 14 | } | |
| 15 | } | |
| 16 | } | |
| 17 | ||
| 18 | module.exports = { id, ipv4, ipv4Array: c.array(ipv4) } | |
| 19 | ||
| 20 | function id(host, port, out = b4a.allocUnsafeSlow(32)) { | |
| 21 | const addr = out.subarray(0, 6) | |
| 22 | ipv4.encode({ start: 0, end: 6, buffer: addr }, { host, port }) | |
| 23 | sodium.crypto_generichash(out, addr) | |
| 24 | return out | |
| 25 | } |
| … | … | @@ -0,0 +1,409 @@ |
| 1 | const { Readable, getStreamError } = require('streamx') | |
| 2 | const b4a = require('b4a') | |
| 3 | const peer = require('./peer') | |
| 4 | const { DOWN_HINT } = require('./commands') | |
| 5 | ||
| 6 | const DONE = [] | |
| 7 | const DOWN = [] | |
| 8 | ||
| 9 | module.exports = class Query extends Readable { | |
| 10 | constructor(dht, target, internal, command, value, opts = {}) { | |
| 11 | super() | |
| 12 | ||
| 13 | dht.stats.queries.total++ | |
| 14 | dht.stats.queries.active++ | |
| 15 | ||
| 16 | this.force = !!opts.force | |
| 17 | this.dht = dht | |
| 18 | this.k = this.dht.table.k | |
| 19 | this.target = target | |
| 20 | this.internal = internal | |
| 21 | this.command = command | |
| 22 | this.value = value | |
| 23 | this.errors = 0 | |
| 24 | this.successes = 0 | |
| 25 | this.concurrency = opts.concurrency || this.dht.concurrency | |
| 26 | this.inflight = 0 | |
| 27 | this.map = opts.map || defaultMap | |
| 28 | this.retries = | |
| 29 | opts.retries === 0 ? 0 : opts.retries || (this.internal && command === DOWN_HINT ? 3 : 5) | |
| 30 | this.closestReplies = [] | |
| 31 | ||
| 32 | this._slow = 0 | |
| 33 | this._slowdown = false | |
| 34 | this._seen = new Map() | |
| 35 | this._pending = [] | |
| 36 | this._fromTable = false | |
| 37 | this._commit = opts.commit === true ? autoCommit : opts.commit || null | |
| 38 | this._commiting = false | |
| 39 | this._session = opts.session || dht.session() | |
| 40 | this._autoDestroySession = !opts.session | |
| 41 | this._onlyClosestNodes = false | |
| 42 | ||
| 43 | this._onvisitbound = this._onvisit.bind(this) | |
| 44 | this._onerrorbound = this._onerror.bind(this) | |
| 45 | this._oncyclebound = this._oncycle.bind(this) | |
| 46 | ||
| 47 | const nodes = opts.nodes || opts.closestNodes | |
| 48 | const replies = opts.replies || opts.closestReplies | |
| 49 | ||
| 50 | // add them reverse as we pop below | |
| 51 | if (nodes) { | |
| 52 | for (let i = nodes.length - 1; i >= 0; i--) { | |
| 53 | const node = nodes[i] | |
| 54 | this._addPending( | |
| 55 | { | |
| 56 | id: node.id || peer.id(node.host, node.port), | |
| 57 | host: node.host, | |
| 58 | port: node.port | |
| 59 | }, | |
| 60 | null | |
| 61 | ) | |
| 62 | } | |
| 63 | } else if (replies) { | |
| 64 | for (let i = replies.length - 1; i >= 0; i--) { | |
| 65 | this._addPending(replies[i].from, null) | |
| 66 | } | |
| 67 | } | |
| 68 | ||
| 69 | if (opts.onlyClosestNodes) this._onlyClosestNodes = true | |
| 70 | } | |
| 71 | ||
| 72 | get closestNodes() { | |
| 73 | const nodes = new Array(this.closestReplies.length) | |
| 74 | ||
| 75 | for (let i = 0; i < nodes.length; i++) { | |
| 76 | nodes[i] = this.closestReplies[i].from | |
| 77 | } | |
| 78 | ||
| 79 | return nodes | |
| 80 | } | |
| 81 | ||
| 82 | finished() { | |
| 83 | return new Promise((resolve, reject) => { | |
| 84 | if (this.destroyed) { | |
| 85 | const error = getStreamError(this) | |
| 86 | if (error) reject(error) | |
| 87 | else resolve() | |
| 88 | return | |
| 89 | } | |
| 90 | ||
| 91 | const self = this | |
| 92 | let error = null | |
| 93 | ||
| 94 | this.resume() | |
| 95 | this.on('error', onerror) | |
| 96 | this.on('close', onclose) | |
| 97 | ||
| 98 | function onclose() { | |
| 99 | self.removeListener('error', onerror) | |
| 100 | self.removeListener('close', onclose) | |
| 101 | if (error) reject(error) | |
| 102 | else resolve() | |
| 103 | } | |
| 104 | ||
| 105 | function onerror(err) { | |
| 106 | error = err | |
| 107 | } | |
| 108 | }) | |
| 109 | } | |
| 110 | ||
| 111 | _addFromTable() { | |
| 112 | if (this._pending.length >= this.k) return | |
| 113 | this._fromTable = true | |
| 114 | ||
| 115 | const closest = this.dht.table.closest(this.target, this.k - this._pending.length) | |
| 116 | ||
| 117 | for (const node of closest) { | |
| 118 | this._addPending({ id: node.id, host: node.host, port: node.port }, null) | |
| 119 | } | |
| 120 | } | |
| 121 | ||
| 122 | async _open(cb) { | |
| 123 | this._addFromTable() | |
| 124 | if (this._pending.length >= this.k) return cb(null) | |
| 125 | ||
| 126 | for await (const node of this.dht._resolveBootstrapNodes()) { | |
| 127 | this._addPending(node, null) | |
| 128 | } | |
| 129 | ||
| 130 | cb(null) | |
| 131 | } | |
| 132 | ||
| 133 | _isCloser(id) { | |
| 134 | return ( | |
| 135 | this.closestReplies.length < this.k || | |
| 136 | this._compare(id, this.closestReplies[this.closestReplies.length - 1].from.id) < 0 | |
| 137 | ) | |
| 138 | } | |
| 139 | ||
| 140 | _addPending(node, ref) { | |
| 141 | if (this._onlyClosestNodes) return false | |
| 142 | ||
| 143 | const addr = node.host + ':' + node.port | |
| 144 | const refs = this._seen.get(addr) | |
| 145 | const isCloser = this._isCloser(node.id) | |
| 146 | ||
| 147 | if (refs === DONE) { | |
| 148 | return isCloser | |
| 149 | } | |
| 150 | ||
| 151 | if (refs === DOWN) { | |
| 152 | if (ref) this._downHint(ref, node) | |
| 153 | return isCloser | |
| 154 | } | |
| 155 | ||
| 156 | if (refs) { | |
| 157 | if (ref !== null) refs.push(ref) | |
| 158 | return isCloser | |
| 159 | } | |
| 160 | ||
| 161 | if (!isCloser) { | |
| 162 | return false | |
| 163 | } | |
| 164 | ||
| 165 | this._seen.set(addr, ref === null ? [] : [ref]) | |
| 166 | this._pending.push(node) | |
| 167 | ||
| 168 | return true | |
| 169 | } | |
| 170 | ||
| 171 | _read(cb) { | |
| 172 | this._readMore() | |
| 173 | cb(null) | |
| 174 | } | |
| 175 | ||
| 176 | _readMore() { | |
| 177 | if (this.destroying || this._commiting) return | |
| 178 | ||
| 179 | const concurrency = (this._slowdown ? 3 : this.concurrency) + this._slow | |
| 180 | ||
| 181 | while (this.inflight < concurrency && this._pending.length > 0) { | |
| 182 | const next = this._pending.pop() | |
| 183 | if (next && next.id && !this._isCloser(next.id)) continue | |
| 184 | this._visit(next) | |
| 185 | } | |
| 186 | ||
| 187 | // if reusing closest nodes, slow down after the first readMore tick to allow | |
| 188 | // the closest node a chance to reply before going broad to question more | |
| 189 | if (!this._fromTable && this.successes === 0 && this.errors === 0) { | |
| 190 | this._slowdown = true | |
| 191 | } | |
| 192 | ||
| 193 | if (this._pending.length > 0) return | |
| 194 | ||
| 195 | // if no inflight OR all the queries we are waiting on are marked as slow and we have a full result. | |
| 196 | if ( | |
| 197 | this.inflight === 0 || | |
| 198 | (this._slow === this.inflight && this.closestReplies.length >= this.k) | |
| 199 | ) { | |
| 200 | // if more than 3/4 failed and we only used cached nodes, try again from the routing table | |
| 201 | if (!this._fromTable && this.successes < this.k / 4) { | |
| 202 | this._addFromTable() | |
| 203 | this._readMore() | |
| 204 | return | |
| 205 | } | |
| 206 | ||
| 207 | this._flush() | |
| 208 | } | |
| 209 | } | |
| 210 | ||
| 211 | _flush() { | |
| 212 | if (this._commiting) return | |
| 213 | this._commiting = true | |
| 214 | ||
| 215 | if (this._commit === null) { | |
| 216 | this.push(null) | |
| 217 | return | |
| 218 | } | |
| 219 | ||
| 220 | const p = [] | |
| 221 | for (const m of this.closestReplies) p.push(this._commit(m, this.dht, this)) | |
| 222 | this._endAfterCommit(p) | |
| 223 | } | |
| 224 | ||
| 225 | _endAfterCommit(ps) { | |
| 226 | if (!ps.length) { | |
| 227 | this.destroy(new Error('Too few nodes responded')) | |
| 228 | return | |
| 229 | } | |
| 230 | ||
| 231 | const self = this | |
| 232 | ||
| 233 | let pending = ps.length | |
| 234 | let success = 0 | |
| 235 | ||
| 236 | for (const p of ps) p.then(ondone, onerror) | |
| 237 | ||
| 238 | function ondone() { | |
| 239 | success++ | |
| 240 | if (--pending === 0) self.push(null) | |
| 241 | } | |
| 242 | ||
| 243 | function onerror(err) { | |
| 244 | if (--pending > 0) return | |
| 245 | if (success) self.push(null) | |
| 246 | else self.destroy(err) | |
| 247 | } | |
| 248 | } | |
| 249 | ||
| 250 | _dec(req) { | |
| 251 | if (req.oncycle === noop) { | |
| 252 | this._slow-- | |
| 253 | } else { | |
| 254 | req.oncycle = noop | |
| 255 | } | |
| 256 | this.inflight-- | |
| 257 | } | |
| 258 | ||
| 259 | _onvisit(m, req) { | |
| 260 | this._dec(req) | |
| 261 | ||
| 262 | const addr = req.to.host + ':' + req.to.port | |
| 263 | this._seen.set(addr, DONE) | |
| 264 | ||
| 265 | if (this._commiting) return | |
| 266 | ||
| 267 | if (m.error === 0) this.successes++ | |
| 268 | else this.errors++ | |
| 269 | ||
| 270 | if (m.error === 0 && m.from.id !== null && this._isCloser(m.from.id)) this._pushClosest(m) | |
| 271 | ||
| 272 | if (m.closerNodes !== null) { | |
| 273 | for (const node of m.closerNodes) { | |
| 274 | node.id = peer.id(node.host, node.port) | |
| 275 | if (this.dht._filterNode !== null && !this.dht._filterNode(node)) continue | |
| 276 | if (b4a.equals(node.id, this.dht.table.id)) continue | |
| 277 | // TODO: we could continue here instead of breaking to ensure that one of the nodes in the closer list | |
| 278 | // is later marked as DOWN that we gossip that back | |
| 279 | if (!this._addPending(node, m.from)) break | |
| 280 | } | |
| 281 | } | |
| 282 | ||
| 283 | if (!this._fromTable && this.successes + this.errors >= this.concurrency) { | |
| 284 | this._slowdown = false | |
| 285 | } | |
| 286 | ||
| 287 | if (m.error !== 0) { | |
| 288 | this._readMore() | |
| 289 | return | |
| 290 | } | |
| 291 | ||
| 292 | const data = this.map(m) | |
| 293 | if (!data || this.push(data) !== false) { | |
| 294 | this._readMore() | |
| 295 | } | |
| 296 | } | |
| 297 | ||
| 298 | _onerror(err, req) { | |
| 299 | const addr = req.to.host + ':' + req.to.port | |
| 300 | const refs = this._seen.get(addr) | |
| 301 | ||
| 302 | if (err.code === 'REQUEST_TIMEOUT') { | |
| 303 | this._seen.set(addr, DOWN) | |
| 304 | for (const node of refs) this._downHint(node, req.to) | |
| 305 | } | |
| 306 | ||
| 307 | this._dec(req) | |
| 308 | this.errors++ | |
| 309 | this._readMore() | |
| 310 | } | |
| 311 | ||
| 312 | _oncycle(req) { | |
| 313 | req.oncycle = noop | |
| 314 | this._slow++ | |
| 315 | this._readMore() | |
| 316 | } | |
| 317 | ||
| 318 | _downHint(node, down) { | |
| 319 | // Check rate limit | |
| 320 | if ( | |
| 321 | this.dht._downHintsRateLimit !== -1 && | |
| 322 | this.dht._downHintsSentPerTick >= this.dht._downHintsRateLimit | |
| 323 | ) { | |
| 324 | return null | |
| 325 | } | |
| 326 | ||
| 327 | this.dht._downHintsSentPerTick++ | |
| 328 | ||
| 329 | const state = { start: 0, end: 6, buffer: b4a.allocUnsafe(6) } | |
| 330 | peer.ipv4.encode(state, down) | |
| 331 | this.dht._request(node, false, true, DOWN_HINT, null, state.buffer, this._session, noop, noop) | |
| 332 | } | |
| 333 | ||
| 334 | _pushClosest(m) { | |
| 335 | this.closestReplies.push(m) | |
| 336 | for (let i = this.closestReplies.length - 2; i >= 0; i--) { | |
| 337 | const prev = this.closestReplies[i] | |
| 338 | const cmp = this._compare(prev.from.id, m.from.id) | |
| 339 | // if sorted, done! | |
| 340 | if (cmp < 0) break | |
| 341 | // if dup, splice it out (rare) | |
| 342 | if (cmp === 0) { | |
| 343 | this.closestReplies.splice(i + 1, 1) | |
| 344 | break | |
| 345 | } | |
| 346 | // swap and continue down | |
| 347 | this.closestReplies[i + 1] = prev | |
| 348 | this.closestReplies[i] = m | |
| 349 | } | |
| 350 | if (this.closestReplies.length > this.k) this.closestReplies.pop() | |
| 351 | } | |
| 352 | ||
| 353 | _compare(a, b) { | |
| 354 | for (let i = 0; i < a.length; i++) { | |
| 355 | if (a[i] === b[i]) continue | |
| 356 | const t = this.target[i] | |
| 357 | return (t ^ a[i]) - (t ^ b[i]) | |
| 358 | } | |
| 359 | return 0 | |
| 360 | } | |
| 361 | ||
| 362 | _visit(to) { | |
| 363 | this.inflight++ | |
| 364 | ||
| 365 | const req = this.dht._request( | |
| 366 | to, | |
| 367 | this.force, | |
| 368 | this.internal, | |
| 369 | this.command, | |
| 370 | this.target, | |
| 371 | this.value, | |
| 372 | this._session, | |
| 373 | this._onvisitbound, | |
| 374 | this._onerrorbound | |
| 375 | ) | |
| 376 | if (req === null) { | |
| 377 | this.destroy(new Error('Node was destroyed')) | |
| 378 | return | |
| 379 | } | |
| 380 | req.retries = this.retries | |
| 381 | req.oncycle = this._oncyclebound | |
| 382 | if (this.force) req.retries = 0 | |
| 383 | } | |
| 384 | ||
| 385 | _destroy(cb) { | |
| 386 | this.dht.stats.queries.active-- | |
| 387 | if (this._autoDestroySession) this._session.destroy() | |
| 388 | cb(null) | |
| 389 | } | |
| 390 | } | |
| 391 | ||
| 392 | function autoCommit(reply, dht, query) { | |
| 393 | if (!reply.token) return Promise.reject(new Error('No token received for closest node')) | |
| 394 | return dht.request( | |
| 395 | { | |
| 396 | token: reply.token, | |
| 397 | target: query.target, | |
| 398 | command: query.command, | |
| 399 | value: query.value | |
| 400 | }, | |
| 401 | reply.from | |
| 402 | ) | |
| 403 | } | |
| 404 | ||
| 405 | function defaultMap(m) { | |
| 406 | return m | |
| 407 | } | |
| 408 | ||
| 409 | function noop() {} |
| … | … | @@ -0,0 +1,47 @@ |
| 1 | module.exports = class Session { | |
| 2 | constructor(dht) { | |
| 3 | this.dht = dht | |
| 4 | this.inflight = [] | |
| 5 | } | |
| 6 | ||
| 7 | _attach(req) { | |
| 8 | req.index = this.inflight.push(req) - 1 | |
| 9 | } | |
| 10 | ||
| 11 | _detach(req) { | |
| 12 | const i = req.index | |
| 13 | if (i === -1) return | |
| 14 | req.index = -1 | |
| 15 | ||
| 16 | if (i === this.inflight.length - 1) this.inflight.pop() | |
| 17 | else { | |
| 18 | const req = (this.inflight[i] = this.inflight.pop()) | |
| 19 | req.index = i | |
| 20 | } | |
| 21 | } | |
| 22 | ||
| 23 | query({ target, command, value }, opts = {}) { | |
| 24 | return this.dht.query({ target, command, value }, { ...opts, session: this }) | |
| 25 | } | |
| 26 | ||
| 27 | request({ token, command, target, value }, { host, port }, opts = {}) { | |
| 28 | return this.dht.request( | |
| 29 | { token, command, target, value }, | |
| 30 | { host, port }, | |
| 31 | { ...opts, session: this } | |
| 32 | ) | |
| 33 | } | |
| 34 | ||
| 35 | ping({ host, port }, opts = {}) { | |
| 36 | return this.dht.ping({ host, port }, { ...opts, session: this }) | |
| 37 | } | |
| 38 | ||
| 39 | destroy(err) { | |
| 40 | while (this.inflight.length) { | |
| 41 | const req = this.inflight[0] | |
| 42 | // prevent destroyed requests from contributing to congestion counts | |
| 43 | this.dht.io.congestion.recv() | |
| 44 | req.destroy(err) | |
| 45 | } | |
| 46 | } | |
| 47 | } |
| … | … | @@ -0,0 +1,54 @@ |
| 1 | { | |
| 2 | "name": "dht-rpc", | |
| 3 | "version": "6.26.3", | |
| 4 | "description": "Make RPC calls over a Kademlia based DHT", | |
| 5 | "main": "index.js", | |
| 6 | "files": [ | |
| 7 | "index.js", | |
| 8 | "lib/*.js" | |
| 9 | ], | |
| 10 | "imports": { | |
| 11 | "events": { | |
| 12 | "bare": "bare-events", | |
| 13 | "default": "events" | |
| 14 | } | |
| 15 | }, | |
| 16 | "dependencies": { | |
| 17 | "adaptive-timeout": "^1.0.1", | |
| 18 | "b4a": "^1.6.1", | |
| 19 | "bare-events": "^2.2.0", | |
| 20 | "compact-encoding": "^2.11.0", | |
| 21 | "compact-encoding-net": "^1.2.0", | |
| 22 | "fast-fifo": "^1.1.0", | |
| 23 | "kademlia-routing-table": "^1.0.1", | |
| 24 | "nat-sampler": "^1.0.1", | |
| 25 | "sodium-universal": "^5.0.0", | |
| 26 | "streamx": "^2.13.2", | |
| 27 | "time-ordered-set": "^2.0.0", | |
| 28 | "udx-native": "^1.5.3" | |
| 29 | }, | |
| 30 | "devDependencies": { | |
| 31 | "brittle": "^3.0.0", | |
| 32 | "lunte": "^1.3.0", | |
| 33 | "prettier": "^3.6.2", | |
| 34 | "prettier-config-holepunch": "^2.0.0", | |
| 35 | "test-suspend": "^1.0.0" | |
| 36 | }, | |
| 37 | "scripts": { | |
| 38 | "format": "prettier --write .", | |
| 39 | "test": "npm run test:node && npm run test:bare", | |
| 40 | "test:bare": "brittle-bare --coverage test.js", | |
| 41 | "lint": "prettier --check . && lunte", | |
| 42 | "test:node": "brittle-node --coverage test.js" | |
| 43 | }, | |
| 44 | "repository": { | |
| 45 | "type": "git", | |
| 46 | "url": "https://github.com/mafintosh/dht-rpc.git" | |
| 47 | }, | |
| 48 | "author": "Mathias Buus (@mafintosh)", | |
| 49 | "license": "MIT", | |
| 50 | "bugs": { | |
| 51 | "url": "https://github.com/mafintosh/dht-rpc/issues" | |
| 52 | }, | |
| 53 | "homepage": "https://github.com/mafintosh/dht-rpc" | |
| 54 | } |
| … | … | @@ -0,0 +1,19 @@ |
| 1 | Copyright (c) 2017 Eric Wendelin and other contributors | |
| 2 | ||
| 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of | |
| 4 | this software and associated documentation files (the "Software"), to deal in | |
| 5 | the Software without restriction, including without limitation the rights to | |
| 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies | |
| 7 | of the Software, and to permit persons to whom the Software is furnished to do | |
| 8 | so, subject to the following conditions: | |
| 9 | ||
| 10 | The above copyright notice and this permission notice shall be included in all | |
| 11 | copies or substantial portions of the Software. | |
| 12 | ||
| 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
| 19 | SOFTWARE. |
| … | … | @@ -0,0 +1,44 @@ |
| 1 | error-stack-parser.js - Extract meaning from JS Errors | |
| 2 | =============== | |
| 3 | [](https://github.com/stacktracejs/error-stack-parser/actions?query=workflow%3AContinuous+Integration+branch%3Amaster) | |
| 4 | [](https://coveralls.io/r/stacktracejs/error-stack-parser?branch=master) | |
| 5 | [](https://opensource.org/licenses/MIT) | |
| 6 | [](https://github.com/stacktracejs/error-stack-parser/releases) | |
| 7 | [](https://github.com/stacktracejs/error-stack-parser/releases) | |
| 8 | [](https://github.com/stacktracejs/error-stack-parser/releases) | |
| 9 | [](http://todogroup.org/opencodeofconduct/#stacktrace.js/me@eriwen.com) | |
| 10 | [](https://www.jsdelivr.com/package/npm/error-stack-parser) | |
| 11 | ||
| 12 | Simple, cross-browser [Error](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) parser. | |
| 13 | This library parses and extracts function names, URLs, line numbers, and column numbers from the given Error's `stack` as | |
| 14 | an Array of [StackFrame](http://git.io/stackframe)s. | |
| 15 | ||
| 16 | Once you have parsed out StackFrames, you can do much more interesting things. See [stacktrace-gps](http://git.io/stacktrace-gps). | |
| 17 | ||
| 18 | Note that in IE9 and earlier, `Error` objects don't have enough information to extract much of anything. In IE 10, `Error`s | |
| 19 | are given a `stack` once they're `throw`n. | |
| 20 | ||
| 21 | ## Browser Support | |
| 22 | [](https://saucelabs.com/u/stacktracejs) | |
| 23 | ||
| 24 | ## Usage | |
| 25 | ```js | |
| 26 | ErrorStackParser.parse(new Error('BOOM')); | |
| 27 | ||
| 28 | => [ | |
| 29 | StackFrame({functionName: 'foo', args: [], fileName: 'path/to/file.js', lineNumber: 35, columnNumber: 79, isNative: false, isEval: false}), | |
| 30 | StackFrame({functionName: 'Bar', fileName: 'https://cdn.somewherefast.com/utils.min.js', lineNumber: 1, columnNumber: 832, isNative: false, isEval: false, isConstructor: true}), | |
| 31 | StackFrame(... and so on ...) | |
| 32 | ] | |
| 33 | ``` | |
| 34 | ||
| 35 | ## Installation | |
| 36 | ```bash | |
| 37 | npm install error-stack-parser | |
| 38 | bower install error-stack-parser | |
| 39 | https://raw.githubusercontent.com/stacktracejs/error-stack-parser/master/dist/error-stack-parser.min.js | |
| 40 | ``` | |
| 41 | ||
| 42 | ## Contributing | |
| 43 | Want to be listed as a *Contributor*? Start with the [Contributing Guide](.github/CONTRIBUTING.md)! | |
| 44 |
| … | … | @@ -0,0 +1,202 @@ |
| 1 | (function(root, factory) { | |
| 2 | 'use strict'; | |
| 3 | // Universal Module Definition (UMD) to support AMD, CommonJS/Node.js, Rhino, and browsers. | |
| 4 | ||
| 5 | /* istanbul ignore next */ | |
| 6 | if (typeof define === 'function' && define.amd) { | |
| 7 | define('error-stack-parser', ['stackframe'], factory); | |
| 8 | } else if (typeof exports === 'object') { | |
| 9 | module.exports = factory(require('stackframe')); | |
| 10 | } else { | |
| 11 | root.ErrorStackParser = factory(root.StackFrame); | |
| 12 | } | |
| 13 | }(this, function ErrorStackParser(StackFrame) { | |
| 14 | 'use strict'; | |
| 15 | ||
| 16 | var FIREFOX_SAFARI_STACK_REGEXP = /(^|@)\S+:\d+/; | |
| 17 | var CHROME_IE_STACK_REGEXP = /^\s*at .*(\S+:\d+|\(native\))/m; | |
| 18 | var SAFARI_NATIVE_CODE_REGEXP = /^(eval@)?(\[native code])?$/; | |
| 19 | ||
| 20 | return { | |
| 21 | /** | |
| 22 | * Given an Error object, extract the most information from it. | |
| 23 | * | |
| 24 | * @param {Error} error object | |
| 25 | * @return {Array} of StackFrames | |
| 26 | */ | |
| 27 | parse: function ErrorStackParser$$parse(error) { | |
| 28 | if (typeof error.stacktrace !== 'undefined' || typeof error['opera#sourceloc'] !== 'undefined') { | |
| 29 | return this.parseOpera(error); | |
| 30 | } else if (error.stack && error.stack.match(CHROME_IE_STACK_REGEXP)) { | |
| 31 | return this.parseV8OrIE(error); | |
| 32 | } else if (error.stack) { | |
| 33 | return this.parseFFOrSafari(error); | |
| 34 | } else { | |
| 35 | throw new Error('Cannot parse given Error object'); | |
| 36 | } | |
| 37 | }, | |
| 38 | ||
| 39 | // Separate line and column numbers from a string of the form: (URI:Line:Column) | |
| 40 | extractLocation: function ErrorStackParser$$extractLocation(urlLike) { | |
| 41 | // Fail-fast but return locations like "(native)" | |
| 42 | if (urlLike.indexOf(':') === -1) { | |
| 43 | return [urlLike]; | |
| 44 | } | |
| 45 | ||
| 46 | var regExp = /(.+?)(?::(\d+))?(?::(\d+))?$/; | |
| 47 | var parts = regExp.exec(urlLike.replace(/[()]/g, '')); | |
| 48 | return [parts[1], parts[2] || undefined, parts[3] || undefined]; | |
| 49 | }, | |
| 50 | ||
| 51 | parseV8OrIE: function ErrorStackParser$$parseV8OrIE(error) { | |
| 52 | var filtered = error.stack.split('\n').filter(function(line) { | |
| 53 | return !!line.match(CHROME_IE_STACK_REGEXP); | |
| 54 | }, this); | |
| 55 | ||
| 56 | return filtered.map(function(line) { | |
| 57 | if (line.indexOf('(eval ') > -1) { | |
| 58 | // Throw away eval information until we implement stacktrace.js/stackframe#8 | |
| 59 | line = line.replace(/eval code/g, 'eval').replace(/(\(eval at [^()]*)|(,.*$)/g, ''); | |
| 60 | } | |
| 61 | var sanitizedLine = line.replace(/^\s+/, '').replace(/\(eval code/g, '(').replace(/^.*?\s+/, ''); | |
| 62 | ||
| 63 | // capture and preseve the parenthesized location "(/foo/my bar.js:12:87)" in | |
| 64 | // case it has spaces in it, as the string is split on \s+ later on | |
| 65 | var location = sanitizedLine.match(/ (\(.+\)$)/); | |
| 66 | ||
| 67 | // remove the parenthesized location from the line, if it was matched | |
| 68 | sanitizedLine = location ? sanitizedLine.replace(location[0], '') : sanitizedLine; | |
| 69 | ||
| 70 | // if a location was matched, pass it to extractLocation() otherwise pass all sanitizedLine | |
| 71 | // because this line doesn't have function name | |
| 72 | var locationParts = this.extractLocation(location ? location[1] : sanitizedLine); | |
| 73 | var functionName = location && sanitizedLine || undefined; | |
| 74 | var fileName = ['eval', '<anonymous>'].indexOf(locationParts[0]) > -1 ? undefined : locationParts[0]; | |
| 75 | ||
| 76 | return new StackFrame({ | |
| 77 | functionName: functionName, | |
| 78 | fileName: fileName, | |
| 79 | lineNumber: locationParts[1], | |
| 80 | columnNumber: locationParts[2], | |
| 81 | source: line | |
| 82 | }); | |
| 83 | }, this); | |
| 84 | }, | |
| 85 | ||
| 86 | parseFFOrSafari: function ErrorStackParser$$parseFFOrSafari(error) { | |
| 87 | var filtered = error.stack.split('\n').filter(function(line) { | |
| 88 | return !line.match(SAFARI_NATIVE_CODE_REGEXP); | |
| 89 | }, this); | |
| 90 | ||
| 91 | return filtered.map(function(line) { | |
| 92 | // Throw away eval information until we implement stacktrace.js/stackframe#8 | |
| 93 | if (line.indexOf(' > eval') > -1) { | |
| 94 | line = line.replace(/ line (\d+)(?: > eval line \d+)* > eval:\d+:\d+/g, ':$1'); | |
| 95 | } | |
| 96 | ||
| 97 | if (line.indexOf('@') === -1 && line.indexOf(':') === -1) { | |
| 98 | // Safari eval frames only have function names and nothing else | |
| 99 | return new StackFrame({ | |
| 100 | functionName: line | |
| 101 | }); | |
| 102 | } else { | |
| 103 | var functionNameRegex = /((.*".+"[^@]*)?[^@]*)(?:@)/; | |
| 104 | var matches = line.match(functionNameRegex); | |
| 105 | var functionName = matches && matches[1] ? matches[1] : undefined; | |
| 106 | var locationParts = this.extractLocation(line.replace(functionNameRegex, '')); | |
| 107 | ||
| 108 | return new StackFrame({ | |
| 109 | functionName: functionName, | |
| 110 | fileName: locationParts[0], | |
| 111 | lineNumber: locationParts[1], | |
| 112 | columnNumber: locationParts[2], | |
| 113 | source: line | |
| 114 | }); | |
| 115 | } | |
| 116 | }, this); | |
| 117 | }, | |
| 118 | ||
| 119 | parseOpera: function ErrorStackParser$$parseOpera(e) { | |
| 120 | if (!e.stacktrace || (e.message.indexOf('\n') > -1 && | |
| 121 | e.message.split('\n').length > e.stacktrace.split('\n').length)) { | |
| 122 | return this.parseOpera9(e); | |
| 123 | } else if (!e.stack) { | |
| 124 | return this.parseOpera10(e); | |
| 125 | } else { | |
| 126 | return this.parseOpera11(e); | |
| 127 | } | |
| 128 | }, | |
| 129 | ||
| 130 | parseOpera9: function ErrorStackParser$$parseOpera9(e) { | |
| 131 | var lineRE = /Line (\d+).*script (?:in )?(\S+)/i; | |
| 132 | var lines = e.message.split('\n'); | |
| 133 | var result = []; | |
| 134 | ||
| 135 | for (var i = 2, len = lines.length; i < len; i += 2) { | |
| 136 | var match = lineRE.exec(lines[i]); | |
| 137 | if (match) { | |
| 138 | result.push(new StackFrame({ | |
| 139 | fileName: match[2], | |
| 140 | lineNumber: match[1], | |
| 141 | source: lines[i] | |
| 142 | })); | |
| 143 | } | |
| 144 | } | |
| 145 | ||
| 146 | return result; | |
| 147 | }, | |
| 148 | ||
| 149 | parseOpera10: function ErrorStackParser$$parseOpera10(e) { | |
| 150 | var lineRE = /Line (\d+).*script (?:in )?(\S+)(?:: In function (\S+))?$/i; | |
| 151 | var lines = e.stacktrace.split('\n'); | |
| 152 | var result = []; | |
| 153 | ||
| 154 | for (var i = 0, len = lines.length; i < len; i += 2) { | |
| 155 | var match = lineRE.exec(lines[i]); | |
| 156 | if (match) { | |
| 157 | result.push( | |
| 158 | new StackFrame({ | |
| 159 | functionName: match[3] || undefined, | |
| 160 | fileName: match[2], | |
| 161 | lineNumber: match[1], | |
| 162 | source: lines[i] | |
| 163 | }) | |
| 164 | ); | |
| 165 | } | |
| 166 | } | |
| 167 | ||
| 168 | return result; | |
| 169 | }, | |
| 170 | ||
| 171 | // Opera 10.65+ Error.stack very similar to FF/Safari | |
| 172 | parseOpera11: function ErrorStackParser$$parseOpera11(error) { | |
| 173 | var filtered = error.stack.split('\n').filter(function(line) { | |
| 174 | return !!line.match(FIREFOX_SAFARI_STACK_REGEXP) && !line.match(/^Error created at/); | |
| 175 | }, this); | |
| 176 | ||
| 177 | return filtered.map(function(line) { | |
| 178 | var tokens = line.split('@'); | |
| 179 | var locationParts = this.extractLocation(tokens.pop()); | |
| 180 | var functionCall = (tokens.shift() || ''); | |
| 181 | var functionName = functionCall | |
| 182 | .replace(/<anonymous function(: (\w+))?>/, '$2') | |
| 183 | .replace(/\([^)]*\)/g, '') || undefined; | |
| 184 | var argsRaw; | |
| 185 | if (functionCall.match(/\(([^)]*)\)/)) { | |
| 186 | argsRaw = functionCall.replace(/^[^(]+\(([^)]*)\)$/, '$1'); | |
| 187 | } | |
| 188 | var args = (argsRaw === undefined || argsRaw === '[arguments not available]') ? | |
| 189 | undefined : argsRaw.split(','); | |
| 190 | ||
| 191 | return new StackFrame({ | |
| 192 | functionName: functionName, | |
| 193 | args: args, | |
| 194 | fileName: locationParts[0], | |
| 195 | lineNumber: locationParts[1], | |
| 196 | columnNumber: locationParts[2], | |
| 197 | source: line | |
| 198 | }); | |
| 199 | }, this); | |
| 200 | } | |
| 201 | }; | |
| 202 | })); |
| … | … | @@ -0,0 +1,2 @@ |
| 1 | !function(e,t){"use strict";"function"==typeof define&&define.amd?define("stackframe",[],t):"object"==typeof exports?module.exports=t():e.StackFrame=t()}(this,function(){"use strict";function e(e){return e.charAt(0).toUpperCase()+e.substring(1)}function t(e){return function(){return this[e]}}var r=["isConstructor","isEval","isNative","isToplevel"],n=["columnNumber","lineNumber"],i=["fileName","functionName","source"],a=r.concat(n,i,["args"],["evalOrigin"]);function o(t){if(t)for(var r=0;r<a.length;r++)void 0!==t[a[r]]&&this["set"+e(a[r])](t[a[r]])}o.prototype={getArgs:function(){return this.args},setArgs:function(e){if("[object Array]"!==Object.prototype.toString.call(e))throw new TypeError("Args must be an Array");this.args=e},getEvalOrigin:function(){return this.evalOrigin},setEvalOrigin:function(e){if(e instanceof o)this.evalOrigin=e;else{if(!(e instanceof Object))throw new TypeError("Eval Origin must be an Object or StackFrame");this.evalOrigin=new o(e)}},toString:function(){var e=this.getFileName()||"",t=this.getLineNumber()||"",r=this.getColumnNumber()||"",n=this.getFunctionName()||"";return this.getIsEval()?e?"[eval] ("+e+":"+t+":"+r+")":"[eval]:"+t+":"+r:n?n+" ("+e+":"+t+":"+r+")":e+":"+t+":"+r}},o.fromString=function(e){var t=e.indexOf("("),r=e.lastIndexOf(")"),n=e.substring(0,t),i=e.substring(t+1,r).split(","),a=e.substring(r+1);if(0===a.indexOf("@"))var s=/@(.+?)(?::(\d+))?(?::(\d+))?$/.exec(a,""),c=s[1],u=s[2],f=s[3];return new o({functionName:n,args:i||void 0,fileName:c,lineNumber:u||void 0,columnNumber:f||void 0})};for(var s=0;s<r.length;s++)o.prototype["get"+e(r[s])]=t(r[s]),o.prototype["set"+e(r[s])]=function(e){return function(t){this[e]=Boolean(t)}}(r[s]);for(var c=0;c<n.length;c++)o.prototype["get"+e(n[c])]=t(n[c]),o.prototype["set"+e(n[c])]=function(e){return function(t){if(r=t,isNaN(parseFloat(r))||!isFinite(r))throw new TypeError(e+" must be a Number");var r;this[e]=Number(t)}}(n[c]);for(var u=0;u<i.length;u++)o.prototype["get"+e(i[u])]=t(i[u]),o.prototype["set"+e(i[u])]=function(e){return function(t){this[e]=String(t)}}(i[u]);return o}),function(e,t){"use strict";"function"==typeof define&&define.amd?define("error-stack-parser",["stackframe"],t):"object"==typeof exports?module.exports=t(require("stackframe")):e.ErrorStackParser=t(e.StackFrame)}(this,function(e){"use strict";var t=/(^|@)\S+:\d+/,r=/^\s*at .*(\S+:\d+|\(native\))/m,n=/^(eval@)?(\[native code])?$/;return{parse:function(e){if(void 0!==e.stacktrace||void 0!==e["opera#sourceloc"])return this.parseOpera(e);if(e.stack&&e.stack.match(r))return this.parseV8OrIE(e);if(e.stack)return this.parseFFOrSafari(e);throw new Error("Cannot parse given Error object")},extractLocation:function(e){if(-1===e.indexOf(":"))return[e];var t=/(.+?)(?::(\d+))?(?::(\d+))?$/.exec(e.replace(/[()]/g,""));return[t[1],t[2]||void 0,t[3]||void 0]},parseV8OrIE:function(t){return t.stack.split("\n").filter(function(e){return!!e.match(r)},this).map(function(t){t.indexOf("(eval ")>-1&&(t=t.replace(/eval code/g,"eval").replace(/(\(eval at [^()]*)|(,.*$)/g,""));var r=t.replace(/^\s+/,"").replace(/\(eval code/g,"(").replace(/^.*?\s+/,""),n=r.match(/ (\(.+\)$)/);r=n?r.replace(n[0],""):r;var i=this.extractLocation(n?n[1]:r),a=n&&r||void 0,o=["eval","<anonymous>"].indexOf(i[0])>-1?void 0:i[0];return new e({functionName:a,fileName:o,lineNumber:i[1],columnNumber:i[2],source:t})},this)},parseFFOrSafari:function(t){return t.stack.split("\n").filter(function(e){return!e.match(n)},this).map(function(t){if(t.indexOf(" > eval")>-1&&(t=t.replace(/ line (\d+)(?: > eval line \d+)* > eval:\d+:\d+/g,":$1")),-1===t.indexOf("@")&&-1===t.indexOf(":"))return new e({functionName:t});var r=/((.*".+"[^@]*)?[^@]*)(?:@)/,n=t.match(r),i=n&&n[1]?n[1]:void 0,a=this.extractLocation(t.replace(r,""));return new e({functionName:i,fileName:a[0],lineNumber:a[1],columnNumber:a[2],source:t})},this)},parseOpera:function(e){return!e.stacktrace||e.message.indexOf("\n")>-1&&e.message.split("\n").length>e.stacktrace.split("\n").length?this.parseOpera9(e):e.stack?this.parseOpera11(e):this.parseOpera10(e)},parseOpera9:function(t){for(var r=/Line (\d+).*script (?:in )?(\S+)/i,n=t.message.split("\n"),i=[],a=2,o=n.length;a<o;a+=2){var s=r.exec(n[a]);s&&i.push(new e({fileName:s[2],lineNumber:s[1],source:n[a]}))}return i},parseOpera10:function(t){for(var r=/Line (\d+).*script (?:in )?(\S+)(?:: In function (\S+))?$/i,n=t.stacktrace.split("\n"),i=[],a=0,o=n.length;a<o;a+=2){var s=r.exec(n[a]);s&&i.push(new e({functionName:s[3]||void 0,fileName:s[2],lineNumber:s[1],source:n[a]}))}return i},parseOpera11:function(r){return r.stack.split("\n").filter(function(e){return!!e.match(t)&&!e.match(/^Error created at/)},this).map(function(t){var r,n=t.split("@"),i=this.extractLocation(n.pop()),a=n.shift()||"",o=a.replace(/<anonymous function(: (\w+))?>/,"$2").replace(/\([^)]*\)/g,"")||void 0;a.match(/\(([^)]*)\)/)&&(r=a.replace(/^[^(]+\(([^)]*)\)$/,"$1"));var s=void 0===r||"[arguments not available]"===r?void 0:r.split(",");return new e({functionName:o,args:s,fileName:i[0],lineNumber:i[1],columnNumber:i[2],source:t})},this)}}}); | |
| 2 | //# sourceMappingURL=error-stack-parser.min.js.map |
| … | … | @@ -0,0 +1 @@ |
| 1 | {"version":3,"sources":["node_modules/stackframe/stackframe.js","error-stack-parser.js"],"names":["root","factory","define","amd","exports","module","StackFrame","this","_capitalize","str","charAt","toUpperCase","substring","_getter","p","booleanProps","numericProps","stringProps","props","concat","obj","i","length","undefined","prototype","getArgs","args","setArgs","v","Object","toString","call","TypeError","getEvalOrigin","evalOrigin","setEvalOrigin","fileName","getFileName","lineNumber","getLineNumber","columnNumber","getColumnNumber","functionName","getFunctionName","getIsEval","fromString","argsStartIndex","indexOf","argsEndIndex","lastIndexOf","split","locationString","parts","exec","Boolean","j","n","isNaN","parseFloat","isFinite","Number","k","String","require","ErrorStackParser","FIREFOX_SAFARI_STACK_REGEXP","CHROME_IE_STACK_REGEXP","SAFARI_NATIVE_CODE_REGEXP","parse","error","stacktrace","parseOpera","stack","match","parseV8OrIE","parseFFOrSafari","Error","extractLocation","urlLike","replace","filter","line","map","sanitizedLine","location","locationParts","source","functionNameRegex","matches","e","message","parseOpera9","parseOpera11","parseOpera10","lineRE","lines","result","len","push","argsRaw","tokens","pop","functionCall","shift"],"mappings":"CAAC,SAASA,EAAMC,GACZ,aAIsB,mBAAXC,QAAyBA,OAAOC,IACvCD,OAAO,gBAAkBD,GACC,iBAAZG,QACdC,OAAOD,QAAUH,IAEjBD,EAAKM,WAAaL,IAV1B,CAYEM,KAAM,WACJ,aAKA,SAASC,EAAYC,GACjB,OAAOA,EAAIC,OAAO,GAAGC,cAAgBF,EAAIG,UAAU,GAGvD,SAASC,EAAQC,GACb,OAAO,WACH,OAAOP,KAAKO,IAIpB,IAAIC,GAAgB,gBAAiB,SAAU,WAAY,cACvDC,GAAgB,eAAgB,cAChCC,GAAe,WAAY,eAAgB,UAI3CC,EAAQH,EAAaI,OAAOH,EAAcC,GAH5B,SACC,eAInB,SAASX,EAAWc,GAChB,GAAKA,EACL,IAAK,IAAIC,EAAI,EAAGA,EAAIH,EAAMI,OAAQD,SACRE,IAAlBH,EAAIF,EAAMG,KACVd,KAAK,MAAQC,EAAYU,EAAMG,KAAKD,EAAIF,EAAMG,KAK1Df,EAAWkB,WACPC,QAAS,WACL,OAAOlB,KAAKmB,MAEhBC,QAAS,SAASC,GACd,GAA0C,mBAAtCC,OAAOL,UAAUM,SAASC,KAAKH,GAC/B,MAAM,IAAII,UAAU,yBAExBzB,KAAKmB,KAAOE,GAGhBK,cAAe,WACX,OAAO1B,KAAK2B,YAEhBC,cAAe,SAASP,GACpB,GAAIA,aAAatB,EACbC,KAAK2B,WAAaN,MACf,CAAA,KAAIA,aAAaC,QAGpB,MAAM,IAAIG,UAAU,+CAFpBzB,KAAK2B,WAAa,IAAI5B,EAAWsB,KAMzCE,SAAU,WACN,IAAIM,EAAW7B,KAAK8B,eAAiB,GACjCC,EAAa/B,KAAKgC,iBAAmB,GACrCC,EAAejC,KAAKkC,mBAAqB,GACzCC,EAAenC,KAAKoC,mBAAqB,GAC7C,OAAIpC,KAAKqC,YACDR,EACO,WAAaA,EAAW,IAAME,EAAa,IAAME,EAAe,IAEpE,UAAYF,EAAa,IAAME,EAEtCE,EACOA,EAAe,KAAON,EAAW,IAAME,EAAa,IAAME,EAAe,IAE7EJ,EAAW,IAAME,EAAa,IAAME,IAInDlC,EAAWuC,WAAa,SAAgCpC,GACpD,IAAIqC,EAAiBrC,EAAIsC,QAAQ,KAC7BC,EAAevC,EAAIwC,YAAY,KAE/BP,EAAejC,EAAIG,UAAU,EAAGkC,GAChCpB,EAAOjB,EAAIG,UAAUkC,EAAiB,EAAGE,GAAcE,MAAM,KAC7DC,EAAiB1C,EAAIG,UAAUoC,EAAe,GAElD,GAAoC,IAAhCG,EAAeJ,QAAQ,KACvB,IAAIK,EAAQ,gCAAgCC,KAAKF,EAAgB,IAC7Df,EAAWgB,EAAM,GACjBd,EAAac,EAAM,GACnBZ,EAAeY,EAAM,GAG7B,OAAO,IAAI9C,GACPoC,aAAcA,EACdhB,KAAMA,QAAQH,EACda,SAAUA,EACVE,WAAYA,QAAcf,EAC1BiB,aAAcA,QAAgBjB,KAItC,IAAK,IAAIF,EAAI,EAAGA,EAAIN,EAAaO,OAAQD,IACrCf,EAAWkB,UAAU,MAAQhB,EAAYO,EAAaM,KAAOR,EAAQE,EAAaM,IAClFf,EAAWkB,UAAU,MAAQhB,EAAYO,EAAaM,KAAO,SAAUP,GACnE,OAAO,SAASc,GACZrB,KAAKO,GAAKwC,QAAQ1B,IAFmC,CAI1Db,EAAaM,IAGpB,IAAK,IAAIkC,EAAI,EAAGA,EAAIvC,EAAaM,OAAQiC,IACrCjD,EAAWkB,UAAU,MAAQhB,EAAYQ,EAAauC,KAAO1C,EAAQG,EAAauC,IAClFjD,EAAWkB,UAAU,MAAQhB,EAAYQ,EAAauC,KAAO,SAAUzC,GACnE,OAAO,SAASc,GACZ,GA9GO4B,EA8GQ5B,EA7Gf6B,MAAMC,WAAWF,MAAOG,SAASH,GA8G7B,MAAM,IAAIxB,UAAUlB,EAAI,qBA/GxC,IAAmB0C,EAiHPjD,KAAKO,GAAK8C,OAAOhC,IALoC,CAO1DZ,EAAauC,IAGpB,IAAK,IAAIM,EAAI,EAAGA,EAAI5C,EAAYK,OAAQuC,IACpCvD,EAAWkB,UAAU,MAAQhB,EAAYS,EAAY4C,KAAOhD,EAAQI,EAAY4C,IAChFvD,EAAWkB,UAAU,MAAQhB,EAAYS,EAAY4C,KAAO,SAAU/C,GAClE,OAAO,SAASc,GACZrB,KAAKO,GAAKgD,OAAOlC,IAFmC,CAIzDX,EAAY4C,IAGnB,OAAOvD,IC7IV,SAASN,EAAMC,GACZ,aAIsB,mBAAXC,QAAyBA,OAAOC,IACvCD,OAAO,sBAAuB,cAAeD,GACnB,iBAAZG,QACdC,OAAOD,QAAUH,EAAQ8D,QAAQ,eAEjC/D,EAAKgE,iBAAmB/D,EAAQD,EAAKM,YAV7C,CAYEC,KAAM,SAA0BD,GAC9B,aAEA,IAAI2D,EAA8B,eAC9BC,EAAyB,iCACzBC,EAA4B,8BAEhC,OAOIC,MAAO,SAAiCC,GACpC,QAAgC,IAArBA,EAAMC,iBAAkE,IAA7BD,EAAM,mBACxD,OAAO9D,KAAKgE,WAAWF,GACpB,GAAIA,EAAMG,OAASH,EAAMG,MAAMC,MAAMP,GACxC,OAAO3D,KAAKmE,YAAYL,GACrB,GAAIA,EAAMG,MACb,OAAOjE,KAAKoE,gBAAgBN,GAE5B,MAAM,IAAIO,MAAM,oCAKxBC,gBAAiB,SAA2CC,GAExD,IAA8B,IAA1BA,EAAQ/B,QAAQ,KAChB,OAAQ+B,GAGZ,IACI1B,EADS,+BACMC,KAAKyB,EAAQC,QAAQ,QAAS,KACjD,OAAQ3B,EAAM,GAAIA,EAAM,SAAM7B,EAAW6B,EAAM,SAAM7B,IAGzDmD,YAAa,SAAuCL,GAKhD,OAJeA,EAAMG,MAAMtB,MAAM,MAAM8B,OAAO,SAASC,GACnD,QAASA,EAAKR,MAAMP,IACrB3D,MAEa2E,IAAI,SAASD,GACrBA,EAAKlC,QAAQ,WAAa,IAE1BkC,EAAOA,EAAKF,QAAQ,aAAc,QAAQA,QAAQ,6BAA8B,KAEpF,IAAII,EAAgBF,EAAKF,QAAQ,OAAQ,IAAIA,QAAQ,eAAgB,KAAKA,QAAQ,UAAW,IAIzFK,EAAWD,EAAcV,MAAM,cAGnCU,EAAgBC,EAAWD,EAAcJ,QAAQK,EAAS,GAAI,IAAMD,EAIpE,IAAIE,EAAgB9E,KAAKsE,gBAAgBO,EAAWA,EAAS,GAAKD,GAC9DzC,EAAe0C,GAAYD,QAAiB5D,EAC5Ca,GAAY,OAAQ,eAAeW,QAAQsC,EAAc,KAAO,OAAI9D,EAAY8D,EAAc,GAElG,OAAO,IAAI/E,GACPoC,aAAcA,EACdN,SAAUA,EACVE,WAAY+C,EAAc,GAC1B7C,aAAc6C,EAAc,GAC5BC,OAAQL,KAEb1E,OAGPoE,gBAAiB,SAA2CN,GAKxD,OAJeA,EAAMG,MAAMtB,MAAM,MAAM8B,OAAO,SAASC,GACnD,OAAQA,EAAKR,MAAMN,IACpB5D,MAEa2E,IAAI,SAASD,GAMzB,GAJIA,EAAKlC,QAAQ,YAAc,IAC3BkC,EAAOA,EAAKF,QAAQ,mDAAoD,SAGjD,IAAvBE,EAAKlC,QAAQ,OAAsC,IAAvBkC,EAAKlC,QAAQ,KAEzC,OAAO,IAAIzC,GACPoC,aAAcuC,IAGlB,IAAIM,EAAoB,6BACpBC,EAAUP,EAAKR,MAAMc,GACrB7C,EAAe8C,GAAWA,EAAQ,GAAKA,EAAQ,QAAKjE,EACpD8D,EAAgB9E,KAAKsE,gBAAgBI,EAAKF,QAAQQ,EAAmB,KAEzE,OAAO,IAAIjF,GACPoC,aAAcA,EACdN,SAAUiD,EAAc,GACxB/C,WAAY+C,EAAc,GAC1B7C,aAAc6C,EAAc,GAC5BC,OAAQL,KAGjB1E,OAGPgE,WAAY,SAAsCkB,GAC9C,OAAKA,EAAEnB,YAAemB,EAAEC,QAAQ3C,QAAQ,OAAS,GAC7C0C,EAAEC,QAAQxC,MAAM,MAAM5B,OAASmE,EAAEnB,WAAWpB,MAAM,MAAM5B,OACjDf,KAAKoF,YAAYF,GAChBA,EAAEjB,MAGHjE,KAAKqF,aAAaH,GAFlBlF,KAAKsF,aAAaJ,IAMjCE,YAAa,SAAuCF,GAKhD,IAJA,IAAIK,EAAS,oCACTC,EAAQN,EAAEC,QAAQxC,MAAM,MACxB8C,KAEK3E,EAAI,EAAG4E,EAAMF,EAAMzE,OAAQD,EAAI4E,EAAK5E,GAAK,EAAG,CACjD,IAAIoD,EAAQqB,EAAOzC,KAAK0C,EAAM1E,IAC1BoD,GACAuB,EAAOE,KAAK,IAAI5F,GACZ8B,SAAUqC,EAAM,GAChBnC,WAAYmC,EAAM,GAClBa,OAAQS,EAAM1E,MAK1B,OAAO2E,GAGXH,aAAc,SAAwCJ,GAKlD,IAJA,IAAIK,EAAS,6DACTC,EAAQN,EAAEnB,WAAWpB,MAAM,MAC3B8C,KAEK3E,EAAI,EAAG4E,EAAMF,EAAMzE,OAAQD,EAAI4E,EAAK5E,GAAK,EAAG,CACjD,IAAIoD,EAAQqB,EAAOzC,KAAK0C,EAAM1E,IAC1BoD,GACAuB,EAAOE,KACH,IAAI5F,GACAoC,aAAc+B,EAAM,SAAMlD,EAC1Ba,SAAUqC,EAAM,GAChBnC,WAAYmC,EAAM,GAClBa,OAAQS,EAAM1E,MAM9B,OAAO2E,GAIXJ,aAAc,SAAwCvB,GAKlD,OAJeA,EAAMG,MAAMtB,MAAM,MAAM8B,OAAO,SAASC,GACnD,QAASA,EAAKR,MAAMR,KAAiCgB,EAAKR,MAAM,sBACjElE,MAEa2E,IAAI,SAASD,GACzB,IAMIkB,EANAC,EAASnB,EAAK/B,MAAM,KACpBmC,EAAgB9E,KAAKsE,gBAAgBuB,EAAOC,OAC5CC,EAAgBF,EAAOG,SAAW,GAClC7D,EAAe4D,EACdvB,QAAQ,iCAAkC,MAC1CA,QAAQ,aAAc,UAAOxD,EAE9B+E,EAAa7B,MAAM,iBACnB0B,EAAUG,EAAavB,QAAQ,qBAAsB,OAEzD,IAAIrD,OAAoBH,IAAZ4E,GAAqC,8BAAZA,OACjC5E,EAAY4E,EAAQjD,MAAM,KAE9B,OAAO,IAAI5C,GACPoC,aAAcA,EACdhB,KAAMA,EACNU,SAAUiD,EAAc,GACxB/C,WAAY+C,EAAc,GAC1B7C,aAAc6C,EAAc,GAC5BC,OAAQL,KAEb1E"} |
| … | … | @@ -0,0 +1,19 @@ |
| 1 | // Type definitions for ErrorStackParser v2.1.0 | |
| 2 | // Project: https://github.com/stacktracejs/error-stack-parser | |
| 3 | // Definitions by: Eric Wendelin <https://www.eriwen.com> | |
| 4 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped | |
| 5 | ||
| 6 | import StackFrame = require("stackframe"); | |
| 7 | ||
| 8 | declare namespace ErrorStackParser { | |
| 9 | export type {StackFrame}; | |
| 10 | /** | |
| 11 | * Given an Error object, extract the most information from it. | |
| 12 | * | |
| 13 | * @param {Error} error object | |
| 14 | * @return {Array} of StackFrames | |
| 15 | */ | |
| 16 | export function parse(error: Error): StackFrame[]; | |
| 17 | } | |
| 18 | ||
| 19 | export = ErrorStackParser; |
| … | … | @@ -0,0 +1,202 @@ |
| 1 | (function(root, factory) { | |
| 2 | 'use strict'; | |
| 3 | // Universal Module Definition (UMD) to support AMD, CommonJS/Node.js, Rhino, and browsers. | |
| 4 | ||
| 5 | /* istanbul ignore next */ | |
| 6 | if (typeof define === 'function' && define.amd) { | |
| 7 | define('error-stack-parser', ['stackframe'], factory); | |
| 8 | } else if (typeof exports === 'object') { | |
| 9 | module.exports = factory(require('stackframe')); | |
| 10 | } else { | |
| 11 | root.ErrorStackParser = factory(root.StackFrame); | |
| 12 | } | |
| 13 | }(this, function ErrorStackParser(StackFrame) { | |
| 14 | 'use strict'; | |
| 15 | ||
| 16 | var FIREFOX_SAFARI_STACK_REGEXP = /(^|@)\S+:\d+/; | |
| 17 | var CHROME_IE_STACK_REGEXP = /^\s*at .*(\S+:\d+|\(native\))/m; | |
| 18 | var SAFARI_NATIVE_CODE_REGEXP = /^(eval@)?(\[native code])?$/; | |
| 19 | ||
| 20 | return { | |
| 21 | /** | |
| 22 | * Given an Error object, extract the most information from it. | |
| 23 | * | |
| 24 | * @param {Error} error object | |
| 25 | * @return {Array} of StackFrames | |
| 26 | */ | |
| 27 | parse: function ErrorStackParser$$parse(error) { | |
| 28 | if (typeof error.stacktrace !== 'undefined' || typeof error['opera#sourceloc'] !== 'undefined') { | |
| 29 | return this.parseOpera(error); | |
| 30 | } else if (error.stack && error.stack.match(CHROME_IE_STACK_REGEXP)) { | |
| 31 | return this.parseV8OrIE(error); | |
| 32 | } else if (error.stack) { | |
| 33 | return this.parseFFOrSafari(error); | |
| 34 | } else { | |
| 35 | throw new Error('Cannot parse given Error object'); | |
| 36 | } | |
| 37 | }, | |
| 38 | ||
| 39 | // Separate line and column numbers from a string of the form: (URI:Line:Column) | |
| 40 | extractLocation: function ErrorStackParser$$extractLocation(urlLike) { | |
| 41 | // Fail-fast but return locations like "(native)" | |
| 42 | if (urlLike.indexOf(':') === -1) { | |
| 43 | return [urlLike]; | |
| 44 | } | |
| 45 | ||
| 46 | var regExp = /(.+?)(?::(\d+))?(?::(\d+))?$/; | |
| 47 | var parts = regExp.exec(urlLike.replace(/[()]/g, '')); | |
| 48 | return [parts[1], parts[2] || undefined, parts[3] || undefined]; | |
| 49 | }, | |
| 50 | ||
| 51 | parseV8OrIE: function ErrorStackParser$$parseV8OrIE(error) { | |
| 52 | var filtered = error.stack.split('\n').filter(function(line) { | |
| 53 | return !!line.match(CHROME_IE_STACK_REGEXP); | |
| 54 | }, this); | |
| 55 | ||
| 56 | return filtered.map(function(line) { | |
| 57 | if (line.indexOf('(eval ') > -1) { | |
| 58 | // Throw away eval information until we implement stacktrace.js/stackframe#8 | |
| 59 | line = line.replace(/eval code/g, 'eval').replace(/(\(eval at [^()]*)|(,.*$)/g, ''); | |
| 60 | } | |
| 61 | var sanitizedLine = line.replace(/^\s+/, '').replace(/\(eval code/g, '(').replace(/^.*?\s+/, ''); | |
| 62 | ||
| 63 | // capture and preseve the parenthesized location "(/foo/my bar.js:12:87)" in | |
| 64 | // case it has spaces in it, as the string is split on \s+ later on | |
| 65 | var location = sanitizedLine.match(/ (\(.+\)$)/); | |
| 66 | ||
| 67 | // remove the parenthesized location from the line, if it was matched | |
| 68 | sanitizedLine = location ? sanitizedLine.replace(location[0], '') : sanitizedLine; | |
| 69 | ||
| 70 | // if a location was matched, pass it to extractLocation() otherwise pass all sanitizedLine | |
| 71 | // because this line doesn't have function name | |
| 72 | var locationParts = this.extractLocation(location ? location[1] : sanitizedLine); | |
| 73 | var functionName = location && sanitizedLine || undefined; | |
| 74 | var fileName = ['eval', '<anonymous>'].indexOf(locationParts[0]) > -1 ? undefined : locationParts[0]; | |
| 75 | ||
| 76 | return new StackFrame({ | |
| 77 | functionName: functionName, | |
| 78 | fileName: fileName, | |
| 79 | lineNumber: locationParts[1], | |
| 80 | columnNumber: locationParts[2], | |
| 81 | source: line | |
| 82 | }); | |
| 83 | }, this); | |
| 84 | }, | |
| 85 | ||
| 86 | parseFFOrSafari: function ErrorStackParser$$parseFFOrSafari(error) { | |
| 87 | var filtered = error.stack.split('\n').filter(function(line) { | |
| 88 | return !line.match(SAFARI_NATIVE_CODE_REGEXP); | |
| 89 | }, this); | |
| 90 | ||
| 91 | return filtered.map(function(line) { | |
| 92 | // Throw away eval information until we implement stacktrace.js/stackframe#8 | |
| 93 | if (line.indexOf(' > eval') > -1) { | |
| 94 | line = line.replace(/ line (\d+)(?: > eval line \d+)* > eval:\d+:\d+/g, ':$1'); | |
| 95 | } | |
| 96 | ||
| 97 | if (line.indexOf('@') === -1 && line.indexOf(':') === -1) { | |
| 98 | // Safari eval frames only have function names and nothing else | |
| 99 | return new StackFrame({ | |
| 100 | functionName: line | |
| 101 | }); | |
| 102 | } else { | |
| 103 | var functionNameRegex = /((.*".+"[^@]*)?[^@]*)(?:@)/; | |
| 104 | var matches = line.match(functionNameRegex); | |
| 105 | var functionName = matches && matches[1] ? matches[1] : undefined; | |
| 106 | var locationParts = this.extractLocation(line.replace(functionNameRegex, '')); | |
| 107 | ||
| 108 | return new StackFrame({ | |
| 109 | functionName: functionName, | |
| 110 | fileName: locationParts[0], | |
| 111 | lineNumber: locationParts[1], | |
| 112 | columnNumber: locationParts[2], | |
| 113 | source: line | |
| 114 | }); | |
| 115 | } | |
| 116 | }, this); | |
| 117 | }, | |
| 118 | ||
| 119 | parseOpera: function ErrorStackParser$$parseOpera(e) { | |
| 120 | if (!e.stacktrace || (e.message.indexOf('\n') > -1 && | |
| 121 | e.message.split('\n').length > e.stacktrace.split('\n').length)) { | |
| 122 | return this.parseOpera9(e); | |
| 123 | } else if (!e.stack) { | |
| 124 | return this.parseOpera10(e); | |
| 125 | } else { | |
| 126 | return this.parseOpera11(e); | |
| 127 | } | |
| 128 | }, | |
| 129 | ||
| 130 | parseOpera9: function ErrorStackParser$$parseOpera9(e) { | |
| 131 | var lineRE = /Line (\d+).*script (?:in )?(\S+)/i; | |
| 132 | var lines = e.message.split('\n'); | |
| 133 | var result = []; | |
| 134 | ||
| 135 | for (var i = 2, len = lines.length; i < len; i += 2) { | |
| 136 | var match = lineRE.exec(lines[i]); | |
| 137 | if (match) { | |
| 138 | result.push(new StackFrame({ | |
| 139 | fileName: match[2], | |
| 140 | lineNumber: match[1], | |
| 141 | source: lines[i] | |
| 142 | })); | |
| 143 | } | |
| 144 | } | |
| 145 | ||
| 146 | return result; | |
| 147 | }, | |
| 148 | ||
| 149 | parseOpera10: function ErrorStackParser$$parseOpera10(e) { | |
| 150 | var lineRE = /Line (\d+).*script (?:in )?(\S+)(?:: In function (\S+))?$/i; | |
| 151 | var lines = e.stacktrace.split('\n'); | |
| 152 | var result = []; | |
| 153 | ||
| 154 | for (var i = 0, len = lines.length; i < len; i += 2) { | |
| 155 | var match = lineRE.exec(lines[i]); | |
| 156 | if (match) { | |
| 157 | result.push( | |
| 158 | new StackFrame({ | |
| 159 | functionName: match[3] || undefined, | |
| 160 | fileName: match[2], | |
| 161 | lineNumber: match[1], | |
| 162 | source: lines[i] | |
| 163 | }) | |
| 164 | ); | |
| 165 | } | |
| 166 | } | |
| 167 | ||
| 168 | return result; | |
| 169 | }, | |
| 170 | ||
| 171 | // Opera 10.65+ Error.stack very similar to FF/Safari | |
| 172 | parseOpera11: function ErrorStackParser$$parseOpera11(error) { | |
| 173 | var filtered = error.stack.split('\n').filter(function(line) { | |
| 174 | return !!line.match(FIREFOX_SAFARI_STACK_REGEXP) && !line.match(/^Error created at/); | |
| 175 | }, this); | |
| 176 | ||
| 177 | return filtered.map(function(line) { | |
| 178 | var tokens = line.split('@'); | |
| 179 | var locationParts = this.extractLocation(tokens.pop()); | |
| 180 | var functionCall = (tokens.shift() || ''); | |
| 181 | var functionName = functionCall | |
| 182 | .replace(/<anonymous function(: (\w+))?>/, '$2') | |
| 183 | .replace(/\([^)]*\)/g, '') || undefined; | |
| 184 | var argsRaw; | |
| 185 | if (functionCall.match(/\(([^)]*)\)/)) { | |
| 186 | argsRaw = functionCall.replace(/^[^(]+\(([^)]*)\)$/, '$1'); | |
| 187 | } | |
| 188 | var args = (argsRaw === undefined || argsRaw === '[arguments not available]') ? | |
| 189 | undefined : argsRaw.split(','); | |
| 190 | ||
| 191 | return new StackFrame({ | |
| 192 | functionName: functionName, | |
| 193 | args: args, | |
| 194 | fileName: locationParts[0], | |
| 195 | lineNumber: locationParts[1], | |
| 196 | columnNumber: locationParts[2], | |
| 197 | source: line | |
| 198 | }); | |
| 199 | }, this); | |
| 200 | } | |
| 201 | }; | |
| 202 | })); |
| … | … | @@ -0,0 +1,63 @@ |
| 1 | { | |
| 2 | "name": "error-stack-parser", | |
| 3 | "description": "Extract meaning from JS Errors", | |
| 4 | "maintainers": [ | |
| 5 | "Eric Wendelin <me@eriwen.com> (https://www.eriwen.com)", | |
| 6 | "Victor Homyakov <vkhomyackov@gmail.com> (https://github.com/victor-homyakov)", | |
| 7 | "Oliver Salzburg (https://github.com/oliversalzburg)", | |
| 8 | "Ben Gourley (https://github.com/bengourley)" | |
| 9 | ], | |
| 10 | "version": "2.1.4", | |
| 11 | "license": "MIT", | |
| 12 | "keywords": [ | |
| 13 | "stacktrace", | |
| 14 | "error", | |
| 15 | "stack", | |
| 16 | "parser" | |
| 17 | ], | |
| 18 | "homepage": "https://www.stacktracejs.com", | |
| 19 | "dependencies": { | |
| 20 | "stackframe": "^1.3.4" | |
| 21 | }, | |
| 22 | "repository": { | |
| 23 | "type": "git", | |
| 24 | "url": "git://github.com/stacktracejs/error-stack-parser.git" | |
| 25 | }, | |
| 26 | "devDependencies": { | |
| 27 | "eslint": "^8.17.0", | |
| 28 | "jasmine": "^4.1.0", | |
| 29 | "jasmine-core": "^4.1.1", | |
| 30 | "karma": "^6.3.20", | |
| 31 | "karma-chrome-launcher": "^3.1.1", | |
| 32 | "karma-coverage": "^2.2.0", | |
| 33 | "karma-coveralls": "^2.1.0", | |
| 34 | "karma-firefox-launcher": "^2.1.2", | |
| 35 | "karma-ie-launcher": "^1.0.0", | |
| 36 | "karma-jasmine": "^4.0.2", | |
| 37 | "karma-opera-launcher": "^1.0.0", | |
| 38 | "karma-phantomjs-launcher": "^1.0.4", | |
| 39 | "karma-safari-launcher": "^1.0.0", | |
| 40 | "karma-sauce-launcher": "^4.3.6", | |
| 41 | "karma-spec-reporter": "^0.0.34", | |
| 42 | "uglify-es": "^3.3.9" | |
| 43 | }, | |
| 44 | "bugs": { | |
| 45 | "url": "https://github.com/stacktracejs/error-stack-parser/issues" | |
| 46 | }, | |
| 47 | "main": "./error-stack-parser.js", | |
| 48 | "typings": "./error-stack-parser.d.ts", | |
| 49 | "files": [ | |
| 50 | "LICENSE", | |
| 51 | "README.md", | |
| 52 | "error-stack-parser.js", | |
| 53 | "error-stack-parser.d.ts", | |
| 54 | "dist/" | |
| 55 | ], | |
| 56 | "scripts": { | |
| 57 | "lint": "eslint --fix .", | |
| 58 | "test": "karma start karma.conf.js --single-run", | |
| 59 | "test-pr": "karma start karma.conf.js --single-run --browsers Firefox,Chrome_No_Sandbox", | |
| 60 | "test-ci": "karma start karma.conf.ci.js --single-run", | |
| 61 | "prepare": "cp error-stack-parser.js dist/ && uglifyjs node_modules/stackframe/stackframe.js error-stack-parser.js -o dist/error-stack-parser.min.js --compress --mangle --source-map \"url=error-stack-parser.min.js.map\"" | |
| 62 | } | |
| 63 | } |
| … | … | @@ -0,0 +1,44 @@ |
| 1 | name: Test with Bare | |
| 2 | ||
| 3 | on: | |
| 4 | push: | |
| 5 | branches: [ main ] | |
| 6 | pull_request: | |
| 7 | branches: [ main ] | |
| 8 | ||
| 9 | jobs: | |
| 10 | test: | |
| 11 | strategy: | |
| 12 | matrix: | |
| 13 | include: | |
| 14 | - os: ubuntu-latest | |
| 15 | platform: linux | |
| 16 | arch: x64 | |
| 17 | ||
| 18 | - os: macos-latest | |
| 19 | platform: darwin | |
| 20 | arch: x64 | |
| 21 | ||
| 22 | - os: windows-latest | |
| 23 | platform: win32 | |
| 24 | arch: x64 | |
| 25 | ||
| 26 | runs-on: ${{ matrix.os }} | |
| 27 | name: ${{ matrix.platform }}-${{ matrix.arch }} | |
| 28 | steps: | |
| 29 | - uses: actions/checkout@v4 | |
| 30 | ||
| 31 | - name: Setup Node.js | |
| 32 | uses: actions/setup-node@v4 | |
| 33 | ||
| 34 | - name: Setup bare | |
| 35 | run: npm i -g bare-runtime | |
| 36 | ||
| 37 | - name: Install dependencies | |
| 38 | run: npm i | |
| 39 | ||
| 40 | - name: Lint | |
| 41 | run: npm run lint | |
| 42 | ||
| 43 | - name: Run tests | |
| 44 | run: npm run test:bare |
| … | … | @@ -0,0 +1,41 @@ |
| 1 | name: Test with Node | |
| 2 | ||
| 3 | on: | |
| 4 | push: | |
| 5 | branches: [ main ] | |
| 6 | pull_request: | |
| 7 | branches: [ main ] | |
| 8 | ||
| 9 | jobs: | |
| 10 | test: | |
| 11 | strategy: | |
| 12 | matrix: | |
| 13 | include: | |
| 14 | - os: ubuntu-latest | |
| 15 | platform: linux | |
| 16 | arch: x64 | |
| 17 | ||
| 18 | - os: macos-latest | |
| 19 | platform: darwin | |
| 20 | arch: x64 | |
| 21 | ||
| 22 | - os: windows-latest | |
| 23 | platform: win32 | |
| 24 | arch: x64 | |
| 25 | ||
| 26 | runs-on: ${{ matrix.os }} | |
| 27 | name: ${{ matrix.platform }}-${{ matrix.arch }} | |
| 28 | steps: | |
| 29 | - uses: actions/checkout@v4 | |
| 30 | ||
| 31 | - name: Setup Node.js | |
| 32 | uses: actions/setup-node@v4 | |
| 33 | ||
| 34 | - name: Install dependencies | |
| 35 | run: npm i | |
| 36 | ||
| 37 | - name: Lint | |
| 38 | run: npm run lint | |
| 39 | ||
| 40 | - name: Run tests | |
| 41 | run: npm test |
| … | … | @@ -0,0 +1,202 @@ |
| 1 | Apache License | |
| 2 | Version 2.0, January 2004 | |
| 3 | http://www.apache.org/licenses/ | |
| 4 | ||
| 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | |
| 6 | ||
| 7 | 1. Definitions. | |
| 8 | ||
| 9 | "License" shall mean the terms and conditions for use, reproduction, | |
| 10 | and distribution as defined by Sections 1 through 9 of this document. | |
| 11 | ||
| 12 | "Licensor" shall mean the copyright owner or entity authorized by | |
| 13 | the copyright owner that is granting the License. | |
| 14 | ||
| 15 | "Legal Entity" shall mean the union of the acting entity and all | |
| 16 | other entities that control, are controlled by, or are under common | |
| 17 | control with that entity. For the purposes of this definition, | |
| 18 | "control" means (i) the power, direct or indirect, to cause the | |
| 19 | direction or management of such entity, whether by contract or | |
| 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the | |
| 21 | outstanding shares, or (iii) beneficial ownership of such entity. | |
| 22 | ||
| 23 | "You" (or "Your") shall mean an individual or Legal Entity | |
| 24 | exercising permissions granted by this License. | |
| 25 | ||
| 26 | "Source" form shall mean the preferred form for making modifications, | |
| 27 | including but not limited to software source code, documentation | |
| 28 | source, and configuration files. | |
| 29 | ||
| 30 | "Object" form shall mean any form resulting from mechanical | |
| 31 | transformation or translation of a Source form, including but | |
| 32 | not limited to compiled object code, generated documentation, | |
| 33 | and conversions to other media types. | |
| 34 | ||
| 35 | "Work" shall mean the work of authorship, whether in Source or | |
| 36 | Object form, made available under the License, as indicated by a | |
| 37 | copyright notice that is included in or attached to the work | |
| 38 | (an example is provided in the Appendix below). | |
| 39 | ||
| 40 | "Derivative Works" shall mean any work, whether in Source or Object | |
| 41 | form, that is based on (or derived from) the Work and for which the | |
| 42 | editorial revisions, annotations, elaborations, or other modifications | |
| 43 | represent, as a whole, an original work of authorship. For the purposes | |
| 44 | of this License, Derivative Works shall not include works that remain | |
| 45 | separable from, or merely link (or bind by name) to the interfaces of, | |
| 46 | the Work and Derivative Works thereof. | |
| 47 | ||
| 48 | "Contribution" shall mean any work of authorship, including | |
| 49 | the original version of the Work and any modifications or additions | |
| 50 | to that Work or Derivative Works thereof, that is intentionally | |
| 51 | submitted to Licensor for inclusion in the Work by the copyright owner | |
| 52 | or by an individual or Legal Entity authorized to submit on behalf of | |
| 53 | the copyright owner. For the purposes of this definition, "submitted" | |
| 54 | means any form of electronic, verbal, or written communication sent | |
| 55 | to the Licensor or its representatives, including but not limited to | |
| 56 | communication on electronic mailing lists, source code control systems, | |
| 57 | and issue tracking systems that are managed by, or on behalf of, the | |
| 58 | Licensor for the purpose of discussing and improving the Work, but | |
| 59 | excluding communication that is conspicuously marked or otherwise | |
| 60 | designated in writing by the copyright owner as "Not a Contribution." | |
| 61 | ||
| 62 | "Contributor" shall mean Licensor and any individual or Legal Entity | |
| 63 | on behalf of whom a Contribution has been received by Licensor and | |
| 64 | subsequently incorporated within the Work. | |
| 65 | ||
| 66 | 2. Grant of Copyright License. Subject to the terms and conditions of | |
| 67 | this License, each Contributor hereby grants to You a perpetual, | |
| 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 69 | copyright license to reproduce, prepare Derivative Works of, | |
| 70 | publicly display, publicly perform, sublicense, and distribute the | |
| 71 | Work and such Derivative Works in Source or Object form. | |
| 72 | ||
| 73 | 3. Grant of Patent License. Subject to the terms and conditions of | |
| 74 | this License, each Contributor hereby grants to You a perpetual, | |
| 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 76 | (except as stated in this section) patent license to make, have made, | |
| 77 | use, offer to sell, sell, import, and otherwise transfer the Work, | |
| 78 | where such license applies only to those patent claims licensable | |
| 79 | by such Contributor that are necessarily infringed by their | |
| 80 | Contribution(s) alone or by combination of their Contribution(s) | |
| 81 | with the Work to which such Contribution(s) was submitted. If You | |
| 82 | institute patent litigation against any entity (including a | |
| 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work | |
| 84 | or a Contribution incorporated within the Work constitutes direct | |
| 85 | or contributory patent infringement, then any patent licenses | |
| 86 | granted to You under this License for that Work shall terminate | |
| 87 | as of the date such litigation is filed. | |
| 88 | ||
| 89 | 4. Redistribution. You may reproduce and distribute copies of the | |
| 90 | Work or Derivative Works thereof in any medium, with or without | |
| 91 | modifications, and in Source or Object form, provided that You | |
| 92 | meet the following conditions: | |
| 93 | ||
| 94 | (a) You must give any other recipients of the Work or | |
| 95 | Derivative Works a copy of this License; and | |
| 96 | ||
| 97 | (b) You must cause any modified files to carry prominent notices | |
| 98 | stating that You changed the files; and | |
| 99 | ||
| 100 | (c) You must retain, in the Source form of any Derivative Works | |
| 101 | that You distribute, all copyright, patent, trademark, and | |
| 102 | attribution notices from the Source form of the Work, | |
| 103 | excluding those notices that do not pertain to any part of | |
| 104 | the Derivative Works; and | |
| 105 | ||
| 106 | (d) If the Work includes a "NOTICE" text file as part of its | |
| 107 | distribution, then any Derivative Works that You distribute must | |
| 108 | include a readable copy of the attribution notices contained | |
| 109 | within such NOTICE file, excluding those notices that do not | |
| 110 | pertain to any part of the Derivative Works, in at least one | |
| 111 | of the following places: within a NOTICE text file distributed | |
| 112 | as part of the Derivative Works; within the Source form or | |
| 113 | documentation, if provided along with the Derivative Works; or, | |
| 114 | within a display generated by the Derivative Works, if and | |
| 115 | wherever such third-party notices normally appear. The contents | |
| 116 | of the NOTICE file are for informational purposes only and | |
| 117 | do not modify the License. You may add Your own attribution | |
| 118 | notices within Derivative Works that You distribute, alongside | |
| 119 | or as an addendum to the NOTICE text from the Work, provided | |
| 120 | that such additional attribution notices cannot be construed | |
| 121 | as modifying the License. | |
| 122 | ||
| 123 | You may add Your own copyright statement to Your modifications and | |
| 124 | may provide additional or different license terms and conditions | |
| 125 | for use, reproduction, or distribution of Your modifications, or | |
| 126 | for any such Derivative Works as a whole, provided Your use, | |
| 127 | reproduction, and distribution of the Work otherwise complies with | |
| 128 | the conditions stated in this License. | |
| 129 | ||
| 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, | |
| 131 | any Contribution intentionally submitted for inclusion in the Work | |
| 132 | by You to the Licensor shall be under the terms and conditions of | |
| 133 | this License, without any additional terms or conditions. | |
| 134 | Notwithstanding the above, nothing herein shall supersede or modify | |
| 135 | the terms of any separate license agreement you may have executed | |
| 136 | with Licensor regarding such Contributions. | |
| 137 | ||
| 138 | 6. Trademarks. This License does not grant permission to use the trade | |
| 139 | names, trademarks, service marks, or product names of the Licensor, | |
| 140 | except as required for reasonable and customary use in describing the | |
| 141 | origin of the Work and reproducing the content of the NOTICE file. | |
| 142 | ||
| 143 | 7. Disclaimer of Warranty. Unless required by applicable law or | |
| 144 | agreed to in writing, Licensor provides the Work (and each | |
| 145 | Contributor provides its Contributions) on an "AS IS" BASIS, | |
| 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |
| 147 | implied, including, without limitation, any warranties or conditions | |
| 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | |
| 149 | PARTICULAR PURPOSE. You are solely responsible for determining the | |
| 150 | appropriateness of using or redistributing the Work and assume any | |
| 151 | risks associated with Your exercise of permissions under this License. | |
| 152 | ||
| 153 | 8. Limitation of Liability. In no event and under no legal theory, | |
| 154 | whether in tort (including negligence), contract, or otherwise, | |
| 155 | unless required by applicable law (such as deliberate and grossly | |
| 156 | negligent acts) or agreed to in writing, shall any Contributor be | |
| 157 | liable to You for damages, including any direct, indirect, special, | |
| 158 | incidental, or consequential damages of any character arising as a | |
| 159 | result of this License or out of the use or inability to use the | |
| 160 | Work (including but not limited to damages for loss of goodwill, | |
| 161 | work stoppage, computer failure or malfunction, or any and all | |
| 162 | other commercial damages or losses), even if such Contributor | |
| 163 | has been advised of the possibility of such damages. | |
| 164 | ||
| 165 | 9. Accepting Warranty or Additional Liability. While redistributing | |
| 166 | the Work or Derivative Works thereof, You may choose to offer, | |
| 167 | and charge a fee for, acceptance of support, warranty, indemnity, | |
| 168 | or other liability obligations and/or rights consistent with this | |
| 169 | License. However, in accepting such obligations, You may act only | |
| 170 | on Your own behalf and on Your sole responsibility, not on behalf | |
| 171 | of any other Contributor, and only if You agree to indemnify, | |
| 172 | defend, and hold each Contributor harmless for any liability | |
| 173 | incurred by, or claims asserted against, such Contributor by reason | |
| 174 | of your accepting any such warranty or additional liability. | |
| 175 | ||
| 176 | END OF TERMS AND CONDITIONS | |
| 177 | ||
| 178 | APPENDIX: How to apply the Apache License to your work. | |
| 179 | ||
| 180 | To apply the Apache License to your work, attach the following | |
| 181 | boilerplate notice, with the fields enclosed by brackets "[]" | |
| 182 | replaced with your own identifying information. (Don't include | |
| 183 | the brackets!) The text should be enclosed in the appropriate | |
| 184 | comment syntax for the file format. We also recommend that a | |
| 185 | file or class name and description of purpose be included on the | |
| 186 | same "printed page" as the copyright notice for easier | |
| 187 | identification within third-party archives. | |
| 188 | ||
| 189 | Copyright [yyyy] [name of copyright owner] | |
| 190 | ||
| 191 | Licensed under the Apache License, Version 2.0 (the "License"); | |
| 192 | you may not use this file except in compliance with the License. | |
| 193 | You may obtain a copy of the License at | |
| 194 | ||
| 195 | http://www.apache.org/licenses/LICENSE-2.0 | |
| 196 | ||
| 197 | Unless required by applicable law or agreed to in writing, software | |
| 198 | distributed under the License is distributed on an "AS IS" BASIS, | |
| 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 200 | See the License for the specific language governing permissions and | |
| 201 | limitations under the License. | |
| 202 |
| … | … | @@ -0,0 +1,13 @@ |
| 1 | Copyright 2023-2024 Holepunch Inc | |
| 2 | ||
| 3 | Licensed under the Apache License, Version 2.0 (the "License"); | |
| 4 | you may not use this file except in compliance with the License. | |
| 5 | You may obtain a copy of the License at | |
| 6 | ||
| 7 | http://www.apache.org/licenses/LICENSE-2.0 | |
| 8 | ||
| 9 | Unless required by applicable law or agreed to in writing, software | |
| 10 | distributed under the License is distributed on an "AS IS" BASIS, | |
| 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 12 | See the License for the specific language governing permissions and | |
| 13 | limitations under the License. |
| … | … | @@ -0,0 +1,22 @@ |
| 1 | # globbie | |
| 2 | A simple directory glob matcher using picomatch | |
| 3 | ||
| 4 | ## Usage | |
| 5 | ```js | |
| 6 | const Globbie = require('globbie') | |
| 7 | ||
| 8 | const pattern = 'src/**/*.js' | |
| 9 | ||
| 10 | const globbie = new Globbie(pattern) | |
| 11 | await globbie.match() // => ['src/index.js', 'src/utils/index.js', ...] | |
| 12 | ||
| 13 | const globbieSync = new Globbie(pattern, { sync: true }) | |
| 14 | globbie.match() // => ['src/index.js', 'src/utils/index.js', ...] | |
| 15 | ``` | |
| 16 | ||
| 17 | ## Options | |
| 18 | The following options are available to be set in the `options` object: | |
| 19 | - `sync` (boolean, default: `false`): Whether to use async or sync methods when running match | |
| 20 | ||
| 21 | ## License | |
| 22 | Apache-2.0 |
| … | … | @@ -0,0 +1,48 @@ |
| 1 | const fs = require('fs') | |
| 2 | const path = require('path') | |
| 3 | const picomatch = require('picomatch') | |
| 4 | const process = require('process') | |
| 5 | ||
| 6 | class Globbie { | |
| 7 | constructor (pattern, { sync = false } = {}) { | |
| 8 | this._isMatch = picomatch(pattern, { windows: process.platform === 'win32' }) | |
| 9 | this._sync = sync | |
| 10 | } | |
| 11 | ||
| 12 | match (dir = '.') { | |
| 13 | return this._sync ? this.#matchSync(dir) : this.#matchAsync(dir) | |
| 14 | } | |
| 15 | ||
| 16 | #matchSync (dir = '.') { | |
| 17 | const matches = [] | |
| 18 | for (const file of fs.readdirSync(dir)) { | |
| 19 | const filePath = path.join(dir, file) | |
| 20 | if (fs.lstatSync(filePath).isDirectory()) matches.push(...this.match(filePath)) | |
| 21 | else if (this._isMatch(filePath)) matches.push(filePath) | |
| 22 | } | |
| 23 | ||
| 24 | return matches | |
| 25 | } | |
| 26 | ||
| 27 | async #matchAsync (dir = '.') { | |
| 28 | const files = await fs.promises.readdir(dir) | |
| 29 | ||
| 30 | const matches = [] | |
| 31 | const promises = files.map(async (file) => { | |
| 32 | const filePath = path.join(dir, file) | |
| 33 | ||
| 34 | if ((await fs.promises.lstat(filePath)).isDirectory()) matches.push(...(await this.#matchAsync(filePath))) | |
| 35 | else if (this._isMatch(filePath)) matches.push(filePath) | |
| 36 | }) | |
| 37 | ||
| 38 | for (const promise of promises) promise.catch(() => {}) | |
| 39 | ||
| 40 | const results = await Promise.allSettled(promises) | |
| 41 | const error = results.find(({ status }) => status === 'rejected')?.reason | |
| 42 | if (error) throw error | |
| 43 | ||
| 44 | return matches | |
| 45 | } | |
| 46 | } | |
| 47 | ||
| 48 | module.exports = Globbie |
| … | … | @@ -0,0 +1,47 @@ |
| 1 | { | |
| 2 | "name": "globbie", | |
| 3 | "version": "1.0.3", | |
| 4 | "description": "Simple directory globbing using picomatch", | |
| 5 | "main": "index.js", | |
| 6 | "scripts": { | |
| 7 | "test": "node test/globbie.test.js", | |
| 8 | "test:bare": "bare test/globbie.test.js", | |
| 9 | "lint": "standard" | |
| 10 | }, | |
| 11 | "repository": { | |
| 12 | "type": "git", | |
| 13 | "url": "git+https://github.com/holepunchto/globbie.git" | |
| 14 | }, | |
| 15 | "keywords": [ | |
| 16 | "globbie", | |
| 17 | "glob", | |
| 18 | "picomatch" | |
| 19 | ], | |
| 20 | "author": "Holepunch", | |
| 21 | "license": "Apache-2.0", | |
| 22 | "bugs": { | |
| 23 | "url": "https://github.com/holepunchto/globbie/issues" | |
| 24 | }, | |
| 25 | "homepage": "https://github.com/holepunchto/globbie#readme", | |
| 26 | "imports": { | |
| 27 | "fs": { | |
| 28 | "bare": "bare-fs" | |
| 29 | }, | |
| 30 | "path": { | |
| 31 | "bare": "bare-path" | |
| 32 | }, | |
| 33 | "process": { | |
| 34 | "bare": "bare-process" | |
| 35 | } | |
| 36 | }, | |
| 37 | "dependencies": { | |
| 38 | "bare-fs": "^4.1.2", | |
| 39 | "bare-path": "^3.0.0", | |
| 40 | "bare-process": "^4.2.1", | |
| 41 | "picomatch": "^4.0.2" | |
| 42 | }, | |
| 43 | "devDependencies": { | |
| 44 | "brittle": "^3.16.2", | |
| 45 | "standard": "^17.1.0" | |
| 46 | } | |
| 47 | } |
| … | … | @@ -0,0 +1,133 @@ |
| 1 | const Globbie = require('../index') | |
| 2 | const path = require('path') | |
| 3 | const test = require('brittle') | |
| 4 | const process = require('process') | |
| 5 | ||
| 6 | process.chdir(path.join(__dirname, 'fixtures')) | |
| 7 | ||
| 8 | function toPlatformPaths (paths) { | |
| 9 | return process.platform === 'win32' ? paths.map(p => p.replace(/\//g, path.sep)) : paths | |
| 10 | } | |
| 11 | ||
| 12 | test('match all js files', async (t) => { | |
| 13 | const g = new Globbie('**.js', { sync: true }) | |
| 14 | const matches = g.match() | |
| 15 | ||
| 16 | t.alike(matches.sort(), toPlatformPaths([ | |
| 17 | 'rootfile-1.js', | |
| 18 | 'rootfile-2.js', | |
| 19 | 'rootfile-3.js', | |
| 20 | 'subpath/subpathfile-1.js', | |
| 21 | 'subpath/subpathfile-2.js', | |
| 22 | 'subpath/subpathfile-3.js', | |
| 23 | 'subpath/subsubpath/subsubpathfile-1.js', | |
| 24 | 'subpath/subsubpath/subsubpathfile-2.js', | |
| 25 | 'subpath/subsubpath/subsubpathfile-3.js' | |
| 26 | ])) | |
| 27 | }) | |
| 28 | ||
| 29 | test('match all js and ts files', async (t) => { | |
| 30 | const g = new Globbie('**/*.{js,ts}', { sync: true }) | |
| 31 | const matches = g.match() | |
| 32 | ||
| 33 | t.alike(matches.sort(), toPlatformPaths([ | |
| 34 | 'rootfile-1.js', | |
| 35 | 'rootfile-2.js', | |
| 36 | 'rootfile-3.js', | |
| 37 | 'rootfile-invalid.ts', | |
| 38 | 'subpath/subpathfile-1.js', | |
| 39 | 'subpath/subpathfile-2.js', | |
| 40 | 'subpath/subpathfile-3.js', | |
| 41 | 'subpath/subpathfile-invalid.ts', | |
| 42 | 'subpath/subsubpath/subsubpathfile-1.js', | |
| 43 | 'subpath/subsubpath/subsubpathfile-2.js', | |
| 44 | 'subpath/subsubpath/subsubpathfile-3.js', | |
| 45 | 'subpath/subsubpath/subsubpathfile-invalid.ts' | |
| 46 | ])) | |
| 47 | }) | |
| 48 | ||
| 49 | test('match only subpath js files', async (t) => { | |
| 50 | const g = new Globbie('subpath/**/*.js', { sync: true }) | |
| 51 | const matches = g.match() | |
| 52 | ||
| 53 | t.alike(matches.sort(), toPlatformPaths([ | |
| 54 | 'subpath/subpathfile-1.js', | |
| 55 | 'subpath/subpathfile-2.js', | |
| 56 | 'subpath/subpathfile-3.js', | |
| 57 | 'subpath/subsubpath/subsubpathfile-1.js', | |
| 58 | 'subpath/subsubpath/subsubpathfile-2.js', | |
| 59 | 'subpath/subsubpath/subsubpathfile-3.js' | |
| 60 | ])) | |
| 61 | }) | |
| 62 | ||
| 63 | test('match only subpath js files - dir set to subsubpath', async (t) => { | |
| 64 | const g = new Globbie('subpath/**/*.js', { sync: true }) | |
| 65 | const matches = g.match('subpath/subsubpath') | |
| 66 | ||
| 67 | t.alike(matches.sort(), toPlatformPaths([ | |
| 68 | 'subpath/subsubpath/subsubpathfile-1.js', | |
| 69 | 'subpath/subsubpath/subsubpathfile-2.js', | |
| 70 | 'subpath/subsubpath/subsubpathfile-3.js' | |
| 71 | ])) | |
| 72 | }) | |
| 73 | ||
| 74 | test('async match all js files', async (t) => { | |
| 75 | const g = new Globbie('**.js') | |
| 76 | const matches = await g.match() | |
| 77 | t.alike(matches.sort(), toPlatformPaths([ | |
| 78 | 'rootfile-1.js', | |
| 79 | 'rootfile-2.js', | |
| 80 | 'rootfile-3.js', | |
| 81 | 'subpath/subpathfile-1.js', | |
| 82 | 'subpath/subpathfile-2.js', | |
| 83 | 'subpath/subpathfile-3.js', | |
| 84 | 'subpath/subsubpath/subsubpathfile-1.js', | |
| 85 | 'subpath/subsubpath/subsubpathfile-2.js', | |
| 86 | 'subpath/subsubpath/subsubpathfile-3.js' | |
| 87 | ])) | |
| 88 | }) | |
| 89 | ||
| 90 | test('async match all js and ts files', async (t) => { | |
| 91 | const g = new Globbie('**/*.{js,ts}') | |
| 92 | const matches = await g.match() | |
| 93 | ||
| 94 | t.alike(matches.sort(), toPlatformPaths([ | |
| 95 | 'rootfile-1.js', | |
| 96 | 'rootfile-2.js', | |
| 97 | 'rootfile-3.js', | |
| 98 | 'rootfile-invalid.ts', | |
| 99 | 'subpath/subpathfile-1.js', | |
| 100 | 'subpath/subpathfile-2.js', | |
| 101 | 'subpath/subpathfile-3.js', | |
| 102 | 'subpath/subpathfile-invalid.ts', | |
| 103 | 'subpath/subsubpath/subsubpathfile-1.js', | |
| 104 | 'subpath/subsubpath/subsubpathfile-2.js', | |
| 105 | 'subpath/subsubpath/subsubpathfile-3.js', | |
| 106 | 'subpath/subsubpath/subsubpathfile-invalid.ts' | |
| 107 | ])) | |
| 108 | }) | |
| 109 | ||
| 110 | test('async match only subpath', async (t) => { | |
| 111 | const g = new Globbie('subpath/**/*.js') | |
| 112 | const matches = await g.match() | |
| 113 | ||
| 114 | t.alike(matches.sort(), toPlatformPaths([ | |
| 115 | 'subpath/subpathfile-1.js', | |
| 116 | 'subpath/subpathfile-2.js', | |
| 117 | 'subpath/subpathfile-3.js', | |
| 118 | 'subpath/subsubpath/subsubpathfile-1.js', | |
| 119 | 'subpath/subsubpath/subsubpathfile-2.js', | |
| 120 | 'subpath/subsubpath/subsubpathfile-3.js' | |
| 121 | ])) | |
| 122 | }) | |
| 123 | ||
| 124 | test('async match only subpath js files - dir set to subsubpath', async (t) => { | |
| 125 | const g = new Globbie('subpath/**/*.js') | |
| 126 | const matches = await g.match('subpath/subsubpath') | |
| 127 | ||
| 128 | t.alike(matches.sort(), toPlatformPaths([ | |
| 129 | 'subpath/subsubpath/subsubpathfile-1.js', | |
| 130 | 'subpath/subsubpath/subsubpathfile-2.js', | |
| 131 | 'subpath/subsubpath/subsubpathfile-3.js' | |
| 132 | ])) | |
| 133 | }) |
| … | … | @@ -0,0 +1,21 @@ |
| 1 | The MIT License (MIT) | |
| 2 | ||
| 3 | Copyright (c) 2018-2019 Mathias Buus, David Mark Clements & Contributors | |
| 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 | |
| 13 | all 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 | |
| 21 | THE SOFTWARE. |
| … | … | @@ -0,0 +1,334 @@ |
| 1 | # hyperdht | |
| 2 | ||
| 3 | ### [See the full API docs at docs.pears.com](https://docs.pears.com/building-blocks/hyperdht) | |
| 4 | ||
| 5 | The DHT powering Hyperswarm | |
| 6 | ||
| 7 | ``` | |
| 8 | npm install hyperdht | |
| 9 | ``` | |
| 10 | ||
| 11 | Built on top of [dht-rpc](https://github.com/mafintosh/dht-rpc). | |
| 12 | ||
| 13 | The Hyperswarm DHT uses a series of holepunching techniques to make sure connectivity works on most networks, | |
| 14 | and is mainly used to facilitate finding and connecting to peers using end to end encrypted Noise streams. | |
| 15 | ||
| 16 | ## Usage | |
| 17 | ||
| 18 | To try it out, first instantiate a DHT instance | |
| 19 | ||
| 20 | ```js | |
| 21 | import DHT from 'hyperdht' | |
| 22 | ||
| 23 | const node = new DHT() | |
| 24 | ``` | |
| 25 | ||
| 26 | Then on one computer listen for connections | |
| 27 | ||
| 28 | ```js | |
| 29 | // create a server to listen for secure connections | |
| 30 | const server = node.createServer() | |
| 31 | ||
| 32 | server.on('connection', function (socket) { | |
| 33 | // socket is E2E encrypted between you and the other peer | |
| 34 | console.log('Remote public key', socket.remotePublicKey) | |
| 35 | ||
| 36 | // pipe it somewhere like any duplex stream | |
| 37 | process.stdin.pipe(socket).pipe(process.stdout) | |
| 38 | }) | |
| 39 | ||
| 40 | // make a ed25519 keypair to listen on | |
| 41 | const keyPair = DHT.keyPair() | |
| 42 | ||
| 43 | // this makes the server accept connections on this keypair | |
| 44 | await server.listen(keyPair) | |
| 45 | ``` | |
| 46 | ||
| 47 | Then on another connect to the computer using the public key of the key-pair it is listening on | |
| 48 | ||
| 49 | ```js | |
| 50 | // publicKey here is keyPair.publicKey from above | |
| 51 | const socket = anotherNode.connect(publicKey) | |
| 52 | ||
| 53 | socket.on('open', function () { | |
| 54 | // socket fully open with the other peer | |
| 55 | }) | |
| 56 | ||
| 57 | // pipe it somewhere like any duplex stream | |
| 58 | process.stdin.pipe(socket).pipe(process.stdout) | |
| 59 | ``` | |
| 60 | ||
| 61 | ## API | |
| 62 | ||
| 63 | #### `const node = new DHT([options])` | |
| 64 | ||
| 65 | Create a new DHT node. | |
| 66 | ||
| 67 | Options include: | |
| 68 | ||
| 69 | ```js | |
| 70 | { | |
| 71 | // Optionally overwrite the default bootstrap servers, just need to be an array of any known dht node(s) | |
| 72 | // Defaults to Pear.config.dht.bootstrap in a Pear app or ['88.99.3.86@node1.hyperdht.org:49737', '142.93.90.113@node2.hyperdht.org:49737', '138.68.147.8@node3.hyperdht.org:49737'] elsewhere | |
| 73 | // Supports suggested-IP to avoid DNS calls: [suggested-IP@]<host>:<port> | |
| 74 | bootstrap: ['host:port'], | |
| 75 | keyPair, // set the default key pair to use for server.listen and connect | |
| 76 | connectionKeepAlive, // set a default keep-alive (in ms) on all opened sockets. Defaults to 5000. Set false to turn off (advanced usage). | |
| 77 | randomPunchInterval: 20000 // set a default time for interval between punches (in ms). Defaults to 20000. | |
| 78 | ||
| 79 | } | |
| 80 | ``` | |
| 81 | ||
| 82 | See [dht-rpc](https://github.com/mafintosh/dht-rpc) for more options as HyperDHT inherits from that. | |
| 83 | ||
| 84 | _Note:_ The default bootstrap servers are publicly served on behalf of the commons. To run a fully isolated DHT, start one or more dht nodes with an empty bootstrap array (`new DHT({bootstrap:[]})`) and then use the addresses of those nodes as the `bootstrap` option in all other dht nodes. You'll need at least one persistent node for the network to be completely operational. | |
| 85 | ||
| 86 | #### `keyPair = DHT.keyPair([seed])` | |
| 87 | ||
| 88 | Use this method to generate the required keypair for DHT operations. | |
| 89 | ||
| 90 | Returns an object with `{publicKey, secretKey}`. `publicKey` holds a public key buffer, `secretKey` holds a private key buffer. | |
| 91 | ||
| 92 | If you pass any options they are forwarded to dht-rpc. | |
| 93 | ||
| 94 | #### `await node.destroy([options])` | |
| 95 | ||
| 96 | Fully destroy this DHT node. | |
| 97 | ||
| 98 | This will also unannounce any running servers. | |
| 99 | If you want to force close the node without waiting for the servers to unannounce pass `{ force: true }`. | |
| 100 | ||
| 101 | #### `node = DHT.bootstrapper(port, host, [options])` | |
| 102 | ||
| 103 | If you want to run your own Hyperswarm network use this method to easily create a bootstrap node. | |
| 104 | ||
| 105 | ## Creating P2P servers | |
| 106 | ||
| 107 | #### `const server = node.createServer([options], [onconnection])` | |
| 108 | ||
| 109 | Create a new server for accepting incoming encrypted P2P connections. | |
| 110 | ||
| 111 | Options include: | |
| 112 | ||
| 113 | ```js | |
| 114 | { | |
| 115 | firewall (remotePublicKey, remoteHandshakePayload) { | |
| 116 | // validate if you want a connection from remotePublicKey | |
| 117 | // if you do return false, else return true | |
| 118 | // remoteHandshakePayload contains their ip and some more info | |
| 119 | return true | |
| 120 | } | |
| 121 | } | |
| 122 | ``` | |
| 123 | ||
| 124 | You can run servers on normal home computers, as the DHT will UDP holepunch connections for you. | |
| 125 | ||
| 126 | #### `await server.listen(keyPair)` | |
| 127 | ||
| 128 | Make the server listen on a keyPair. | |
| 129 | To connect to this server use keyPair.publicKey as the connect address. | |
| 130 | ||
| 131 | #### `server.refresh()` | |
| 132 | ||
| 133 | Refresh the server, causing it to reannounce its address. This is automatically called on network changes. | |
| 134 | ||
| 135 | #### `server.on('connection', socket)` | |
| 136 | ||
| 137 | Emitted when a new encrypted connection has passed the firewall check. | |
| 138 | ||
| 139 | `socket` is a [NoiseSecretStream](https://github.com/holepunchto/hyperswarm-secret-stream) instance. | |
| 140 | ||
| 141 | You can check who you are connected to using `socket.remotePublicKey` and `socket.handshakeHash` contains a unique hash representing this crypto session (same on both sides). | |
| 142 | ||
| 143 | #### `server.on('listening')` | |
| 144 | ||
| 145 | Emitted when the server is fully listening on a keyPair. | |
| 146 | ||
| 147 | #### `server.address()` | |
| 148 | ||
| 149 | Returns an object containing the address of the server: | |
| 150 | ||
| 151 | ```js | |
| 152 | { | |
| 153 | ;(host, // external IP of the server, | |
| 154 | port, // external port of the server if predictable, | |
| 155 | publicKey) // public key of the server | |
| 156 | } | |
| 157 | ``` | |
| 158 | ||
| 159 | You can also get this info from `node.remoteAddress()` minus the public key. | |
| 160 | ||
| 161 | #### `await server.close()` | |
| 162 | ||
| 163 | Stop listening. | |
| 164 | ||
| 165 | #### `server.on('close')` | |
| 166 | ||
| 167 | Emitted when the server is fully closed. | |
| 168 | ||
| 169 | ## Connecting to P2P servers | |
| 170 | ||
| 171 | #### `const socket = node.connect(remotePublicKey, [options])` | |
| 172 | ||
| 173 | Connect to a remote server. Similar to `createServer` this performs UDP holepunching for P2P connectivity. | |
| 174 | ||
| 175 | The remote public key can be encoded as either a buffer, a hex string or a z-base32 string. | |
| 176 | ||
| 177 | Options include: | |
| 178 | ||
| 179 | ```js | |
| 180 | { | |
| 181 | nodes: [...], // optional array of close dht nodes to speed up connecting | |
| 182 | keyPair // optional key pair to use when connection (defaults to node.defaultKeyPair) | |
| 183 | } | |
| 184 | ``` | |
| 185 | ||
| 186 | #### `socket.on('open')` | |
| 187 | ||
| 188 | Emitted when the encrypted connection has been fully established with the server. | |
| 189 | ||
| 190 | #### `socket.remotePublicKey` | |
| 191 | ||
| 192 | The public key of the remote peer. | |
| 193 | ||
| 194 | #### `socket.publicKey` | |
| 195 | ||
| 196 | The public key of the local socket. | |
| 197 | ||
| 198 | ## Additional peer discovery | |
| 199 | ||
| 200 | #### `const stream = node.lookup(topic, [options])` | |
| 201 | ||
| 202 | Look for peers in the DHT on the given topic. Topic should be a 32 byte buffer (normally a hash of something). | |
| 203 | ||
| 204 | The returned stream looks like this | |
| 205 | ||
| 206 | ```js | |
| 207 | { | |
| 208 | // Who sent the response? | |
| 209 | from: { id, host, port }, | |
| 210 | // What address they responded to (i.e. your address) | |
| 211 | to: { host, port }, | |
| 212 | // List of peers announcing under this topic | |
| 213 | peers: [ { publicKey, nodes: [{ host, port }, ...] } ] | |
| 214 | } | |
| 215 | ``` | |
| 216 | ||
| 217 | To connect to the peers you should afterwards call `connect` with those public keys. | |
| 218 | ||
| 219 | If you pass any options they are forwarded to dht-rpc. | |
| 220 | ||
| 221 | #### `const stream = node.announce(topic, keyPair, [relayAddresses], [options])` | |
| 222 | ||
| 223 | Announce that you are listening on a key-pair to the DHT under a specific topic. | |
| 224 | ||
| 225 | When announcing you'll send a signed proof to peers that you own the key-pair and wish to announce under the specific topic. Optionally you can provide up to 3 nodes, indicating which DHT nodes can relay messages to you - this speeds up connects later on for other users. | |
| 226 | ||
| 227 | An announce does a parallel lookup so the stream returned looks like the lookup stream. | |
| 228 | ||
| 229 | Creating a server using `dht.createServer` automatically announces itself periodically on the key-pair it is listening on. When announcing the server under a specific topic, you can access the nodes it is close to using `server.nodes`. | |
| 230 | ||
| 231 | If you pass any options they are forwarded to dht-rpc. | |
| 232 | ||
| 233 | #### `await node.unannounce(topic, keyPair, [options])` | |
| 234 | ||
| 235 | Unannounce a key-pair. | |
| 236 | ||
| 237 | If you pass any options they are forwarded to dht-rpc. | |
| 238 | ||
| 239 | ## Mutable/immutable records | |
| 240 | ||
| 241 | #### `const { hash, closestNodes } = await node.immutablePut(value, [options])` | |
| 242 | ||
| 243 | Store an immutable value in the DHT. When successful, the hash of the value is returned. | |
| 244 | ||
| 245 | If you pass any options they are forwarded to dht-rpc. | |
| 246 | ||
| 247 | #### `const { value, from } = await node.immutableGet(hash, [options])` | |
| 248 | ||
| 249 | Fetch an immutable value from the DHT. When successful, it returns the value corresponding to the hash. | |
| 250 | ||
| 251 | If you pass any options they are forwarded to dht-rpc. | |
| 252 | ||
| 253 | #### `const { publicKey, closestNodes, seq, signature } = await node.mutablePut(keyPair, value, [options])` | |
| 254 | ||
| 255 | Store a mutable value in the DHT. | |
| 256 | ||
| 257 | If you pass any options they are forwarded to dht-rpc. | |
| 258 | ||
| 259 | #### `const { value, from, seq, signature } = await node.mutableGet(publicKey, [options])` | |
| 260 | ||
| 261 | Fetch a mutable value from the DHT. | |
| 262 | ||
| 263 | Options: | |
| 264 | ||
| 265 | - `seq` - OPTIONAL, default `0`, a number which will only return values with corresponding `seq` values that are greater than or equal to the supplied `seq` option. | |
| 266 | - `latest` - OPTIONAL - default `false`, a boolean indicating whether the query should try to find the highest seq before returning, or just the first verified value larger than `options.seq` it sees. | |
| 267 | ||
| 268 | Any additional options you pass are forwarded to dht-rpc. | |
| 269 | ||
| 270 | ## Additional API | |
| 271 | ||
| 272 | See [dht-rpc](https://github.com/mafintosh/dht-rpc) for the additional APIs the DHT exposes. | |
| 273 | ||
| 274 | ## CLI | |
| 275 | ||
| 276 | You can start a DHT node in the command line: | |
| 277 | ||
| 278 | ```sh | |
| 279 | npm install -g hyperdht | |
| 280 | ``` | |
| 281 | ||
| 282 | Run a DHT node: | |
| 283 | ||
| 284 | ```sh | |
| 285 | hyperdht # [--port 0] [--host 0.0.0.0] [--bootstrap <comma separated list of ip:port>] | |
| 286 | ``` | |
| 287 | ||
| 288 | Or run multiple nodes: | |
| 289 | ||
| 290 | ```sh | |
| 291 | hyperdht --nodes 5 # [--host 0.0.0.0] [--bootstrap <list>] | |
| 292 | ``` | |
| 293 | ||
| 294 | Note: by default it uses the [mainnet bootstrap nodes](lib/constants.js). | |
| 295 | ||
| 296 | #### Isolated DHT network | |
| 297 | ||
| 298 | To create your own DHT network is as follows: | |
| 299 | ||
| 300 | 1. Run your first bootstrap node: | |
| 301 | ||
| 302 | ```sh | |
| 303 | hyperdht --bootstrap --host (server-ip) # [--port 49737] | |
| 304 | ``` | |
| 305 | ||
| 306 | Important: it requires the port to be open. | |
| 307 | ||
| 308 | Now your bootstrap node is ready to use at `(server-ip):49737`, for example: | |
| 309 | ||
| 310 | ```js | |
| 311 | const dht = new DHT({ bootstrap: ['(server-ip):49737'] }) | |
| 312 | ``` | |
| 313 | ||
| 314 | Note: You could configure some DNS for the bootstrap IP addresses. | |
| 315 | ||
| 316 | For the network to be fully operational it needs at least one persistent node. | |
| 317 | ||
| 318 | 2. Provide the first node by using your own bootstrap values: | |
| 319 | ||
| 320 | ```sh | |
| 321 | hyperdht --port 49738 --bootstrap (server-ip):49737 | |
| 322 | ``` | |
| 323 | ||
| 324 | Important: it requires the port to be open too. | |
| 325 | ||
| 326 | You need to wait ~30 mins for the node to become persistent. | |
| 327 | ||
| 328 | Having persistent nodes in different places makes the network more decentralized and resilient! | |
| 329 | ||
| 330 | For more information: [`examples/isolated-dht.mjs`](examples/isolated-dht.mjs) | |
| 331 | ||
| 332 | ## License | |
| 333 | ||
| 334 | MIT |
| … | … | @@ -0,0 +1,83 @@ |
| 1 | #!/usr/bin/env node | |
| 2 | ||
| 3 | const HyperDHT = require('./') | |
| 4 | ||
| 5 | const bootstrap = arg('bootstrap') | |
| 6 | const nodes = arg('node') ? '' : arg('nodes') | |
| 7 | ||
| 8 | const isBootstrap = bootstrap === '' || (bootstrap !== null && bootstrap.startsWith('--')) | |
| 9 | ||
| 10 | if (isBootstrap) { | |
| 11 | const port = Number(arg('port') || '0') || 49737 | |
| 12 | const host = arg('host') | |
| 13 | if (!host) throw new Error('You need to specify --host <node ip>') | |
| 14 | startBootstrapNode(port, host) | |
| 15 | } else { | |
| 16 | startNodes(Number(nodes) || 1, bootstrap ? bootstrap.split(',') : undefined) | |
| 17 | } | |
| 18 | ||
| 19 | function arg(name) { | |
| 20 | const i = process.argv.indexOf('--' + name) | |
| 21 | if (i === -1) return null | |
| 22 | return i < process.argv.length - 1 ? process.argv[i + 1] : '' | |
| 23 | } | |
| 24 | ||
| 25 | async function startBootstrapNode(port, host) { | |
| 26 | console.log('Starting DHT bootstrap node...') | |
| 27 | ||
| 28 | const node = HyperDHT.bootstrapper(port, host) | |
| 29 | await node.ready() | |
| 30 | ||
| 31 | node.on('close', function () { | |
| 32 | console.log('Bootstrap node closed') | |
| 33 | }) | |
| 34 | ||
| 35 | console.log('Bootstrap node bound to', node.address()) | |
| 36 | console.log('Fully started Hyperswarm DHT bootstrap node') | |
| 37 | ||
| 38 | process.once('SIGINT', function () { | |
| 39 | node.destroy() | |
| 40 | }) | |
| 41 | } | |
| 42 | ||
| 43 | async function startNodes(cnt, bootstrap) { | |
| 44 | console.log('Booting DHT nodes...') | |
| 45 | ||
| 46 | const port = Number(arg('port') || '0') || 0 | |
| 47 | const host = arg('host') || undefined | |
| 48 | const all = [] | |
| 49 | ||
| 50 | if (port && cnt !== 1) throw new Error('--port is only valid when running a single node') | |
| 51 | ||
| 52 | while (all.length < cnt) { | |
| 53 | const node = new HyperDHT({ host, port, anyPort: !port, bootstrap }) | |
| 54 | await node.ready() | |
| 55 | ||
| 56 | all.push(node) | |
| 57 | ||
| 58 | const id = all.push(node) - 1 | |
| 59 | console.log('Node #' + id + ' bound to', node.address()) | |
| 60 | ||
| 61 | node.on('ephemeral', function () { | |
| 62 | console.log('Node #' + id + ' is ephemeral', node.address()) | |
| 63 | }) | |
| 64 | ||
| 65 | node.on('persistent', function () { | |
| 66 | console.log('Node #' + id + ' is persistent, joining remote routing tables', node.address()) | |
| 67 | }) | |
| 68 | ||
| 69 | node.on('close', function () { | |
| 70 | console.log('Node #' + id + ' closed') | |
| 71 | }) | |
| 72 | } | |
| 73 | ||
| 74 | console.log('Fully started ' + cnt + ' Hyperswarm DHT node' + (cnt === 1 ? '' : 's')) | |
| 75 | ||
| 76 | process.once('SIGINT', function () { | |
| 77 | console.log('Shutting down nodes...') | |
| 78 | ||
| 79 | for (const node of all) { | |
| 80 | node.destroy() | |
| 81 | } | |
| 82 | }) | |
| 83 | } |
| … | … | @@ -0,0 +1,15 @@ |
| 1 | const { hash, createKeyPair } = require('./lib/crypto') | |
| 2 | ||
| 3 | module.exports = class Stub { | |
| 4 | constructor() { | |
| 5 | throw new Error('hyperdht is not supported in browsers') | |
| 6 | } | |
| 7 | ||
| 8 | static keyPair(seed) { | |
| 9 | return createKeyPair(seed) | |
| 10 | } | |
| 11 | ||
| 12 | static hash(data) { | |
| 13 | return hash(data) | |
| 14 | } | |
| 15 | } |
| … | … | @@ -0,0 +1,620 @@ |
| 1 | const DHT = require('dht-rpc') | |
| 2 | const sodium = require('sodium-universal') | |
| 3 | const c = require('compact-encoding') | |
| 4 | const b4a = require('b4a') | |
| 5 | const safetyCatch = require('safety-catch') | |
| 6 | const m = require('./lib/messages') | |
| 7 | const SocketPool = require('./lib/socket-pool') | |
| 8 | const Persistent = require('./lib/persistent') | |
| 9 | const Router = require('./lib/router') | |
| 10 | const Cache = require('xache') | |
| 11 | const Server = require('./lib/server') | |
| 12 | const connect = require('./lib/connect') | |
| 13 | const { FIREWALL, BOOTSTRAP_NODES, KNOWN_NODES, COMMANDS } = require('./lib/constants') | |
| 14 | const { hash, createKeyPair } = require('./lib/crypto') | |
| 15 | const { decode } = require('hypercore-id-encoding') | |
| 16 | const RawStreamSet = require('./lib/raw-stream-set') | |
| 17 | const ConnectionPool = require('./lib/connection-pool') | |
| 18 | const { STREAM_NOT_CONNECTED } = require('./lib/errors') | |
| 19 | ||
| 20 | const DEFAULTS = { | |
| 21 | ...DHT.DEFAULTS, | |
| 22 | connectionKeepAlive: 5000, | |
| 23 | randomPunchInterval: 20000 | |
| 24 | } | |
| 25 | ||
| 26 | class HyperDHT extends DHT { | |
| 27 | constructor(opts = {}) { | |
| 28 | const port = opts.port || 49737 | |
| 29 | const bootstrap = opts.bootstrap || BOOTSTRAP_NODES | |
| 30 | const nodes = opts.nodes || KNOWN_NODES | |
| 31 | ||
| 32 | super({ ...opts, port, bootstrap, nodes, filterNode }) | |
| 33 | ||
| 34 | const { router, relayAddresses, persistent } = defaultCacheOpts(opts) | |
| 35 | ||
| 36 | this.defaultKeyPair = opts.keyPair || createKeyPair(opts.seed) | |
| 37 | this.listening = new Set() | |
| 38 | this.connectionKeepAlive = | |
| 39 | opts.connectionKeepAlive === false | |
| 40 | ? 0 | |
| 41 | : opts.connectionKeepAlive || DEFAULTS.connectionKeepAlive | |
| 42 | ||
| 43 | // stats is inherited from dht-rpc so fwd the ones from there | |
| 44 | this.stats = { | |
| 45 | punches: { consistent: 0, random: 0, open: 0 }, | |
| 46 | relaying: { attempts: 0, successes: 0, aborts: 0 }, | |
| 47 | ...this.stats | |
| 48 | } | |
| 49 | this.rawStreams = new RawStreamSet(this) | |
| 50 | ||
| 51 | this._router = new Router(this, router) | |
| 52 | this._socketPool = new SocketPool(this, opts.host || '0.0.0.0') | |
| 53 | this._persistent = null | |
| 54 | this._validatedLocalAddresses = new Map() | |
| 55 | this._relayAddressesCache = new Cache(relayAddresses) | |
| 56 | ||
| 57 | this._deferRandomPunch = !!opts.deferRandomPunch | |
| 58 | this._lastRandomPunch = this._deferRandomPunch ? Date.now() : 0 | |
| 59 | this._connectable = true | |
| 60 | this._randomPunchInterval = opts.randomPunchInterval || DEFAULTS.randomPunchInterval // min 20s between random punches... | |
| 61 | this._randomPunches = 0 | |
| 62 | this._randomPunchLimit = 1 // set to one for extra safety for now | |
| 63 | ||
| 64 | this.once('persistent', () => { | |
| 65 | this._persistent = new Persistent(this, persistent) | |
| 66 | }) | |
| 67 | ||
| 68 | this.on('network-change', () => { | |
| 69 | for (const server of this.listening) server.refresh() | |
| 70 | }) | |
| 71 | ||
| 72 | this.on('network-update', () => { | |
| 73 | if (!this.online) return | |
| 74 | for (const server of this.listening) server.notifyOnline() | |
| 75 | }) | |
| 76 | } | |
| 77 | ||
| 78 | static DEFAULTS = DEFAULTS | |
| 79 | ||
| 80 | connect(remotePublicKey, opts) { | |
| 81 | return connect(this, decode(remotePublicKey), opts) | |
| 82 | } | |
| 83 | ||
| 84 | createServer(opts, onconnection) { | |
| 85 | if (typeof opts === 'function') return this.createServer({}, opts) | |
| 86 | if (opts && opts.onconnection) onconnection = opts.onconnection | |
| 87 | const s = new Server(this, opts) | |
| 88 | if (onconnection) s.on('connection', onconnection) | |
| 89 | return s | |
| 90 | } | |
| 91 | ||
| 92 | pool() { | |
| 93 | return new ConnectionPool(this) | |
| 94 | } | |
| 95 | ||
| 96 | async resume({ log = noop } = {}) { | |
| 97 | if (this._deferRandomPunch) this._lastRandomPunch = Date.now() | |
| 98 | await super.resume({ log }) | |
| 99 | const resuming = [] | |
| 100 | for (const server of this.listening) resuming.push(server.resume()) | |
| 101 | log('Resuming hyperdht servers') | |
| 102 | await Promise.allSettled(resuming) | |
| 103 | log('Done, hyperdht fully resumed') | |
| 104 | } | |
| 105 | ||
| 106 | async suspend({ log = noop } = {}) { | |
| 107 | this._connectable = false // just so nothing gets connected during suspension | |
| 108 | const suspending = [] | |
| 109 | for (const server of this.listening) suspending.push(server.suspend()) | |
| 110 | log('Suspending all hyperdht servers') | |
| 111 | await Promise.allSettled(suspending) | |
| 112 | log('Done, clearing all raw streams') | |
| 113 | await this.rawStreams.clear() | |
| 114 | log('Done, suspending dht-rpc') | |
| 115 | await super.suspend({ log }) | |
| 116 | log('Done, clearing raw streams again') | |
| 117 | await this.rawStreams.clear() | |
| 118 | log('Done, hyperdht fully suspended') | |
| 119 | this._connectable = true | |
| 120 | } | |
| 121 | ||
| 122 | async destroy({ force = false } = {}) { | |
| 123 | if (!force) { | |
| 124 | const closing = [] | |
| 125 | for (const server of this.listening) closing.push(server.close()) | |
| 126 | await Promise.allSettled(closing) | |
| 127 | } | |
| 128 | this._router.destroy() | |
| 129 | if (this._persistent) this._persistent.destroy() | |
| 130 | await this.rawStreams.clear() | |
| 131 | await this._socketPool.destroy() | |
| 132 | await super.destroy() | |
| 133 | } | |
| 134 | ||
| 135 | async validateLocalAddresses(addresses) { | |
| 136 | const list = [] | |
| 137 | const socks = [] | |
| 138 | const waiting = [] | |
| 139 | ||
| 140 | for (const addr of addresses) { | |
| 141 | const { host } = addr | |
| 142 | ||
| 143 | if (this._validatedLocalAddresses.has(host)) { | |
| 144 | if (await this._validatedLocalAddresses.get(host)) { | |
| 145 | list.push(addr) | |
| 146 | } | |
| 147 | continue | |
| 148 | } | |
| 149 | ||
| 150 | const sock = this.udx.createSocket() | |
| 151 | try { | |
| 152 | sock.bind(0, host) | |
| 153 | } catch { | |
| 154 | this._validatedLocalAddresses.set(host, Promise.resolve(false)) | |
| 155 | continue | |
| 156 | } | |
| 157 | ||
| 158 | socks.push(sock) | |
| 159 | ||
| 160 | // semi terrible heuristic until we proper fix local connections by racing them to the remote... | |
| 161 | const promise = new Promise((resolve) => { | |
| 162 | sock.on('message', () => resolve(true)) | |
| 163 | setTimeout(() => resolve(false), 500) | |
| 164 | sock.trySend(b4a.alloc(1), sock.address().port, addr.host) | |
| 165 | }) | |
| 166 | ||
| 167 | this._validatedLocalAddresses.set(host, promise) | |
| 168 | waiting.push(addr) | |
| 169 | } | |
| 170 | ||
| 171 | for (const addr of waiting) { | |
| 172 | const { host } = addr | |
| 173 | if (this._validatedLocalAddresses.has(host)) { | |
| 174 | if (await this._validatedLocalAddresses.get(host)) { | |
| 175 | list.push(addr) | |
| 176 | } | |
| 177 | continue | |
| 178 | } | |
| 179 | } | |
| 180 | ||
| 181 | for (const sock of socks) await sock.close() | |
| 182 | ||
| 183 | return list | |
| 184 | } | |
| 185 | ||
| 186 | findPeer(publicKey, opts = {}) { | |
| 187 | const target = opts.hash === false ? publicKey : hash(publicKey) | |
| 188 | opts = { ...opts, map: mapFindPeer } | |
| 189 | return this.query({ target, command: COMMANDS.FIND_PEER, value: null }, opts) | |
| 190 | } | |
| 191 | ||
| 192 | lookup(target, opts = {}) { | |
| 193 | opts = { ...opts, map: mapLookup } | |
| 194 | return this.query({ target, command: COMMANDS.LOOKUP, value: null }, opts) | |
| 195 | } | |
| 196 | ||
| 197 | lookupAndUnannounce(target, keyPair, opts = {}) { | |
| 198 | const unannounces = [] | |
| 199 | const dht = this | |
| 200 | const userCommit = opts.commit || noop | |
| 201 | const signUnannounce = opts.signUnannounce || Persistent.signUnannounce | |
| 202 | ||
| 203 | if (this._persistent !== null) { | |
| 204 | // unlink self | |
| 205 | this._persistent.unannounce(target, keyPair.publicKey) | |
| 206 | } | |
| 207 | ||
| 208 | opts = { ...opts, map, commit } | |
| 209 | return this.query({ target, command: COMMANDS.LOOKUP, value: null }, opts) | |
| 210 | ||
| 211 | async function commit(reply, dht, query) { | |
| 212 | await Promise.all(unannounces) // can never fail, caught below | |
| 213 | return userCommit(reply, dht, query) | |
| 214 | } | |
| 215 | ||
| 216 | function map(reply) { | |
| 217 | const data = mapLookup(reply) | |
| 218 | ||
| 219 | if (!data || !data.token) return data | |
| 220 | ||
| 221 | let found = data.peers.length >= 20 | |
| 222 | for (let i = 0; !found && i < data.peers.length; i++) { | |
| 223 | found = b4a.equals(data.peers[i].publicKey, keyPair.publicKey) | |
| 224 | } | |
| 225 | ||
| 226 | if (!found) return data | |
| 227 | ||
| 228 | if (!data.from.id) return data | |
| 229 | ||
| 230 | unannounces.push( | |
| 231 | dht | |
| 232 | ._requestUnannounce(keyPair, dht, target, data.token, data.from, signUnannounce) | |
| 233 | .catch(safetyCatch) | |
| 234 | ) | |
| 235 | ||
| 236 | return data | |
| 237 | } | |
| 238 | } | |
| 239 | ||
| 240 | unannounce(target, keyPair, opts = {}) { | |
| 241 | return this.lookupAndUnannounce(target, keyPair, opts).finished() | |
| 242 | } | |
| 243 | ||
| 244 | announce(target, keyPair, relayAddresses, opts = {}) { | |
| 245 | const signAnnounce = opts.signAnnounce || Persistent.signAnnounce | |
| 246 | const bump = opts.bump || 0 | |
| 247 | ||
| 248 | opts = { ...opts, commit } | |
| 249 | ||
| 250 | return opts.clear ? this.lookupAndUnannounce(target, keyPair, opts) : this.lookup(target, opts) | |
| 251 | ||
| 252 | function commit(reply, dht) { | |
| 253 | return dht._requestAnnounce( | |
| 254 | keyPair, | |
| 255 | dht, | |
| 256 | target, | |
| 257 | reply.token, | |
| 258 | reply.from, | |
| 259 | relayAddresses, | |
| 260 | signAnnounce, | |
| 261 | bump | |
| 262 | ) | |
| 263 | } | |
| 264 | } | |
| 265 | ||
| 266 | async immutableGet(target, opts = {}) { | |
| 267 | opts = { ...opts, map: mapImmutable } | |
| 268 | ||
| 269 | const query = this.query({ target, command: COMMANDS.IMMUTABLE_GET, value: null }, opts) | |
| 270 | const check = b4a.allocUnsafe(32) | |
| 271 | ||
| 272 | for await (const node of query) { | |
| 273 | const { value } = node | |
| 274 | sodium.crypto_generichash(check, value) | |
| 275 | if (b4a.equals(check, target)) return node | |
| 276 | } | |
| 277 | ||
| 278 | return null | |
| 279 | } | |
| 280 | ||
| 281 | async immutablePut(value, opts = {}) { | |
| 282 | const target = b4a.allocUnsafe(32) | |
| 283 | sodium.crypto_generichash(target, value) | |
| 284 | ||
| 285 | opts = { | |
| 286 | ...opts, | |
| 287 | map: mapImmutable, | |
| 288 | commit(reply, dht) { | |
| 289 | return dht.request( | |
| 290 | { token: reply.token, target, command: COMMANDS.IMMUTABLE_PUT, value }, | |
| 291 | reply.from | |
| 292 | ) | |
| 293 | } | |
| 294 | } | |
| 295 | ||
| 296 | const query = this.query({ target, command: COMMANDS.IMMUTABLE_GET, value: null }, opts) | |
| 297 | await query.finished() | |
| 298 | ||
| 299 | return { hash: target, closestNodes: query.closestNodes } | |
| 300 | } | |
| 301 | ||
| 302 | async mutableGet(publicKey, opts = {}) { | |
| 303 | let refresh = opts.refresh || null | |
| 304 | let signed = null | |
| 305 | let result = null | |
| 306 | ||
| 307 | opts = { ...opts, map: mapMutable, commit: refresh ? commit : null } | |
| 308 | ||
| 309 | const target = b4a.allocUnsafe(32) | |
| 310 | sodium.crypto_generichash(target, publicKey) | |
| 311 | ||
| 312 | const userSeq = opts.seq || 0 | |
| 313 | const query = this.query( | |
| 314 | { target, command: COMMANDS.MUTABLE_GET, value: c.encode(c.uint, userSeq) }, | |
| 315 | opts | |
| 316 | ) | |
| 317 | const latest = opts.latest !== false | |
| 318 | ||
| 319 | for await (const node of query) { | |
| 320 | if (result && node.seq <= result.seq) continue | |
| 321 | if ( | |
| 322 | node.seq < userSeq || | |
| 323 | !Persistent.verifyMutable(node.signature, node.seq, node.value, publicKey) | |
| 324 | ) | |
| 325 | continue | |
| 326 | if (!latest) return node | |
| 327 | if (!result || node.seq > result.seq) result = node | |
| 328 | } | |
| 329 | ||
| 330 | return result | |
| 331 | ||
| 332 | function commit(reply, dht) { | |
| 333 | if (!signed && result && refresh) { | |
| 334 | if (refresh(result)) { | |
| 335 | signed = c.encode(m.mutablePutRequest, { | |
| 336 | publicKey, | |
| 337 | seq: result.seq, | |
| 338 | value: result.value, | |
| 339 | signature: result.signature | |
| 340 | }) | |
| 341 | } else { | |
| 342 | refresh = null | |
| 343 | } | |
| 344 | } | |
| 345 | ||
| 346 | return signed | |
| 347 | ? dht.request( | |
| 348 | { token: reply.token, target, command: COMMANDS.MUTABLE_PUT, value: signed }, | |
| 349 | reply.from | |
| 350 | ) | |
| 351 | : Promise.resolve(null) | |
| 352 | } | |
| 353 | } | |
| 354 | ||
| 355 | async mutablePut(keyPair, value, opts = {}) { | |
| 356 | const signMutable = opts.signMutable || Persistent.signMutable | |
| 357 | ||
| 358 | const target = b4a.allocUnsafe(32) | |
| 359 | sodium.crypto_generichash(target, keyPair.publicKey) | |
| 360 | ||
| 361 | const seq = opts.seq || 0 | |
| 362 | const signature = await signMutable(seq, value, keyPair) | |
| 363 | ||
| 364 | const signed = c.encode(m.mutablePutRequest, { | |
| 365 | publicKey: keyPair.publicKey, | |
| 366 | seq, | |
| 367 | value, | |
| 368 | signature | |
| 369 | }) | |
| 370 | ||
| 371 | opts = { | |
| 372 | ...opts, | |
| 373 | map: mapMutable, | |
| 374 | commit(reply, dht) { | |
| 375 | return dht.request( | |
| 376 | { token: reply.token, target, command: COMMANDS.MUTABLE_PUT, value: signed }, | |
| 377 | reply.from | |
| 378 | ) | |
| 379 | } | |
| 380 | } | |
| 381 | ||
| 382 | // use seq = 0, for the query part here, as we don't care about the actual values | |
| 383 | const query = this.query( | |
| 384 | { target, command: COMMANDS.MUTABLE_GET, value: c.encode(c.uint, 0) }, | |
| 385 | opts | |
| 386 | ) | |
| 387 | await query.finished() | |
| 388 | ||
| 389 | return { publicKey: keyPair.publicKey, closestNodes: query.closestNodes, seq, signature } | |
| 390 | } | |
| 391 | ||
| 392 | onrequest(req) { | |
| 393 | switch (req.command) { | |
| 394 | case COMMANDS.PEER_HANDSHAKE: { | |
| 395 | this._router.onpeerhandshake(req) | |
| 396 | return true | |
| 397 | } | |
| 398 | case COMMANDS.PEER_HOLEPUNCH: { | |
| 399 | this._router.onpeerholepunch(req) | |
| 400 | return true | |
| 401 | } | |
| 402 | } | |
| 403 | ||
| 404 | if (this._persistent === null || this.id === null) return false | |
| 405 | ||
| 406 | switch (req.command) { | |
| 407 | case COMMANDS.FIND_PEER: { | |
| 408 | this._persistent.onfindpeer(req) | |
| 409 | return true | |
| 410 | } | |
| 411 | case COMMANDS.LOOKUP: { | |
| 412 | this._persistent.onlookup(req) | |
| 413 | return true | |
| 414 | } | |
| 415 | case COMMANDS.ANNOUNCE: { | |
| 416 | this._persistent.onannounce(req) | |
| 417 | return true | |
| 418 | } | |
| 419 | case COMMANDS.UNANNOUNCE: { | |
| 420 | this._persistent.onunannounce(req) | |
| 421 | return true | |
| 422 | } | |
| 423 | case COMMANDS.MUTABLE_PUT: { | |
| 424 | this._persistent.onmutableput(req) | |
| 425 | return true | |
| 426 | } | |
| 427 | case COMMANDS.MUTABLE_GET: { | |
| 428 | this._persistent.onmutableget(req) | |
| 429 | return true | |
| 430 | } | |
| 431 | case COMMANDS.IMMUTABLE_PUT: { | |
| 432 | this._persistent.onimmutableput(req) | |
| 433 | return true | |
| 434 | } | |
| 435 | case COMMANDS.IMMUTABLE_GET: { | |
| 436 | this._persistent.onimmutableget(req) | |
| 437 | return true | |
| 438 | } | |
| 439 | } | |
| 440 | ||
| 441 | return false | |
| 442 | } | |
| 443 | ||
| 444 | static keyPair(seed) { | |
| 445 | return createKeyPair(seed) | |
| 446 | } | |
| 447 | ||
| 448 | static hash(data) { | |
| 449 | return hash(data) | |
| 450 | } | |
| 451 | ||
| 452 | static connectRawStream(encryptedStream, rawStream, remoteId) { | |
| 453 | const stream = encryptedStream.rawStream | |
| 454 | ||
| 455 | if (!stream.connected) throw STREAM_NOT_CONNECTED() | |
| 456 | ||
| 457 | rawStream.connect(stream.socket, remoteId, stream.remotePort, stream.remoteHost) | |
| 458 | } | |
| 459 | ||
| 460 | createRawStream(opts) { | |
| 461 | return this.rawStreams.add(opts) | |
| 462 | } | |
| 463 | ||
| 464 | async _requestAnnounce(keyPair, dht, target, token, from, relayAddresses, sign, bump) { | |
| 465 | const ann = { | |
| 466 | peer: { | |
| 467 | publicKey: keyPair.publicKey, | |
| 468 | relayAddresses: relayAddresses || [] | |
| 469 | }, | |
| 470 | refresh: null, | |
| 471 | signature: null, | |
| 472 | bump | |
| 473 | } | |
| 474 | ||
| 475 | ann.signature = await sign(target, token, from.id, ann, keyPair) | |
| 476 | ||
| 477 | const value = c.encode(m.announce, ann) | |
| 478 | ||
| 479 | return dht.request( | |
| 480 | { | |
| 481 | token, | |
| 482 | target, | |
| 483 | command: COMMANDS.ANNOUNCE, | |
| 484 | value | |
| 485 | }, | |
| 486 | from | |
| 487 | ) | |
| 488 | } | |
| 489 | ||
| 490 | async _requestUnannounce(keyPair, dht, target, token, from, sign) { | |
| 491 | const unann = { | |
| 492 | peer: { | |
| 493 | publicKey: keyPair.publicKey, | |
| 494 | relayAddresses: [] | |
| 495 | }, | |
| 496 | signature: null | |
| 497 | } | |
| 498 | ||
| 499 | unann.signature = await sign(target, token, from.id, unann, keyPair) | |
| 500 | ||
| 501 | const value = c.encode(m.announce, unann) | |
| 502 | ||
| 503 | return dht.request( | |
| 504 | { | |
| 505 | token, | |
| 506 | target, | |
| 507 | command: COMMANDS.UNANNOUNCE, | |
| 508 | value | |
| 509 | }, | |
| 510 | from | |
| 511 | ) | |
| 512 | } | |
| 513 | } | |
| 514 | ||
| 515 | HyperDHT.BOOTSTRAP = BOOTSTRAP_NODES | |
| 516 | HyperDHT.FIREWALL = FIREWALL | |
| 517 | ||
| 518 | module.exports = HyperDHT | |
| 519 | ||
| 520 | function mapLookup(node) { | |
| 521 | if (!node.value) return null | |
| 522 | ||
| 523 | try { | |
| 524 | const l = c.decode(m.lookupRawReply, node.value) | |
| 525 | ||
| 526 | return { | |
| 527 | token: node.token, | |
| 528 | from: node.from, | |
| 529 | to: node.to, | |
| 530 | peers: l.peers, | |
| 531 | bump: l.bump | |
| 532 | } | |
| 533 | } catch { | |
| 534 | return null | |
| 535 | } | |
| 536 | } | |
| 537 | ||
| 538 | function mapFindPeer(node) { | |
| 539 | if (!node.value) return null | |
| 540 | ||
| 541 | try { | |
| 542 | return { | |
| 543 | token: node.token, | |
| 544 | from: node.from, | |
| 545 | to: node.to, | |
| 546 | peer: c.decode(m.peer, node.value) | |
| 547 | } | |
| 548 | } catch { | |
| 549 | return null | |
| 550 | } | |
| 551 | } | |
| 552 | ||
| 553 | function mapImmutable(node) { | |
| 554 | if (!node.value) return null | |
| 555 | ||
| 556 | return { | |
| 557 | token: node.token, | |
| 558 | from: node.from, | |
| 559 | to: node.to, | |
| 560 | value: node.value | |
| 561 | } | |
| 562 | } | |
| 563 | ||
| 564 | function mapMutable(node) { | |
| 565 | if (!node.value) return null | |
| 566 | ||
| 567 | try { | |
| 568 | const { seq, value, signature } = c.decode(m.mutableGetResponse, node.value) | |
| 569 | ||
| 570 | return { | |
| 571 | token: node.token, | |
| 572 | from: node.from, | |
| 573 | to: node.to, | |
| 574 | seq, | |
| 575 | value, | |
| 576 | signature | |
| 577 | } | |
| 578 | } catch { | |
| 579 | return null | |
| 580 | } | |
| 581 | } | |
| 582 | ||
| 583 | function noop() {} | |
| 584 | ||
| 585 | function filterNode(node) { | |
| 586 | // always skip these testnet nodes that got mixed in by accident, until they get updated | |
| 587 | return ( | |
| 588 | !(node.port === 49738 && (node.host === '134.209.28.98' || node.host === '167.99.142.185')) && | |
| 589 | !(node.port === 9400 && node.host === '35.233.47.252') && | |
| 590 | !(node.host === '150.136.142.116') | |
| 591 | ) | |
| 592 | } | |
| 593 | ||
| 594 | const defaultMaxSize = 65536 | |
| 595 | const defaultMaxAge = 20 * 60 * 1000 // 20 minutes | |
| 596 | ||
| 597 | function defaultCacheOpts(opts) { | |
| 598 | const maxSize = opts.maxSize || defaultMaxSize | |
| 599 | const maxAge = opts.maxAge || defaultMaxAge | |
| 600 | ||
| 601 | return { | |
| 602 | router: { | |
| 603 | forwards: { maxSize, maxAge } | |
| 604 | }, | |
| 605 | relayAddresses: { maxSize: Math.min(maxSize, 512), maxAge: 0 }, | |
| 606 | persistent: { | |
| 607 | records: { maxSize, maxAge }, | |
| 608 | refreshes: { maxSize, maxAge }, | |
| 609 | mutables: { | |
| 610 | maxSize: (maxSize / 2) | 0, | |
| 611 | maxAge: opts.maxAge || 48 * 60 * 60 * 1000 // 48 hours | |
| 612 | }, | |
| 613 | immutables: { | |
| 614 | maxSize: (maxSize / 2) | 0, | |
| 615 | maxAge: opts.maxAge || 48 * 60 * 60 * 1000 // 48 hours | |
| 616 | }, | |
| 617 | bumps: { maxSize, maxAge } | |
| 618 | } | |
| 619 | } | |
| 620 | } |
| … | … | @@ -0,0 +1,303 @@ |
| 1 | const safetyCatch = require('safety-catch') | |
| 2 | const c = require('compact-encoding') | |
| 3 | const Signal = require('signal-promise') | |
| 4 | const { encodeUnslab } = require('./encode') | |
| 5 | const Sleeper = require('./sleeper') | |
| 6 | const m = require('./messages') | |
| 7 | const Persistent = require('./persistent') | |
| 8 | const { COMMANDS } = require('./constants') | |
| 9 | ||
| 10 | const MIN_ACTIVE = 3 | |
| 11 | ||
| 12 | module.exports = class Announcer { | |
| 13 | constructor(dht, keyPair, target, opts = {}) { | |
| 14 | this.dht = dht | |
| 15 | this.keyPair = keyPair | |
| 16 | this.target = target | |
| 17 | this.relays = [] | |
| 18 | this.relayAddresses = [] | |
| 19 | this.stopped = false | |
| 20 | this.suspended = false | |
| 21 | this.record = encodeUnslab(m.peer, { publicKey: keyPair.publicKey, relayAddresses: [] }) | |
| 22 | this.online = new Signal() | |
| 23 | ||
| 24 | this._refreshing = false | |
| 25 | this._closestNodes = null | |
| 26 | this._active = null | |
| 27 | this._sleeper = new Sleeper() | |
| 28 | this._resumed = new Signal() | |
| 29 | this._signAnnounce = opts.signAnnounce || Persistent.signAnnounce | |
| 30 | this._signUnannounce = opts.signUnannounce || Persistent.signUnannounce | |
| 31 | this._updating = null | |
| 32 | this._activeQuery = null | |
| 33 | this._unannouncing = null | |
| 34 | ||
| 35 | this._serverRelays = [new Map(), new Map(), new Map()] | |
| 36 | } | |
| 37 | ||
| 38 | isRelay(addr) { | |
| 39 | const id = addr.host + ':' + addr.port | |
| 40 | const [a, b, c] = this._serverRelays | |
| 41 | return a.has(id) || b.has(id) || c.has(id) | |
| 42 | } | |
| 43 | ||
| 44 | async suspend({ log = noop } = {}) { | |
| 45 | if (this.suspended) return | |
| 46 | this.suspended = true | |
| 47 | ||
| 48 | log('Suspending announcer') | |
| 49 | ||
| 50 | // Suspend has its own sleep logic | |
| 51 | // so we don't want to hang on this one | |
| 52 | this.online.notify() | |
| 53 | ||
| 54 | if (this._activeQuery) this._activeQuery.destroy() | |
| 55 | ||
| 56 | this._sleeper.resume() | |
| 57 | if (this._updating) await this._updating | |
| 58 | log('Suspending announcer (post update)') | |
| 59 | ||
| 60 | if (this.suspended === false || this.stopped) return | |
| 61 | ||
| 62 | log('Suspending announcer (pre unannounce)') | |
| 63 | await this._unannounceCurrent() | |
| 64 | log('Suspending announcer (post unannounce)') | |
| 65 | } | |
| 66 | ||
| 67 | resume() { | |
| 68 | if (!this.suspended) return | |
| 69 | this.suspended = false | |
| 70 | ||
| 71 | this.refresh() | |
| 72 | this._sleeper.resume() | |
| 73 | this._resumed.notify() | |
| 74 | } | |
| 75 | ||
| 76 | refresh() { | |
| 77 | if (this.stopped) return | |
| 78 | this._refreshing = true | |
| 79 | } | |
| 80 | ||
| 81 | async start() { | |
| 82 | if (this.stopped) return | |
| 83 | this._active = this._runUpdate() | |
| 84 | await this._active | |
| 85 | if (this.stopped) return | |
| 86 | this._active = this._background() | |
| 87 | } | |
| 88 | ||
| 89 | async stop() { | |
| 90 | this.stopped = true | |
| 91 | this.online.notify() // Break out of the _background loop if we're offline | |
| 92 | this._sleeper.resume() | |
| 93 | this._resumed.notify() | |
| 94 | await this._active | |
| 95 | await this._unannounceCurrent() | |
| 96 | } | |
| 97 | ||
| 98 | async _unannounceCurrent() { | |
| 99 | while (this._unannouncing !== null) await this._unannouncing | |
| 100 | const un = (this._unannouncing = this._unannounceAll(this._serverRelays[2].values())) | |
| 101 | await this._unannouncing | |
| 102 | if (un === this._unannouncing) this._unannouncing = null | |
| 103 | } | |
| 104 | ||
| 105 | async _background() { | |
| 106 | while (!this.dht.destroyed && !this.stopped) { | |
| 107 | try { | |
| 108 | this._refreshing = false | |
| 109 | ||
| 110 | // ~5min +- | |
| 111 | for (let i = 0; i < 100 && !this.stopped && !this._refreshing && !this.suspended; i++) { | |
| 112 | const pings = [] | |
| 113 | ||
| 114 | for (const node of this._serverRelays[2].values()) { | |
| 115 | pings.push(this.dht.ping(node)) | |
| 116 | } | |
| 117 | ||
| 118 | const active = await resolved(pings) | |
| 119 | if (active < Math.min(pings.length, MIN_ACTIVE)) { | |
| 120 | this.refresh() // we lost too many relay nodes, retry all | |
| 121 | } | |
| 122 | ||
| 123 | if (this.stopped) return | |
| 124 | ||
| 125 | if (!this.suspended && !this._refreshing) await this._sleeper.pause(3000) | |
| 126 | } | |
| 127 | ||
| 128 | while (!this.stopped && this.suspended) await this._resumed.wait() | |
| 129 | ||
| 130 | if (!this.stopped) await this._runUpdate() | |
| 131 | ||
| 132 | while (!this.dht.online && !this.stopped && !this.suspended) { | |
| 133 | // Being offline can make _background repeat very quickly | |
| 134 | // So wait until we're back online | |
| 135 | await this.online.wait() | |
| 136 | } | |
| 137 | } catch (err) { | |
| 138 | safetyCatch(err) | |
| 139 | } | |
| 140 | } | |
| 141 | } | |
| 142 | ||
| 143 | async _runUpdate() { | |
| 144 | this._updating = this._update() | |
| 145 | await this._updating | |
| 146 | this._updating = null | |
| 147 | } | |
| 148 | ||
| 149 | async _update() { | |
| 150 | while (this._unannouncing) await this._unannouncing | |
| 151 | ||
| 152 | this._cycle() | |
| 153 | ||
| 154 | const q = (this._activeQuery = this.dht.findPeer(this.target, { | |
| 155 | hash: false, | |
| 156 | nodes: this._closestNodes | |
| 157 | })) | |
| 158 | ||
| 159 | try { | |
| 160 | await q.finished() | |
| 161 | } catch { | |
| 162 | // ignore failures... | |
| 163 | } | |
| 164 | ||
| 165 | this._activeQuery = null | |
| 166 | ||
| 167 | if (this.stopped || this.suspended) return | |
| 168 | ||
| 169 | const ann = [] | |
| 170 | const replies = pickBest(q.closestReplies) | |
| 171 | ||
| 172 | const relays = [] | |
| 173 | const relayAddresses = [] | |
| 174 | ||
| 175 | if (!this.dht.firewalled) { | |
| 176 | const addr = this.dht.remoteAddress() | |
| 177 | if (addr) relayAddresses.push(addr) | |
| 178 | } | |
| 179 | ||
| 180 | for (const msg of replies) { | |
| 181 | ann.push(this._commit(msg, relays, relayAddresses)) | |
| 182 | } | |
| 183 | ||
| 184 | await Promise.allSettled(ann) | |
| 185 | if (this.stopped || this.suspended) return | |
| 186 | ||
| 187 | this._closestNodes = q.closestNodes | |
| 188 | this.relays = relays | |
| 189 | this.relayAddresses = relayAddresses | |
| 190 | ||
| 191 | const removed = [] | |
| 192 | for (const [key, value] of this._serverRelays[1]) { | |
| 193 | if (!this._serverRelays[2].has(key)) removed.push(value) | |
| 194 | } | |
| 195 | ||
| 196 | await this._unannounceAll(removed) | |
| 197 | } | |
| 198 | ||
| 199 | _unannounceAll(relays) { | |
| 200 | const unann = [] | |
| 201 | for (const r of relays) unann.push(this._unannounce(r)) | |
| 202 | return Promise.allSettled(unann) | |
| 203 | } | |
| 204 | ||
| 205 | async _unannounce(to) { | |
| 206 | const unann = { | |
| 207 | peer: { | |
| 208 | publicKey: this.keyPair.publicKey, | |
| 209 | relayAddresses: [] | |
| 210 | }, | |
| 211 | refresh: null, | |
| 212 | signature: null | |
| 213 | } | |
| 214 | ||
| 215 | const { from, token, value } = await this.dht.request( | |
| 216 | { | |
| 217 | token: null, | |
| 218 | command: COMMANDS.FIND_PEER, | |
| 219 | target: this.target, | |
| 220 | value: null | |
| 221 | }, | |
| 222 | to | |
| 223 | ) | |
| 224 | ||
| 225 | if (!token || !from.id || !value) return | |
| 226 | ||
| 227 | unann.signature = await this._signUnannounce(this.target, token, from.id, unann, this.keyPair) | |
| 228 | ||
| 229 | await this.dht.request( | |
| 230 | { | |
| 231 | token, | |
| 232 | command: COMMANDS.UNANNOUNCE, | |
| 233 | target: this.target, | |
| 234 | value: c.encode(m.announce, unann) | |
| 235 | }, | |
| 236 | to | |
| 237 | ) | |
| 238 | } | |
| 239 | ||
| 240 | async _commit(msg, relays, relayAddresses) { | |
| 241 | const ann = { | |
| 242 | peer: { | |
| 243 | publicKey: this.keyPair.publicKey, | |
| 244 | relayAddresses: [] | |
| 245 | }, | |
| 246 | refresh: null, | |
| 247 | signature: null | |
| 248 | } | |
| 249 | ||
| 250 | ann.signature = await this._signAnnounce(this.target, msg.token, msg.from.id, ann, this.keyPair) | |
| 251 | ||
| 252 | const res = await this.dht.request( | |
| 253 | { | |
| 254 | token: msg.token, | |
| 255 | command: COMMANDS.ANNOUNCE, | |
| 256 | target: this.target, | |
| 257 | value: c.encode(m.announce, ann) | |
| 258 | }, | |
| 259 | msg.from | |
| 260 | ) | |
| 261 | ||
| 262 | if (res.error !== 0) return | |
| 263 | ||
| 264 | if (relayAddresses.length < 3) relayAddresses.push({ host: msg.from.host, port: msg.from.port }) | |
| 265 | relays.push({ relayAddress: msg.from, peerAddress: msg.to }) | |
| 266 | ||
| 267 | this._serverRelays[2].set(msg.from.host + ':' + msg.from.port, msg.from) | |
| 268 | } | |
| 269 | ||
| 270 | _cycle() { | |
| 271 | const tmp = this._serverRelays[0] | |
| 272 | this._serverRelays[0] = this._serverRelays[1] | |
| 273 | this._serverRelays[1] = this._serverRelays[2] | |
| 274 | this._serverRelays[2] = tmp | |
| 275 | tmp.clear() | |
| 276 | } | |
| 277 | } | |
| 278 | ||
| 279 | function resolved(ps) { | |
| 280 | let replied = 0 | |
| 281 | let ticks = ps.length + 1 | |
| 282 | ||
| 283 | return new Promise((resolve) => { | |
| 284 | for (const p of ps) p.then(push, tick) | |
| 285 | tick() | |
| 286 | ||
| 287 | function push(v) { | |
| 288 | replied++ | |
| 289 | tick() | |
| 290 | } | |
| 291 | ||
| 292 | function tick() { | |
| 293 | if (--ticks === 0) resolve(replied) | |
| 294 | } | |
| 295 | }) | |
| 296 | } | |
| 297 | ||
| 298 | function pickBest(replies) { | |
| 299 | // TODO: pick the ones closest to us RTT wise | |
| 300 | return replies.slice(0, 3) | |
| 301 | } | |
| 302 | ||
| 303 | function noop() {} |
| … | … | @@ -0,0 +1,850 @@ |
| 1 | const NoiseSecretStream = require('@hyperswarm/secret-stream') | |
| 2 | const b4a = require('b4a') | |
| 3 | const relay = require('blind-relay') | |
| 4 | const { isReserved, isBogon } = require('bogon') | |
| 5 | const safetyCatch = require('safety-catch') | |
| 6 | const unslab = require('unslab') | |
| 7 | const Semaphore = require('./semaphore') | |
| 8 | const NoiseWrap = require('./noise-wrap') | |
| 9 | const SecurePayload = require('./secure-payload') | |
| 10 | const Holepuncher = require('./holepuncher') | |
| 11 | const Sleeper = require('./sleeper') | |
| 12 | const { FIREWALL, ERROR } = require('./constants') | |
| 13 | const { unslabbedHash } = require('./crypto') | |
| 14 | const { | |
| 15 | CANNOT_HOLEPUNCH, | |
| 16 | HANDSHAKE_INVALID, | |
| 17 | HOLEPUNCH_ABORTED, | |
| 18 | HOLEPUNCH_INVALID, | |
| 19 | HOLEPUNCH_PROBE_TIMEOUT, | |
| 20 | HOLEPUNCH_DOUBLE_RANDOMIZED_NATS, | |
| 21 | PEER_CONNECTION_FAILED, | |
| 22 | PEER_NOT_FOUND, | |
| 23 | REMOTE_ABORTED, | |
| 24 | REMOTE_NOT_HOLEPUNCHABLE, | |
| 25 | REMOTE_NOT_HOLEPUNCHING, | |
| 26 | SERVER_ERROR, | |
| 27 | SERVER_INCOMPATIBLE, | |
| 28 | RELAY_ABORTED, | |
| 29 | SUSPENDED | |
| 30 | } = require('./errors') | |
| 31 | ||
| 32 | module.exports = function connect(dht, publicKey, opts = {}) { | |
| 33 | const pool = opts.pool || null | |
| 34 | ||
| 35 | if (pool && pool.has(publicKey)) return pool.get(publicKey) | |
| 36 | ||
| 37 | publicKey = unslab(publicKey) | |
| 38 | ||
| 39 | const keyPair = opts.keyPair || dht.defaultKeyPair | |
| 40 | const relayThrough = selectRelay(opts.relayThrough || null) | |
| 41 | const encryptedSocket = (opts.createSecretStream || defaultCreateSecretStream)(true, null, { | |
| 42 | publicKey: keyPair.publicKey, | |
| 43 | remotePublicKey: publicKey, | |
| 44 | autoStart: false, | |
| 45 | keepAlive: dht.connectionKeepAlive | |
| 46 | }) | |
| 47 | ||
| 48 | // in case a socket is made during suspended state, destroy it immediately | |
| 49 | if (dht.suspended || !dht._connectable) { | |
| 50 | encryptedSocket.destroy(SUSPENDED()) | |
| 51 | return encryptedSocket | |
| 52 | } | |
| 53 | ||
| 54 | if (pool) pool._attachStream(encryptedSocket, false) | |
| 55 | ||
| 56 | const id = b4a.toString(publicKey, 'hex') | |
| 57 | const c = { | |
| 58 | id, | |
| 59 | dht, | |
| 60 | session: dht.session(), | |
| 61 | relayAddresses: opts.relayAddresses || [], | |
| 62 | remoteRelayAddresses: [], | |
| 63 | pool, | |
| 64 | round: 0, | |
| 65 | target: unslabbedHash(publicKey), | |
| 66 | remotePublicKey: publicKey, | |
| 67 | reusableSocket: !!opts.reusableSocket, | |
| 68 | handshake: (opts.createHandshake || defaultCreateHandshake)(keyPair, publicKey), | |
| 69 | request: null, | |
| 70 | requesting: false, | |
| 71 | lan: opts.localConnection !== false, | |
| 72 | firewall: FIREWALL.UNKNOWN, | |
| 73 | rawStream: dht.createRawStream({ framed: true, firewall }), | |
| 74 | connect: null, | |
| 75 | query: null, | |
| 76 | puncher: null, | |
| 77 | payload: null, | |
| 78 | passiveConnectTimeout: null, | |
| 79 | serverSocket: null, | |
| 80 | serverAddress: null, | |
| 81 | onsocket: null, | |
| 82 | sleeper: new Sleeper(), | |
| 83 | encryptedSocket, | |
| 84 | ||
| 85 | // Relay state | |
| 86 | relayTimeout: null, | |
| 87 | relayThrough, | |
| 88 | relayToken: relayThrough ? relay.token() : null, | |
| 89 | relaySocket: null, | |
| 90 | relayClient: null, | |
| 91 | relayPaired: false, | |
| 92 | relayKeepAlive: opts.relayKeepAlive || 5000 | |
| 93 | } | |
| 94 | ||
| 95 | // If the raw stream receives an error signal pre connect (ie from the firewall hook), make sure | |
| 96 | // to forward that to the encrypted socket for proper teardown | |
| 97 | c.rawStream.on('error', autoDestroy) | |
| 98 | c.rawStream.once('connect', () => { | |
| 99 | c.rawStream.removeListener('error', autoDestroy) | |
| 100 | }) | |
| 101 | ||
| 102 | encryptedSocket.on('close', function () { | |
| 103 | if (c.passiveConnectTimeout) clearPassiveConnectTimeout(c) | |
| 104 | if (c.query) c.query.destroy() | |
| 105 | if (c.puncher) c.puncher.destroy() | |
| 106 | if (c.rawStream) c.rawStream.destroy() | |
| 107 | c.session.destroy() | |
| 108 | c.sleeper.resume() | |
| 109 | }) | |
| 110 | ||
| 111 | // Safe to run in the background - never throws | |
| 112 | if (dht.suspended) encryptedSocket.destroy(SUSPENDED()) | |
| 113 | else connectAndHolepunch(c, opts) | |
| 114 | ||
| 115 | return encryptedSocket | |
| 116 | ||
| 117 | function autoDestroy(err) { | |
| 118 | maybeDestroyEncryptedSocket(c, err) | |
| 119 | } | |
| 120 | ||
| 121 | function firewall(socket, port, host) { | |
| 122 | // Check if the traffic originated from the socket on which we're expecting relay traffic. If so, | |
| 123 | // we haven't hole punched yet and the other side is just sending us traffic through the relay. | |
| 124 | if (c.relaySocket && isRelay(c.relaySocket, socket, port, host)) { | |
| 125 | return false | |
| 126 | } | |
| 127 | ||
| 128 | if (c.onsocket) { | |
| 129 | c.onsocket(socket, port, host) | |
| 130 | } else { | |
| 131 | c.serverSocket = socket | |
| 132 | c.serverAddress = { port, host } | |
| 133 | } | |
| 134 | return false | |
| 135 | } | |
| 136 | } | |
| 137 | ||
| 138 | function isDone(c) { | |
| 139 | // we are destroying or the puncher is connected - done | |
| 140 | if (c.encryptedSocket.destroying || !!(c.puncher && c.puncher.connected)) { | |
| 141 | return true | |
| 142 | } | |
| 143 | // not destroying, but no raw stream - def not done | |
| 144 | if (c.encryptedSocket.rawStream === null) { | |
| 145 | return false | |
| 146 | } | |
| 147 | // we are relayed, but the puncher is not done yet | |
| 148 | if (c.relaySocket && !!(c.puncher && !c.puncher.connected && !c.puncher.destroyed)) { | |
| 149 | return false | |
| 150 | } | |
| 151 | // we are done | |
| 152 | return true | |
| 153 | } | |
| 154 | ||
| 155 | async function retryRoute(c, route) { | |
| 156 | const ref = c.dht._socketPool.lookup(route.socket) | |
| 157 | ||
| 158 | if (!ref) { | |
| 159 | if (route.socket === c.dht.socket) { | |
| 160 | await connectThroughNode(c, route.address, c.dht.socket) | |
| 161 | } | |
| 162 | return | |
| 163 | } | |
| 164 | ||
| 165 | ref.active() | |
| 166 | ||
| 167 | try { | |
| 168 | await connectThroughNode(c, route.address, route.socket) | |
| 169 | } catch { | |
| 170 | // if error, just ignore, and continue through the existing strat | |
| 171 | } | |
| 172 | ||
| 173 | ref.inactive() | |
| 174 | } | |
| 175 | ||
| 176 | async function connectAndHolepunch(c, opts) { | |
| 177 | const route = c.reusableSocket ? c.dht._socketPool.routes.get(c.remotePublicKey) : null | |
| 178 | ||
| 179 | if (route) { | |
| 180 | await retryRoute(c, route) | |
| 181 | if (isDone(c)) return | |
| 182 | } | |
| 183 | ||
| 184 | await findAndConnect(c, opts) | |
| 185 | if (isDone(c)) return | |
| 186 | ||
| 187 | if (!c.connect) { | |
| 188 | // TODO: just a quick fix for now, should retry prob | |
| 189 | maybeDestroyEncryptedSocket(c, HANDSHAKE_INVALID()) | |
| 190 | return | |
| 191 | } | |
| 192 | ||
| 193 | await holepunch(c, opts) | |
| 194 | } | |
| 195 | ||
| 196 | function getFirstRemoteAddress(addrs, serverAddress) { | |
| 197 | for (const addr of addrs) { | |
| 198 | if (isBogon(addr.host)) continue | |
| 199 | return addr | |
| 200 | } | |
| 201 | ||
| 202 | return serverAddress | |
| 203 | } | |
| 204 | ||
| 205 | async function holepunch(c, opts) { | |
| 206 | let { relayAddress, serverAddress, clientAddress, payload } = c.connect | |
| 207 | ||
| 208 | const remoteHolepunchable = !!(payload.holepunch && payload.holepunch.relays.length) | |
| 209 | ||
| 210 | const relayed = diffAddress(serverAddress, relayAddress) | |
| 211 | ||
| 212 | if (payload.firewall === FIREWALL.OPEN || (relayed && !remoteHolepunchable)) { | |
| 213 | const addr = getFirstRemoteAddress(payload.addresses4, serverAddress) | |
| 214 | if (addr) { | |
| 215 | const socket = c.dht.socket | |
| 216 | c.dht.stats.punches.open++ | |
| 217 | c.onsocket(socket, addr.port, addr.host) | |
| 218 | return | |
| 219 | } | |
| 220 | // TODO: check all addresses also obvs | |
| 221 | } | |
| 222 | ||
| 223 | const onabort = () => { | |
| 224 | c.session.destroy() | |
| 225 | maybeDestroyEncryptedSocket(c, HOLEPUNCH_ABORTED()) | |
| 226 | } | |
| 227 | ||
| 228 | if (c.firewall === FIREWALL.OPEN) { | |
| 229 | c.passiveConnectTimeout = setTimeout(onabort, 10000) | |
| 230 | return | |
| 231 | } | |
| 232 | ||
| 233 | // TODO: would be better to just try local addrs in the background whilst continuing with other strategies... | |
| 234 | if (c.lan && relayed && clientAddress.host === serverAddress.host) { | |
| 235 | const serverAddresses = payload.addresses4.filter(onlyNonReserved) | |
| 236 | ||
| 237 | if (serverAddresses.length > 0) { | |
| 238 | const myAddresses = Holepuncher.localAddresses(c.dht.io.serverSocket) | |
| 239 | const addr = Holepuncher.matchAddress(myAddresses, serverAddresses) || serverAddresses[0] | |
| 240 | ||
| 241 | const socket = c.dht.io.serverSocket | |
| 242 | try { | |
| 243 | await c.dht.ping(addr) | |
| 244 | } catch { | |
| 245 | maybeDestroyEncryptedSocket(c, HOLEPUNCH_ABORTED()) | |
| 246 | return | |
| 247 | } | |
| 248 | c.onsocket(socket, addr.port, addr.host) | |
| 249 | return | |
| 250 | } | |
| 251 | } | |
| 252 | ||
| 253 | if (!remoteHolepunchable) { | |
| 254 | maybeDestroyEncryptedSocket(c, CANNOT_HOLEPUNCH()) | |
| 255 | return | |
| 256 | } | |
| 257 | ||
| 258 | c.puncher = new Holepuncher(c.dht, c.session, true, payload.firewall) | |
| 259 | ||
| 260 | c.puncher.onconnect = c.onsocket | |
| 261 | c.puncher.onabort = onabort | |
| 262 | ||
| 263 | const serverRelay = pickServerRelay(payload.holepunch.relays, relayAddress) | |
| 264 | ||
| 265 | // Begin holepunching! | |
| 266 | ||
| 267 | let probe | |
| 268 | try { | |
| 269 | probe = await probeRound(c, opts.fastOpen === false ? null : serverAddress, serverRelay, true) | |
| 270 | } catch (err) { | |
| 271 | destroyPuncher(c) | |
| 272 | // TODO: we should retry here with some of the other relays, bail for now | |
| 273 | maybeDestroyEncryptedSocket(c, err) | |
| 274 | return | |
| 275 | } | |
| 276 | ||
| 277 | if (isDone(c) || !probe) return | |
| 278 | const { token, peerAddress } = probe | |
| 279 | ||
| 280 | // If the relay the server picked is the same as the relay the client picked, | |
| 281 | // then we can use the peerAddress that round one indicates the server wants to use. | |
| 282 | // This shaves off a roundtrip if the server chose to reroll its socket due to some NAT | |
| 283 | // issue with the first one it picked (ie mobile nat inconsistencies...). | |
| 284 | // If the relays were different, then the server would not have a UDP session open on this address | |
| 285 | // to the client relay, which round2 uses. | |
| 286 | if ( | |
| 287 | !diffAddress(serverRelay.relayAddress, relayAddress) && | |
| 288 | diffAddress(serverAddress, peerAddress) | |
| 289 | ) { | |
| 290 | serverAddress = peerAddress | |
| 291 | await c.puncher.openSession(serverAddress) | |
| 292 | if (isDone(c)) return | |
| 293 | } | |
| 294 | ||
| 295 | // TODO: still continue here if a local connection might work, but then do not holepunch... | |
| 296 | if ( | |
| 297 | opts.holepunch && | |
| 298 | !opts.holepunch( | |
| 299 | c.puncher.remoteFirewall, | |
| 300 | c.puncher.nat.firewall, | |
| 301 | c.puncher.remoteAddresses, | |
| 302 | c.puncher.nat.addresses | |
| 303 | ) | |
| 304 | ) { | |
| 305 | await abort(c, serverRelay, HOLEPUNCH_ABORTED('Client aborted holepunch')) | |
| 306 | return | |
| 307 | } | |
| 308 | ||
| 309 | try { | |
| 310 | await roundPunch(c, serverAddress, token, relayAddress, serverRelay, false) | |
| 311 | } catch (err) { | |
| 312 | destroyPuncher(c) | |
| 313 | // TODO: retry with another relay? | |
| 314 | maybeDestroyEncryptedSocket(c, err) | |
| 315 | } | |
| 316 | } | |
| 317 | ||
| 318 | async function findAndConnect(c, opts) { | |
| 319 | let attempts = 0 | |
| 320 | let closestNodes = opts.relayAddresses && opts.relayAddresses.length ? opts.relayAddresses : null | |
| 321 | ||
| 322 | if (!closestNodes) { | |
| 323 | const cachedRelayAddresses = c.dht._relayAddressesCache.get(c.id) | |
| 324 | if (cachedRelayAddresses) closestNodes = cachedRelayAddresses | |
| 325 | } | |
| 326 | ||
| 327 | if (c.dht._persistent) { | |
| 328 | // check if we know the route ourself... | |
| 329 | const route = c.dht._router.get(c.target) | |
| 330 | if (route && route.relay !== null) { | |
| 331 | closestNodes = [{ host: route.relay.host, port: route.relay.port }] | |
| 332 | } | |
| 333 | } | |
| 334 | ||
| 335 | // 2 is how many parallel connect attempts we want to do, we can make this configurable | |
| 336 | const sem = new Semaphore(2) | |
| 337 | const signal = sem.signal.bind(sem) | |
| 338 | const tries = closestNodes !== null ? 2 : 1 | |
| 339 | ||
| 340 | try { | |
| 341 | for (let i = 0; i < tries && !isDone(c) && !c.connect; i++) { | |
| 342 | c.query = c.dht.findPeer(c.target, { | |
| 343 | hash: false, | |
| 344 | session: c.session, | |
| 345 | closestNodes, | |
| 346 | onlyClosestNodes: closestNodes !== null, | |
| 347 | retries: closestNodes ? 1 : 3 | |
| 348 | }) | |
| 349 | ||
| 350 | for await (const data of c.query) { | |
| 351 | await sem.wait() | |
| 352 | if (isDone(c)) return | |
| 353 | ||
| 354 | if (c.connect) { | |
| 355 | sem.signal() | |
| 356 | break | |
| 357 | } | |
| 358 | ||
| 359 | c.remoteRelayAddresses.push(data.from) | |
| 360 | attempts++ | |
| 361 | connectThroughNode(c, data.from, null).then(signal, signal) | |
| 362 | } | |
| 363 | ||
| 364 | closestNodes = null | |
| 365 | ||
| 366 | if (attempts > 0) await sem.flush() | |
| 367 | } | |
| 368 | ||
| 369 | c.query = null | |
| 370 | if (isDone(c)) return | |
| 371 | ||
| 372 | // flush the semaphore | |
| 373 | await sem.flush() | |
| 374 | if (isDone(c)) return | |
| 375 | } catch (err) { | |
| 376 | c.query = null | |
| 377 | maybeDestroyEncryptedSocket(c, err) | |
| 378 | return | |
| 379 | } | |
| 380 | ||
| 381 | if (!c.connect) { | |
| 382 | maybeDestroyEncryptedSocket(c, attempts ? PEER_CONNECTION_FAILED() : PEER_NOT_FOUND()) | |
| 383 | } | |
| 384 | } | |
| 385 | ||
| 386 | async function connectThroughNode(c, address, socket) { | |
| 387 | if (!c.requesting) { | |
| 388 | // If we have a stable server address, send it over now | |
| 389 | const addr = c.dht.remoteAddress() | |
| 390 | const localAddrs = c.lan ? Holepuncher.localAddresses(c.dht.io.serverSocket) : null | |
| 391 | const addresses4 = [] | |
| 392 | ||
| 393 | if (addr) addresses4.push(addr) | |
| 394 | if (localAddrs) addresses4.push(...localAddrs) | |
| 395 | ||
| 396 | c.firewall = addr ? FIREWALL.OPEN : FIREWALL.UNKNOWN | |
| 397 | c.requesting = true | |
| 398 | c.request = await c.handshake.send({ | |
| 399 | error: ERROR.NONE, | |
| 400 | firewall: c.firewall, | |
| 401 | holepunch: null, | |
| 402 | addresses4, | |
| 403 | addresses6: [], | |
| 404 | udx: { | |
| 405 | reusableSocket: c.reusableSocket, | |
| 406 | id: c.rawStream.id, | |
| 407 | seq: 0 | |
| 408 | }, | |
| 409 | secretStream: {}, | |
| 410 | relayThrough: c.relayThrough ? { publicKey: c.relayThrough, token: c.relayToken } : null | |
| 411 | }) | |
| 412 | if (isDone(c)) return | |
| 413 | } | |
| 414 | ||
| 415 | const { serverAddress, clientAddress, relayed, noise } = await c.dht._router.peerHandshake( | |
| 416 | c.target, | |
| 417 | { noise: c.request, socket, session: c.session }, | |
| 418 | address | |
| 419 | ) | |
| 420 | if (isDone(c) || c.connect) return | |
| 421 | ||
| 422 | const payload = await c.handshake.recv(noise) | |
| 423 | if (isDone(c) || !payload) return | |
| 424 | ||
| 425 | if (payload.version !== 1) { | |
| 426 | maybeDestroyEncryptedSocket(c, SERVER_INCOMPATIBLE()) | |
| 427 | return | |
| 428 | } | |
| 429 | if (payload.error !== ERROR.NONE) { | |
| 430 | maybeDestroyEncryptedSocket(c, SERVER_ERROR()) | |
| 431 | return | |
| 432 | } | |
| 433 | if (!payload.udx) { | |
| 434 | maybeDestroyEncryptedSocket(c, SERVER_ERROR('Server did not send UDX data')) | |
| 435 | return | |
| 436 | } | |
| 437 | ||
| 438 | const hs = c.handshake.final() | |
| 439 | ||
| 440 | c.handshake = null | |
| 441 | c.request = null | |
| 442 | c.requesting = false | |
| 443 | c.connect = { | |
| 444 | relayed, | |
| 445 | relayAddress: address, | |
| 446 | clientAddress, | |
| 447 | serverAddress, | |
| 448 | payload | |
| 449 | } | |
| 450 | ||
| 451 | c.payload = new SecurePayload(hs.holepunchSecret) | |
| 452 | ||
| 453 | c.onsocket = function (socket, port, host) { | |
| 454 | if (c.rawStream === null) return // Already hole punched | |
| 455 | ||
| 456 | if (c.rawStream.connected) { | |
| 457 | const remoteChanging = c.rawStream.changeRemote(socket, c.connect.payload.udx.id, port, host) | |
| 458 | ||
| 459 | if (remoteChanging) remoteChanging.catch(safetyCatch) | |
| 460 | } else { | |
| 461 | // cache the relay addrs for a future reconnect, we prefer the remote one so they | |
| 462 | // can give us the correct ones from their pov | |
| 463 | if (payload.relayAddresses && payload.relayAddresses.length) { | |
| 464 | c.dht._relayAddressesCache.set(c.id, payload.relayAddresses) | |
| 465 | } else if (c.remoteRelayAddresses.length) { | |
| 466 | c.dht._relayAddressesCache.set(c.id, c.remoteRelayAddresses) | |
| 467 | } | |
| 468 | ||
| 469 | c.rawStream.connect(socket, c.connect.payload.udx.id, port, host) | |
| 470 | c.encryptedSocket.start(c.rawStream, { handshake: hs }) | |
| 471 | } | |
| 472 | ||
| 473 | if (c.reusableSocket && payload.udx.reusableSocket) { | |
| 474 | c.dht._socketPool.routes.add(c.remotePublicKey, c.rawStream) | |
| 475 | } | |
| 476 | ||
| 477 | if (c.puncher) { | |
| 478 | c.puncher.onabort = noop | |
| 479 | c.puncher.destroy() | |
| 480 | } | |
| 481 | ||
| 482 | if (c.passiveConnectTimeout) { | |
| 483 | clearPassiveConnectTimeout(c) | |
| 484 | } | |
| 485 | ||
| 486 | c.rawStream = null | |
| 487 | } | |
| 488 | ||
| 489 | if (payload.relayThrough || c.relayThrough) { | |
| 490 | relayConnection(c, c.relayThrough, payload, hs) | |
| 491 | } | |
| 492 | ||
| 493 | if (c.serverSocket) { | |
| 494 | c.onsocket(c.serverSocket, c.serverAddress.port, c.serverAddress.host) | |
| 495 | return | |
| 496 | } | |
| 497 | ||
| 498 | if (!relayed) { | |
| 499 | c.onsocket(socket || c.dht.socket, address.port, address.host) | |
| 500 | } | |
| 501 | ||
| 502 | c.session.destroy() | |
| 503 | } | |
| 504 | ||
| 505 | async function updateHolepunch(c, peerAddress, relayAddr, payload) { | |
| 506 | const holepunch = await c.dht._router.peerHolepunch( | |
| 507 | c.target, | |
| 508 | { | |
| 509 | id: c.connect.payload.holepunch.id, | |
| 510 | payload: c.payload.encrypt(payload), | |
| 511 | peerAddress, | |
| 512 | socket: c.puncher.socket, | |
| 513 | session: c.session | |
| 514 | }, | |
| 515 | relayAddr | |
| 516 | ) | |
| 517 | ||
| 518 | if (isDone(c)) return null | |
| 519 | ||
| 520 | const remotePayload = c.payload.decrypt(holepunch.payload) | |
| 521 | if (!remotePayload) { | |
| 522 | throw HOLEPUNCH_INVALID() | |
| 523 | } | |
| 524 | ||
| 525 | const { error, firewall, punching, addresses, remoteToken } = remotePayload | |
| 526 | ||
| 527 | if (error === ERROR.TRY_LATER && c.relayToken && payload.punching) { | |
| 528 | return { | |
| 529 | tryLater: true, | |
| 530 | ...holepunch, | |
| 531 | payload: remotePayload | |
| 532 | } | |
| 533 | } | |
| 534 | ||
| 535 | if (error !== ERROR.NONE) { | |
| 536 | throw REMOTE_ABORTED('Remote aborted with error code ' + error) | |
| 537 | } | |
| 538 | ||
| 539 | const echoed = !!(remoteToken && payload.token && b4a.equals(remoteToken, payload.token)) | |
| 540 | ||
| 541 | c.puncher.updateRemote({ | |
| 542 | punching, | |
| 543 | firewall, | |
| 544 | addresses, | |
| 545 | verified: echoed ? peerAddress.host : null | |
| 546 | }) | |
| 547 | ||
| 548 | return { | |
| 549 | tryLater: false, | |
| 550 | ...holepunch, | |
| 551 | payload: remotePayload | |
| 552 | } | |
| 553 | } | |
| 554 | ||
| 555 | async function probeRound(c, serverAddress, serverRelay, retry) { | |
| 556 | // Open a quick low ttl session against what we think is the server | |
| 557 | if (serverAddress) await c.puncher.openSession(serverAddress) | |
| 558 | ||
| 559 | if (isDone(c)) return null | |
| 560 | ||
| 561 | const reply = await updateHolepunch(c, serverRelay.peerAddress, serverRelay.relayAddress, { | |
| 562 | error: ERROR.NONE, | |
| 563 | firewall: c.puncher.nat.firewall, | |
| 564 | round: c.round++, | |
| 565 | connected: false, | |
| 566 | punching: false, | |
| 567 | addresses: c.puncher.nat.addresses, | |
| 568 | remoteAddress: serverAddress, | |
| 569 | token: null, | |
| 570 | remoteToken: null | |
| 571 | }) | |
| 572 | ||
| 573 | if (isDone(c) || !reply) return null | |
| 574 | ||
| 575 | const { peerAddress } = reply | |
| 576 | const { address, token } = reply.payload | |
| 577 | ||
| 578 | c.puncher.nat.add(reply.to, reply.from) | |
| 579 | ||
| 580 | // Open another quick low ttl session against what the server says their address is, | |
| 581 | // if they haven't said they are random yet | |
| 582 | if ( | |
| 583 | c.puncher.remoteFirewall < FIREWALL.RANDOM && | |
| 584 | address && | |
| 585 | address.host && | |
| 586 | address.port && | |
| 587 | diffAddress(address, serverAddress) | |
| 588 | ) { | |
| 589 | await c.puncher.openSession(address) | |
| 590 | if (isDone(c)) return null | |
| 591 | } | |
| 592 | ||
| 593 | // If the remote told us they didn't know their nat firewall yet, give them a chance to figure it out | |
| 594 | // They might say this to see if the "fast mode" punch comes through first. | |
| 595 | if (c.puncher.remoteFirewall === FIREWALL.UNKNOWN) { | |
| 596 | await c.sleeper.pause(1000) | |
| 597 | if (isDone(c)) return null | |
| 598 | } | |
| 599 | ||
| 600 | let stable = await c.puncher.analyze(false) | |
| 601 | if (isDone(c)) return null | |
| 602 | ||
| 603 | // If the socket seems unstable, try to make it stable by setting the "allowReopen" flag | |
| 604 | // Mostly relevant for mobile networks | |
| 605 | if (!stable) { | |
| 606 | stable = await c.puncher.analyze(true) | |
| 607 | if (isDone(c)) return null | |
| 608 | if (stable) return probeRound(c, serverAddress, serverRelay, false) | |
| 609 | } | |
| 610 | ||
| 611 | if ((c.puncher.remoteFirewall === FIREWALL.UNKNOWN || !token) && retry) { | |
| 612 | return probeRound(c, serverAddress, serverRelay, false) | |
| 613 | } | |
| 614 | ||
| 615 | if ( | |
| 616 | c.puncher.remoteFirewall === FIREWALL.UNKNOWN || | |
| 617 | c.puncher.nat.firewall === FIREWALL.UNKNOWN | |
| 618 | ) { | |
| 619 | await abort(c, serverRelay, HOLEPUNCH_PROBE_TIMEOUT()) | |
| 620 | return null | |
| 621 | } | |
| 622 | ||
| 623 | if (c.puncher.remoteFirewall >= FIREWALL.RANDOM && c.puncher.nat.firewall >= FIREWALL.RANDOM) { | |
| 624 | await abort(c, serverRelay, HOLEPUNCH_DOUBLE_RANDOMIZED_NATS()) | |
| 625 | return null | |
| 626 | } | |
| 627 | ||
| 628 | return { token, peerAddress } | |
| 629 | } | |
| 630 | ||
| 631 | async function roundPunch(c, serverAddress, remoteToken, clientRelay, serverRelay, delayed) { | |
| 632 | // We are gossiping our final NAT status to the other peer now | |
| 633 | // so make sure we don't update our local view for now as that can make things weird | |
| 634 | c.puncher.nat.freeze() | |
| 635 | ||
| 636 | const isRandom = | |
| 637 | c.puncher.remoteFirewall >= FIREWALL.RANDOM || c.puncher.nat.firewall >= FIREWALL.RANDOM | |
| 638 | if (isRandom) { | |
| 639 | while ( | |
| 640 | c.dht._randomPunches >= c.dht._randomPunchLimit || | |
| 641 | Date.now() - c.dht._lastRandomPunch < c.dht._randomPunchInterval | |
| 642 | ) { | |
| 643 | // if no relay can help, bail | |
| 644 | if (!c.relayToken) throw HOLEPUNCH_ABORTED() | |
| 645 | ||
| 646 | if (!delayed) { | |
| 647 | delayed = true | |
| 648 | await updateHolepunch(c, serverAddress, clientRelay, { | |
| 649 | error: ERROR.NONE, | |
| 650 | firewall: c.puncher.nat.firewall, | |
| 651 | round: c.round++, | |
| 652 | connected: false, | |
| 653 | punching: false, | |
| 654 | addresses: c.puncher.nat.addresses, | |
| 655 | remoteAddress: null, | |
| 656 | token: c.payload.token(serverAddress), | |
| 657 | remoteToken | |
| 658 | }) | |
| 659 | if (isDone(c)) return | |
| 660 | } | |
| 661 | ||
| 662 | await tryLater(c) | |
| 663 | if (isDone(c)) return | |
| 664 | } | |
| 665 | } | |
| 666 | ||
| 667 | // increment now, so we can commit to punching | |
| 668 | if (isRandom) c.dht._randomPunches++ | |
| 669 | ||
| 670 | let reply | |
| 671 | ||
| 672 | try { | |
| 673 | // if delayed switch to the servers chosen relay - we validated anyway | |
| 674 | reply = await updateHolepunch( | |
| 675 | c, | |
| 676 | delayed ? serverRelay.peerAddress : serverAddress, | |
| 677 | delayed ? serverRelay.relayAddress : clientRelay, | |
| 678 | { | |
| 679 | error: ERROR.NONE, | |
| 680 | firewall: c.puncher.nat.firewall, | |
| 681 | round: c.round++, | |
| 682 | connected: false, | |
| 683 | punching: true, | |
| 684 | addresses: c.puncher.nat.addresses, | |
| 685 | remoteAddress: null, | |
| 686 | token: delayed ? null : c.payload.token(serverAddress), | |
| 687 | remoteToken | |
| 688 | } | |
| 689 | ) | |
| 690 | } finally { | |
| 691 | // decrement as punch increments for us | |
| 692 | if (isRandom) c.dht._randomPunches-- | |
| 693 | } | |
| 694 | ||
| 695 | if (isDone(c)) return | |
| 696 | if (!reply) return | |
| 697 | ||
| 698 | if (reply.tryLater) { | |
| 699 | await tryLater(c) | |
| 700 | if (isDone(c)) return | |
| 701 | return roundPunch(c, serverAddress, remoteToken, clientRelay, serverRelay, true) | |
| 702 | } | |
| 703 | ||
| 704 | if (!c.puncher.remoteHolepunching) { | |
| 705 | throw REMOTE_NOT_HOLEPUNCHING() | |
| 706 | } | |
| 707 | ||
| 708 | if (!(await c.puncher.punch())) { | |
| 709 | throw REMOTE_NOT_HOLEPUNCHABLE() | |
| 710 | } | |
| 711 | } | |
| 712 | ||
| 713 | async function tryLater(c) { | |
| 714 | if (!c.relayToken) throw HOLEPUNCH_ABORTED() | |
| 715 | await c.sleeper.pause(10000 + Math.round(Math.random() * 10000)) | |
| 716 | } | |
| 717 | ||
| 718 | function maybeDestroyEncryptedSocket(c, err) { | |
| 719 | if (isDone(c)) return | |
| 720 | if (c.encryptedSocket.rawStream) return | |
| 721 | if (c.relaySocket) return // waiting for the relay | |
| 722 | if (c.puncher && !c.puncher.destroyed) return // waiting for the puncher | |
| 723 | c.session.destroy() | |
| 724 | c.encryptedSocket.destroy(err) | |
| 725 | } | |
| 726 | ||
| 727 | async function abort(c, { peerAddress, relayAddress }, err) { | |
| 728 | try { | |
| 729 | await updateHolepunch(peerAddress, relayAddress, { | |
| 730 | error: ERROR.ABORTED, | |
| 731 | firewall: FIREWALL.UNKNOWN, | |
| 732 | round: c.round++, | |
| 733 | connected: false, | |
| 734 | punching: false, | |
| 735 | addresses: null, | |
| 736 | remoteAddress: null, | |
| 737 | token: null, | |
| 738 | remoteToken: null | |
| 739 | }) | |
| 740 | } catch {} | |
| 741 | ||
| 742 | destroyPuncher(c) | |
| 743 | maybeDestroyEncryptedSocket(c, err) | |
| 744 | } | |
| 745 | ||
| 746 | function relayConnection(c, relayThrough, payload, hs) { | |
| 747 | let isInitiator | |
| 748 | let publicKey | |
| 749 | let token | |
| 750 | ||
| 751 | if (payload.relayThrough) { | |
| 752 | isInitiator = false | |
| 753 | publicKey = payload.relayThrough.publicKey | |
| 754 | token = payload.relayThrough.token | |
| 755 | } else { | |
| 756 | isInitiator = true | |
| 757 | publicKey = relayThrough | |
| 758 | token = c.relayToken | |
| 759 | } | |
| 760 | ||
| 761 | c.relayToken = token | |
| 762 | c.relaySocket = c.dht.connect(publicKey) | |
| 763 | c.relaySocket.setKeepAlive(c.relayKeepAlive) | |
| 764 | c.relayClient = relay.Client.from(c.relaySocket, { id: c.relaySocket.publicKey }) | |
| 765 | c.relayTimeout = setTimeout(onabort, 15000, null) | |
| 766 | ||
| 767 | c.relayClient.pair(isInitiator, token, c.rawStream).on('error', onabort).on('data', ondata) | |
| 768 | ||
| 769 | function ondata(remoteId) { | |
| 770 | if (c.relayTimeout) clearRelayTimeout(c) | |
| 771 | if (c.rawStream === null) { | |
| 772 | onabort(null) | |
| 773 | return | |
| 774 | } | |
| 775 | ||
| 776 | c.relayPaired = true | |
| 777 | ||
| 778 | const { remotePort, remoteHost, socket } = c.relaySocket.rawStream | |
| 779 | ||
| 780 | c.rawStream | |
| 781 | .on('close', () => c.relaySocket.destroy()) | |
| 782 | .connect(socket, remoteId, remotePort, remoteHost) | |
| 783 | ||
| 784 | c.encryptedSocket.start(c.rawStream, { handshake: hs }) | |
| 785 | } | |
| 786 | ||
| 787 | function onabort(err) { | |
| 788 | if (c.relayTimeout) clearRelayTimeout(c) | |
| 789 | const socket = c.relaySocket | |
| 790 | c.relayToken = null | |
| 791 | c.relaySocket = null | |
| 792 | if (socket) socket.destroy() | |
| 793 | maybeDestroyEncryptedSocket(c, err || RELAY_ABORTED()) | |
| 794 | } | |
| 795 | } | |
| 796 | ||
| 797 | function clearPassiveConnectTimeout(c) { | |
| 798 | clearTimeout(c.passiveConnectTimeout) | |
| 799 | c.passiveConnectTimeout = null | |
| 800 | } | |
| 801 | ||
| 802 | function clearRelayTimeout(c) { | |
| 803 | clearTimeout(c.relayTimeout) | |
| 804 | c.relayTimeout = null | |
| 805 | } | |
| 806 | ||
| 807 | function destroyPuncher(c) { | |
| 808 | if (c.puncher) c.puncher.destroy() | |
| 809 | c.session.destroy() | |
| 810 | } | |
| 811 | ||
| 812 | function pickServerRelay(relays, clientRelay) { | |
| 813 | for (const r of relays) { | |
| 814 | if (!diffAddress(r.relayAddress, clientRelay)) return r | |
| 815 | } | |
| 816 | return relays[0] | |
| 817 | } | |
| 818 | ||
| 819 | function diffAddress(a, b) { | |
| 820 | return a.host !== b.host || a.port !== b.port | |
| 821 | } | |
| 822 | ||
| 823 | function defaultCreateHandshake(keyPair, remotePublicKey) { | |
| 824 | return new NoiseWrap(keyPair, remotePublicKey) | |
| 825 | } | |
| 826 | ||
| 827 | function defaultCreateSecretStream(isInitiator, rawStream, opts) { | |
| 828 | return new NoiseSecretStream(isInitiator, rawStream, opts) | |
| 829 | } | |
| 830 | ||
| 831 | function onlyNonReserved(addr) { | |
| 832 | return !isReserved(addr.host) | |
| 833 | } | |
| 834 | ||
| 835 | function isRelay(relaySocket, socket, port, host) { | |
| 836 | const stream = relaySocket.rawStream | |
| 837 | if (!stream) return false | |
| 838 | if (stream.socket !== socket) return false | |
| 839 | return port === stream.remotePort && host === stream.remoteHost | |
| 840 | } | |
| 841 | ||
| 842 | function selectRelay(relayThrough) { | |
| 843 | if (typeof relayThrough === 'function') relayThrough = relayThrough() | |
| 844 | if (relayThrough === null) return null | |
| 845 | if (Array.isArray(relayThrough)) | |
| 846 | return relayThrough[Math.floor(Math.random() * relayThrough.length)] | |
| 847 | return relayThrough | |
| 848 | } | |
| 849 | ||
| 850 | function noop() {} |
| … | … | @@ -0,0 +1,142 @@ |
| 1 | const EventEmitter = require('events') | |
| 2 | const b4a = require('b4a') | |
| 3 | const errors = require('./errors') | |
| 4 | ||
| 5 | module.exports = class ConnectionPool extends EventEmitter { | |
| 6 | constructor(dht) { | |
| 7 | super() | |
| 8 | ||
| 9 | this._dht = dht | |
| 10 | this._servers = new Map() | |
| 11 | this._connecting = new Map() | |
| 12 | this._connections = new Map() | |
| 13 | } | |
| 14 | ||
| 15 | _attachServer(server) { | |
| 16 | const keyString = b4a.toString(server.publicKey, 'hex') | |
| 17 | ||
| 18 | this._servers.set(keyString, server) | |
| 19 | ||
| 20 | server | |
| 21 | .on('close', () => { | |
| 22 | this._servers.delete(keyString) | |
| 23 | }) | |
| 24 | .on('connection', (socket) => { | |
| 25 | this._attachStream(socket, true) | |
| 26 | }) | |
| 27 | } | |
| 28 | ||
| 29 | _attachStream(stream, opened) { | |
| 30 | const existing = this.get(stream.remotePublicKey) | |
| 31 | ||
| 32 | if (existing) { | |
| 33 | const keepNew = | |
| 34 | stream.isInitiator === existing.isInitiator || | |
| 35 | b4a.compare(stream.publicKey, stream.remotePublicKey) > 0 | |
| 36 | ||
| 37 | if (keepNew) { | |
| 38 | let closed = false | |
| 39 | ||
| 40 | const onclose = () => { | |
| 41 | closed = true | |
| 42 | } | |
| 43 | ||
| 44 | existing | |
| 45 | .on('error', noop) | |
| 46 | .on('close', () => { | |
| 47 | if (closed) return | |
| 48 | ||
| 49 | stream.off('error', noop).off('close', onclose) | |
| 50 | ||
| 51 | this._attachStream(stream, opened) | |
| 52 | }) | |
| 53 | .destroy(errors.DUPLICATE_CONNECTION()) | |
| 54 | ||
| 55 | stream.on('error', noop).on('close', onclose) | |
| 56 | } else { | |
| 57 | stream.on('error', noop).destroy(errors.DUPLICATE_CONNECTION()) | |
| 58 | } | |
| 59 | ||
| 60 | return | |
| 61 | } | |
| 62 | ||
| 63 | const session = new ConnectionRef(this, stream) | |
| 64 | ||
| 65 | const keyString = b4a.toString(stream.remotePublicKey, 'hex') | |
| 66 | ||
| 67 | if (opened) { | |
| 68 | this._connections.set(keyString, session) | |
| 69 | ||
| 70 | stream.on('close', () => { | |
| 71 | this._connections.delete(keyString) | |
| 72 | }) | |
| 73 | ||
| 74 | this.emit('connection', stream, session) | |
| 75 | } else { | |
| 76 | this._connecting.set(keyString, session) | |
| 77 | ||
| 78 | stream | |
| 79 | .on('error', noop) | |
| 80 | .on('close', () => { | |
| 81 | if (opened) this._connections.delete(keyString) | |
| 82 | else this._connecting.delete(keyString) | |
| 83 | }) | |
| 84 | .on('open', () => { | |
| 85 | opened = true | |
| 86 | ||
| 87 | this._connecting.delete(keyString) | |
| 88 | this._connections.set(keyString, session) | |
| 89 | ||
| 90 | stream.off('error', noop) | |
| 91 | ||
| 92 | this.emit('connection', stream, session) | |
| 93 | }) | |
| 94 | } | |
| 95 | ||
| 96 | return session | |
| 97 | } | |
| 98 | ||
| 99 | get connecting() { | |
| 100 | return this._connecting.size | |
| 101 | } | |
| 102 | ||
| 103 | get connections() { | |
| 104 | return this._connections.values() | |
| 105 | } | |
| 106 | ||
| 107 | has(publicKey) { | |
| 108 | const keyString = b4a.toString(publicKey, 'hex') | |
| 109 | ||
| 110 | return this._connections.has(keyString) || this._connecting.has(keyString) | |
| 111 | } | |
| 112 | ||
| 113 | get(publicKey) { | |
| 114 | const keyString = b4a.toString(publicKey, 'hex') | |
| 115 | ||
| 116 | const existing = this._connections.get(keyString) || this._connecting.get(keyString) | |
| 117 | ||
| 118 | return existing?._stream || null | |
| 119 | } | |
| 120 | } | |
| 121 | ||
| 122 | class ConnectionRef { | |
| 123 | constructor(pool, stream) { | |
| 124 | this._pool = pool | |
| 125 | this._stream = stream | |
| 126 | this._refs = 0 | |
| 127 | } | |
| 128 | ||
| 129 | active() { | |
| 130 | this._refs++ | |
| 131 | } | |
| 132 | ||
| 133 | inactive() { | |
| 134 | this._refs-- | |
| 135 | } | |
| 136 | ||
| 137 | release() { | |
| 138 | this._stream.destroy() | |
| 139 | } | |
| 140 | } | |
| 141 | ||
| 142 | function noop() {} |
| … | … | @@ -0,0 +1,57 @@ |
| 1 | const crypto = require('hypercore-crypto') | |
| 2 | ||
| 3 | const COMMANDS = (exports.COMMANDS = { | |
| 4 | PEER_HANDSHAKE: 0, | |
| 5 | PEER_HOLEPUNCH: 1, | |
| 6 | FIND_PEER: 2, | |
| 7 | LOOKUP: 3, | |
| 8 | ANNOUNCE: 4, | |
| 9 | UNANNOUNCE: 5, | |
| 10 | MUTABLE_PUT: 6, | |
| 11 | MUTABLE_GET: 7, | |
| 12 | IMMUTABLE_PUT: 8, | |
| 13 | IMMUTABLE_GET: 9 | |
| 14 | }) | |
| 15 | ||
| 16 | exports.BOOTSTRAP_NODES = global.Pear?.config.dht?.bootstrap || [ | |
| 17 | '88.99.3.86@node1.hyperdht.org:49737', | |
| 18 | '142.93.90.113@node2.hyperdht.org:49737', | |
| 19 | '138.68.147.8@node3.hyperdht.org:49737' | |
| 20 | ] | |
| 21 | ||
| 22 | exports.KNOWN_NODES = global.Pear?.config.dht?.nodes || [] | |
| 23 | ||
| 24 | exports.FIREWALL = { | |
| 25 | UNKNOWN: 0, | |
| 26 | OPEN: 1, | |
| 27 | CONSISTENT: 2, | |
| 28 | RANDOM: 3 | |
| 29 | } | |
| 30 | ||
| 31 | exports.ERROR = { | |
| 32 | // noise / connection related | |
| 33 | NONE: 0, | |
| 34 | ABORTED: 1, | |
| 35 | VERSION_MISMATCH: 2, | |
| 36 | TRY_LATER: 3, | |
| 37 | // dht related | |
| 38 | SEQ_REUSED: 16, | |
| 39 | SEQ_TOO_LOW: 17 | |
| 40 | } | |
| 41 | ||
| 42 | const [NS_ANNOUNCE, NS_UNANNOUNCE, NS_MUTABLE_PUT, NS_PEER_HANDSHAKE, NS_PEER_HOLEPUNCH] = | |
| 43 | crypto.namespace('hyperswarm/dht', [ | |
| 44 | COMMANDS.ANNOUNCE, | |
| 45 | COMMANDS.UNANNOUNCE, | |
| 46 | COMMANDS.MUTABLE_PUT, | |
| 47 | COMMANDS.PEER_HANDSHAKE, | |
| 48 | COMMANDS.PEER_HOLEPUNCH | |
| 49 | ]) | |
| 50 | ||
| 51 | exports.NS = { | |
| 52 | ANNOUNCE: NS_ANNOUNCE, | |
| 53 | UNANNOUNCE: NS_UNANNOUNCE, | |
| 54 | MUTABLE_PUT: NS_MUTABLE_PUT, | |
| 55 | PEER_HANDSHAKE: NS_PEER_HANDSHAKE, | |
| 56 | PEER_HOLEPUNCH: NS_PEER_HOLEPUNCH | |
| 57 | } |
| … | … | @@ -0,0 +1,28 @@ |
| 1 | const sodium = require('sodium-universal') | |
| 2 | const b4a = require('b4a') | |
| 3 | ||
| 4 | function hash(data) { | |
| 5 | const out = b4a.allocUnsafe(32) | |
| 6 | sodium.crypto_generichash(out, data) | |
| 7 | return out | |
| 8 | } | |
| 9 | ||
| 10 | function unslabbedHash(data) { | |
| 11 | const out = b4a.allocUnsafeSlow(32) | |
| 12 | sodium.crypto_generichash(out, data) | |
| 13 | return out | |
| 14 | } | |
| 15 | ||
| 16 | function createKeyPair(seed) { | |
| 17 | const publicKey = b4a.alloc(32) | |
| 18 | const secretKey = b4a.alloc(64) | |
| 19 | if (seed) sodium.crypto_sign_seed_keypair(publicKey, secretKey, seed) | |
| 20 | else sodium.crypto_sign_keypair(publicKey, secretKey) | |
| 21 | return { publicKey, secretKey } | |
| 22 | } | |
| 23 | ||
| 24 | module.exports = { | |
| 25 | hash, | |
| 26 | unslabbedHash, | |
| 27 | createKeyPair | |
| 28 | } |
| … | … | @@ -0,0 +1,16 @@ |
| 1 | const b4a = require('b4a') | |
| 2 | const cenc = require('compact-encoding') | |
| 3 | ||
| 4 | function encodeUnslab(enc, m) { | |
| 5 | // Faster than unslab(c.encode(enc, data)) because it avoids the mem copy. | |
| 6 | // Makes sense to put in compact-encoding when we need it in other modules too | |
| 7 | const state = cenc.state() | |
| 8 | enc.preencode(state, m) | |
| 9 | state.buffer = b4a.allocUnsafeSlow(state.end) | |
| 10 | enc.encode(state, m) | |
| 11 | return state.buffer | |
| 12 | } | |
| 13 | ||
| 14 | module.exports = { | |
| 15 | encodeUnslab | |
| 16 | } |
| … | … | @@ -0,0 +1,110 @@ |
| 1 | module.exports = class DHTError extends Error { | |
| 2 | constructor(msg, code, fn = DHTError) { | |
| 3 | super(`${code}: ${msg}`) | |
| 4 | this.code = code | |
| 5 | ||
| 6 | if (Error.captureStackTrace) { | |
| 7 | Error.captureStackTrace(this, fn) | |
| 8 | } | |
| 9 | } | |
| 10 | ||
| 11 | get name() { | |
| 12 | return 'DHTError' | |
| 13 | } | |
| 14 | ||
| 15 | static BAD_HANDSHAKE_REPLY(msg = 'Bad handshake reply') { | |
| 16 | return new DHTError(msg, 'BAD_HANDSHAKE_REPLY', DHTError.BAD_HANDSHAKE_REPLY) | |
| 17 | } | |
| 18 | ||
| 19 | static BAD_HOLEPUNCH_REPLY(msg = 'Bad holepunch reply') { | |
| 20 | return new DHTError(msg, 'BAD_HOLEPUNCH_REPLY', DHTError.BAD_HOLEPUNCH_REPLY) | |
| 21 | } | |
| 22 | ||
| 23 | static HOLEPUNCH_ABORTED(msg = 'Holepunch aborted') { | |
| 24 | return new DHTError(msg, 'HOLEPUNCH_ABORTED', DHTError.HOLEPUNCH_ABORTED) | |
| 25 | } | |
| 26 | ||
| 27 | static HOLEPUNCH_INVALID(msg = 'Invalid holepunch payload') { | |
| 28 | return new DHTError(msg, 'HOLEPUNCH_INVALID', DHTError.HOLEPUNCH_INVALID) | |
| 29 | } | |
| 30 | ||
| 31 | static HOLEPUNCH_PROBE_TIMEOUT(msg = 'Holepunching probe did not finish in time') { | |
| 32 | return new DHTError(msg, 'HOLEPUNCH_PROBE_TIMEOUT', DHTError.HOLEPUNCH_PROBE_TIMEOUT) | |
| 33 | } | |
| 34 | ||
| 35 | static HOLEPUNCH_DOUBLE_RANDOMIZED_NATS(msg = 'Both remote and local NATs are randomized') { | |
| 36 | return new DHTError( | |
| 37 | msg, | |
| 38 | 'HOLEPUNCH_DOUBLE_RANDOMIZED_NATS', | |
| 39 | DHTError.HOLEPUNCH_DOUBLE_RANDOMIZED_NATS | |
| 40 | ) | |
| 41 | } | |
| 42 | ||
| 43 | static CANNOT_HOLEPUNCH(msg = 'Cannot holepunch to remote') { | |
| 44 | return new DHTError(msg, 'CANNOT_HOLEPUNCH', DHTError.CANNOT_HOLEPUNCH) | |
| 45 | } | |
| 46 | ||
| 47 | static REMOTE_NOT_HOLEPUNCHING(msg = 'Remote is not holepunching') { | |
| 48 | return new DHTError(msg, 'REMOTE_NOT_HOLEPUNCHING', DHTError.REMOTE_NOT_HOLEPUNCHING) | |
| 49 | } | |
| 50 | ||
| 51 | static REMOTE_NOT_HOLEPUNCHABLE(msg = 'Remote is not holepunchable') { | |
| 52 | return new DHTError(msg, 'REMOTE_NOT_HOLEPUNCHABLE', DHTError.REMOTE_NOT_HOLEPUNCHABLE) | |
| 53 | } | |
| 54 | ||
| 55 | static REMOTE_ABORTED(msg = 'Remote aborted') { | |
| 56 | return new DHTError(msg, 'REMOTE_ABORTED', DHTError.REMOTE_ABORTED) | |
| 57 | } | |
| 58 | ||
| 59 | static HANDSHAKE_UNFINISHED(msg = 'Handshake did not finish') { | |
| 60 | return new DHTError(msg, 'HANDSHAKE_UNFINISHED', DHTError.HANDSHAKE_UNFINISHED) | |
| 61 | } | |
| 62 | ||
| 63 | static HANDSHAKE_INVALID(msg = 'Received invalid handshake') { | |
| 64 | return new DHTError(msg, 'HANDSHAKE_INVALID', DHTError.HANDSHAKE_INVALID) | |
| 65 | } | |
| 66 | ||
| 67 | static ALREADY_LISTENING(msg = 'Already listening') { | |
| 68 | return new DHTError(msg, 'ALREADY_LISTENING', DHTError.ALREADY_LISTENING) | |
| 69 | } | |
| 70 | ||
| 71 | static KEYPAIR_ALREADY_USED(msg = 'Keypair already used') { | |
| 72 | return new DHTError(msg, 'KEYPAIR_ALREADY_USED', DHTError.KEYPAIR_ALREADY_USED) | |
| 73 | } | |
| 74 | ||
| 75 | static NODE_DESTROYED(msg = 'Node destroyed') { | |
| 76 | return new DHTError(msg, 'NODE_DESTROYED', DHTError.NODE_DESTROYED) | |
| 77 | } | |
| 78 | ||
| 79 | static PEER_CONNECTION_FAILED(msg = 'Could not connect to peer') { | |
| 80 | return new DHTError(msg, 'PEER_CONNECTION_FAILED', DHTError.PEER_CONNECTION_FAILED) | |
| 81 | } | |
| 82 | ||
| 83 | static PEER_NOT_FOUND(msg = 'Peer not found') { | |
| 84 | return new DHTError(msg, 'PEER_NOT_FOUND', DHTError.PEER_NOT_FOUND) | |
| 85 | } | |
| 86 | ||
| 87 | static STREAM_NOT_CONNECTED(msg = 'Stream is not connected') { | |
| 88 | return new DHTError(msg, 'STREAM_NOT_CONNECTED', DHTError.STREAM_DISCONNECTED) | |
| 89 | } | |
| 90 | ||
| 91 | static SERVER_INCOMPATIBLE(msg = 'Server is using an incompatible version') { | |
| 92 | return new DHTError(msg, 'SERVER_INCOMPATIBLE', DHTError.SERVER_INCOMPATIBLE) | |
| 93 | } | |
| 94 | ||
| 95 | static SERVER_ERROR(msg = 'Server returned an error') { | |
| 96 | return new DHTError(msg, 'SERVER_ERROR', DHTError.SERVER_ERROR) | |
| 97 | } | |
| 98 | ||
| 99 | static DUPLICATE_CONNECTION(msg = 'Duplicate connection') { | |
| 100 | return new DHTError(msg, 'DUPLICATE_CONNECTION', DHTError.DUPLICATE_CONNECTION) | |
| 101 | } | |
| 102 | ||
| 103 | static RELAY_ABORTED(msg = 'Relay aborted') { | |
| 104 | return new DHTError(msg, 'RELAY_ABORTED', DHTError.RELAY_ABORTED) | |
| 105 | } | |
| 106 | ||
| 107 | static SUSPENDED(msg = 'Suspended') { | |
| 108 | return new DHTError(msg, 'SUSPENDED', DHTError.SUSPENDED) | |
| 109 | } | |
| 110 | } |
| … | … | @@ -0,0 +1,388 @@ |
| 1 | const b4a = require('b4a') | |
| 2 | const Nat = require('./nat') | |
| 3 | const Sleeper = require('./sleeper') | |
| 4 | const { FIREWALL } = require('./constants') | |
| 5 | ||
| 6 | const BIRTHDAY_SOCKETS = 256 | |
| 7 | const HOLEPUNCH = b4a.from([0]) | |
| 8 | const HOLEPUNCH_TTL = 5 | |
| 9 | const DEFAULT_TTL = 64 | |
| 10 | const MAX_REOPENS = 3 | |
| 11 | ||
| 12 | module.exports = class Holepuncher { | |
| 13 | constructor(dht, session, isInitiator, remoteFirewall = FIREWALL.UNKNOWN) { | |
| 14 | const holder = dht._socketPool.acquire() | |
| 15 | ||
| 16 | this.dht = dht | |
| 17 | this.session = session | |
| 18 | ||
| 19 | this.nat = new Nat(dht, session, holder.socket) | |
| 20 | this.nat.autoSample() | |
| 21 | ||
| 22 | this.isInitiator = isInitiator | |
| 23 | ||
| 24 | // events | |
| 25 | this.onconnect = noop | |
| 26 | this.onabort = noop | |
| 27 | ||
| 28 | this.punching = false | |
| 29 | this.connected = false | |
| 30 | this.destroyed = false | |
| 31 | this.randomized = false | |
| 32 | ||
| 33 | // track remote state | |
| 34 | this.remoteFirewall = remoteFirewall | |
| 35 | this.remoteAddresses = [] | |
| 36 | this.remoteHolepunching = false | |
| 37 | ||
| 38 | this._sleeper = new Sleeper() | |
| 39 | this._reopening = null | |
| 40 | this._timeout = null | |
| 41 | this._punching = null | |
| 42 | this._allHolders = [] | |
| 43 | this._holder = this._addRef(holder) | |
| 44 | } | |
| 45 | ||
| 46 | get socket() { | |
| 47 | return this._holder.socket | |
| 48 | } | |
| 49 | ||
| 50 | updateRemote({ punching, firewall, addresses, verified }) { | |
| 51 | const remoteAddresses = [] | |
| 52 | ||
| 53 | if (addresses) { | |
| 54 | for (const addr of addresses) { | |
| 55 | remoteAddresses.push({ | |
| 56 | host: addr.host, | |
| 57 | port: addr.port, | |
| 58 | verified: verified === addr.host || this._isVerified(addr.host) | |
| 59 | }) | |
| 60 | } | |
| 61 | } | |
| 62 | ||
| 63 | this.remoteFirewall = firewall | |
| 64 | this.remoteAddresses = remoteAddresses | |
| 65 | this.remoteHolepunching = punching | |
| 66 | } | |
| 67 | ||
| 68 | _isVerified(host) { | |
| 69 | for (const addr of this.remoteAddresses) { | |
| 70 | if (addr.verified && addr.host === host) { | |
| 71 | return true | |
| 72 | } | |
| 73 | } | |
| 74 | return false | |
| 75 | } | |
| 76 | ||
| 77 | ping(addr, socket = this._holder.socket) { | |
| 78 | return holepunch(socket, addr, false) | |
| 79 | } | |
| 80 | ||
| 81 | openSession(addr, socket = this._holder.socket) { | |
| 82 | return holepunch(socket, addr, true) | |
| 83 | } | |
| 84 | ||
| 85 | async analyze(allowReopen) { | |
| 86 | await this.nat.analyzing | |
| 87 | if (this._unstable()) { | |
| 88 | if (!allowReopen) return false | |
| 89 | if (!this._reopening) this._reopening = this._reopen() | |
| 90 | return this._reopening | |
| 91 | } | |
| 92 | return true | |
| 93 | } | |
| 94 | ||
| 95 | _unstable() { | |
| 96 | // TODO!!: We need an additional heuristic here... If we were NOT random in the past we should also do this. | |
| 97 | const firewall = this.nat.firewall | |
| 98 | return ( | |
| 99 | (this.remoteFirewall >= FIREWALL.RANDOM && firewall >= FIREWALL.RANDOM) || | |
| 100 | firewall === FIREWALL.UNKNOWN | |
| 101 | ) | |
| 102 | } | |
| 103 | ||
| 104 | _reset() { | |
| 105 | const prev = this._holder | |
| 106 | ||
| 107 | this._allHolders.pop() | |
| 108 | this._holder = this._addRef(this.dht._socketPool.acquire()) | |
| 109 | ||
| 110 | prev.release() | |
| 111 | this.nat.destroy() | |
| 112 | ||
| 113 | this.nat = new Nat(this.dht, this.session, this._holder.socket) | |
| 114 | // TODO: maybe make auto sampling configurable somehow? | |
| 115 | this.nat.autoSample() | |
| 116 | } | |
| 117 | ||
| 118 | _addRef(ref) { | |
| 119 | this._allHolders.push(ref) | |
| 120 | ref.onholepunchmessage = (msg, rinfo) => this._onholepunchmessage(msg, rinfo, ref) | |
| 121 | return ref | |
| 122 | } | |
| 123 | ||
| 124 | _onholepunchmessage(_, addr, ref) { | |
| 125 | if (!this.isInitiator) { | |
| 126 | // TODO: we don't need this if we had a way to connect a socket to many hosts | |
| 127 | holepunch(ref.socket, addr, false) // never fails | |
| 128 | return | |
| 129 | } | |
| 130 | ||
| 131 | if (this.connected) return | |
| 132 | ||
| 133 | this.connected = true | |
| 134 | this.punching = false | |
| 135 | ||
| 136 | for (const r of this._allHolders) { | |
| 137 | if (r === ref) continue | |
| 138 | r.release() | |
| 139 | } | |
| 140 | ||
| 141 | this._allHolders[0] = ref | |
| 142 | while (this._allHolders.length > 1) this._allHolders.pop() | |
| 143 | ||
| 144 | this._decrementRandomized() | |
| 145 | this.onconnect(ref.socket, addr.port, addr.host) | |
| 146 | } | |
| 147 | ||
| 148 | _done() { | |
| 149 | return this.destroyed || this.connected | |
| 150 | } | |
| 151 | ||
| 152 | async _reopen() { | |
| 153 | for (let i = 0; this._unstable() && i < MAX_REOPENS && !this._done() && !this.punching; i++) { | |
| 154 | this._reset() | |
| 155 | await this.nat.analyzing | |
| 156 | } | |
| 157 | ||
| 158 | return coerceFirewall(this.nat.firewall) === FIREWALL.CONSISTENT | |
| 159 | } | |
| 160 | ||
| 161 | punch() { | |
| 162 | if (!this._punching) this._punching = this._punch() | |
| 163 | return this._punching | |
| 164 | } | |
| 165 | ||
| 166 | async _punch() { | |
| 167 | if (this._done() || !this.remoteAddresses.length) return false | |
| 168 | ||
| 169 | this.punching = true | |
| 170 | ||
| 171 | // Coerce into consistency for now. Obvs we could make this this more efficient if we use that info | |
| 172 | // but that's seldomly used since those will just use tcp most of the time. | |
| 173 | ||
| 174 | const local = coerceFirewall(this.nat.firewall) | |
| 175 | const remote = coerceFirewall(this.remoteFirewall) | |
| 176 | ||
| 177 | // Note that most of these async functions are meant to run in the background | |
| 178 | // which is why we don't await them here and why they are not allowed to throw | |
| 179 | ||
| 180 | let remoteVerifiedAddress = null | |
| 181 | for (const addr of this.remoteAddresses) { | |
| 182 | if (addr.verified) { | |
| 183 | remoteVerifiedAddress = addr | |
| 184 | break | |
| 185 | } | |
| 186 | } | |
| 187 | ||
| 188 | if (local === FIREWALL.CONSISTENT && remote === FIREWALL.CONSISTENT) { | |
| 189 | this.dht.stats.punches.consistent++ | |
| 190 | this._consistentProbe() | |
| 191 | return true | |
| 192 | } | |
| 193 | ||
| 194 | if (!remoteVerifiedAddress) return false | |
| 195 | ||
| 196 | if (local === FIREWALL.CONSISTENT && remote >= FIREWALL.RANDOM) { | |
| 197 | this.dht.stats.punches.random++ | |
| 198 | this._incrementRandomized() | |
| 199 | this._randomProbes(remoteVerifiedAddress) | |
| 200 | return true | |
| 201 | } | |
| 202 | ||
| 203 | if (local >= FIREWALL.RANDOM && remote === FIREWALL.CONSISTENT) { | |
| 204 | this.dht.stats.punches.random++ | |
| 205 | this._incrementRandomized() | |
| 206 | await this._openBirthdaySockets(remoteVerifiedAddress) | |
| 207 | if (this.punching) this._keepAliveRandomNat(remoteVerifiedAddress) | |
| 208 | return true | |
| 209 | } | |
| 210 | ||
| 211 | return false | |
| 212 | } | |
| 213 | ||
| 214 | // Note that this never throws so it is safe to run in the background | |
| 215 | async _consistentProbe() { | |
| 216 | // Here we do the sleep first because the "fast open" mode in the server just fired a ping | |
| 217 | if (!this.isInitiator) await this._sleeper.pause(1000) | |
| 218 | ||
| 219 | let tries = 0 | |
| 220 | ||
| 221 | while (this.punching && tries++ < 10) { | |
| 222 | for (const addr of this.remoteAddresses) { | |
| 223 | // only try unverified addresses every 4 ticks | |
| 224 | if (!addr.verified && (tries & 3) !== 0) continue | |
| 225 | await holepunch(this._holder.socket, addr, false) | |
| 226 | } | |
| 227 | if (this.punching) await this._sleeper.pause(1000) | |
| 228 | } | |
| 229 | ||
| 230 | this._autoDestroy() | |
| 231 | } | |
| 232 | ||
| 233 | // Note that this never throws so it is safe to run in the background | |
| 234 | async _randomProbes(remoteAddr) { | |
| 235 | let tries = 1750 // ~35s | |
| 236 | ||
| 237 | while (this.punching && tries-- > 0) { | |
| 238 | const addr = { host: remoteAddr.host, port: randomPort() } | |
| 239 | await holepunch(this._holder.socket, addr, false) | |
| 240 | if (this.punching) await this._sleeper.pause(20) | |
| 241 | } | |
| 242 | ||
| 243 | this._autoDestroy() | |
| 244 | } | |
| 245 | ||
| 246 | // Note that this never throws so it is safe to run in the background | |
| 247 | async _keepAliveRandomNat(remoteAddr) { | |
| 248 | let i = 0 | |
| 249 | let lowTTLRounds = 1 | |
| 250 | ||
| 251 | // TODO: experiment with this here. We just bursted all the messages in | |
| 252 | // openOtherSockets to ensure the sockets are open, so it's potentially | |
| 253 | // a good idea to slow down for a bit. | |
| 254 | await this._sleeper.pause(100) | |
| 255 | ||
| 256 | let tries = 1750 // ~35s | |
| 257 | ||
| 258 | while (this.punching && tries-- > 0) { | |
| 259 | if (i === this._allHolders.length) { | |
| 260 | i = 0 | |
| 261 | if (lowTTLRounds > 0) lowTTLRounds-- | |
| 262 | } | |
| 263 | ||
| 264 | await holepunch(this._allHolders[i++].socket, remoteAddr, lowTTLRounds > 0) | |
| 265 | if (this.punching) await this._sleeper.pause(20) | |
| 266 | } | |
| 267 | ||
| 268 | this._autoDestroy() | |
| 269 | } | |
| 270 | ||
| 271 | async _openBirthdaySockets(remoteAddr) { | |
| 272 | while (this.punching && this._allHolders.length < BIRTHDAY_SOCKETS) { | |
| 273 | const ref = this._addRef(this.dht._socketPool.acquire()) | |
| 274 | await holepunch(ref.socket, remoteAddr, HOLEPUNCH_TTL) | |
| 275 | } | |
| 276 | } | |
| 277 | ||
| 278 | _autoDestroy() { | |
| 279 | if (!this.connected) this.destroy() | |
| 280 | } | |
| 281 | ||
| 282 | _incrementRandomized() { | |
| 283 | if (!this.randomized) { | |
| 284 | this.randomized = true | |
| 285 | this.dht._randomPunches++ | |
| 286 | } | |
| 287 | } | |
| 288 | ||
| 289 | _decrementRandomized() { | |
| 290 | if (this.randomized) { | |
| 291 | this.dht._lastRandomPunch = Date.now() | |
| 292 | this.randomized = false | |
| 293 | this.dht._randomPunches-- | |
| 294 | } | |
| 295 | } | |
| 296 | ||
| 297 | destroy() { | |
| 298 | if (this.destroyed) return | |
| 299 | this.destroyed = true | |
| 300 | this.punching = false | |
| 301 | ||
| 302 | for (const ref of this._allHolders) ref.release() | |
| 303 | this._allHolders = [] | |
| 304 | this.nat.destroy() | |
| 305 | ||
| 306 | if (!this.connected) { | |
| 307 | this._decrementRandomized() | |
| 308 | this.onabort() | |
| 309 | } | |
| 310 | } | |
| 311 | ||
| 312 | static ping(socket, addr) { | |
| 313 | return holepunch(socket, addr, false) | |
| 314 | } | |
| 315 | ||
| 316 | static localAddresses(socket) { | |
| 317 | return localAddresses(socket) | |
| 318 | } | |
| 319 | ||
| 320 | static matchAddress(myAddresses, externalAddresses) { | |
| 321 | return matchAddress(myAddresses, externalAddresses) | |
| 322 | } | |
| 323 | } | |
| 324 | ||
| 325 | function holepunch(socket, addr, lowTTL) { | |
| 326 | return socket.send(HOLEPUNCH, addr.port, addr.host, lowTTL ? HOLEPUNCH_TTL : DEFAULT_TTL) | |
| 327 | } | |
| 328 | ||
| 329 | function randomPort() { | |
| 330 | return (1000 + Math.random() * 64536) | 0 | |
| 331 | } | |
| 332 | ||
| 333 | function coerceFirewall(fw) { | |
| 334 | return fw === FIREWALL.OPEN ? FIREWALL.CONSISTENT : fw | |
| 335 | } | |
| 336 | ||
| 337 | function localAddresses(socket) { | |
| 338 | const addrs = [] | |
| 339 | const { host, port } = socket.address() | |
| 340 | ||
| 341 | if (host === '127.0.0.1') return [{ host, port }] | |
| 342 | ||
| 343 | for (const n of socket.udx.networkInterfaces()) { | |
| 344 | if (n.family !== 4 || n.internal) continue | |
| 345 | ||
| 346 | addrs.push({ host: n.host, port }) | |
| 347 | } | |
| 348 | ||
| 349 | if (addrs.length === 0) { | |
| 350 | addrs.push({ host: '127.0.0.1', port }) | |
| 351 | } | |
| 352 | ||
| 353 | return addrs | |
| 354 | } | |
| 355 | ||
| 356 | function matchAddress(localAddresses, remoteLocalAddresses) { | |
| 357 | if (remoteLocalAddresses.length === 0) return null | |
| 358 | ||
| 359 | let best = { segment: 1, addr: null } | |
| 360 | ||
| 361 | for (const localAddress of localAddresses) { | |
| 362 | // => 192.168.122.238 | |
| 363 | const a = localAddress.host.split('.') | |
| 364 | ||
| 365 | for (const remoteAddress of remoteLocalAddresses) { | |
| 366 | // => 192.168.0.23 | |
| 367 | // => 192.168.122.1 | |
| 368 | const b = remoteAddress.host.split('.') | |
| 369 | ||
| 370 | // Matches 192.*.*.* | |
| 371 | if (a[0] === b[0]) { | |
| 372 | if (best.segment === 1) best = { segment: 2, addr: remoteAddress } | |
| 373 | ||
| 374 | // Matches 192.168.*.* | |
| 375 | if (a[1] === b[1]) { | |
| 376 | if (best.segment === 2) best = { segment: 3, addr: remoteAddress } | |
| 377 | ||
| 378 | // Matches 192.168.122.* | |
| 379 | if (a[2] === b[2]) return remoteAddress | |
| 380 | } | |
| 381 | } | |
| 382 | } | |
| 383 | } | |
| 384 | ||
| 385 | return best.addr | |
| 386 | } | |
| 387 | ||
| 388 | function noop() {} |
| … | … | @@ -0,0 +1,421 @@ |
| 1 | const c = require('compact-encoding') | |
| 2 | ||
| 3 | const ipv4 = { | |
| 4 | ...c.ipv4Address, | |
| 5 | decode(state) { | |
| 6 | const ip = c.ipv4Address.decode(state) | |
| 7 | return { | |
| 8 | host: ip.host, | |
| 9 | port: ip.port | |
| 10 | } | |
| 11 | } | |
| 12 | } | |
| 13 | ||
| 14 | const ipv4Array = c.array(ipv4) | |
| 15 | ||
| 16 | const ipv6 = { | |
| 17 | ...c.ipv6Address, | |
| 18 | decode(state) { | |
| 19 | const ip = c.ipv6Address.decode(state) | |
| 20 | return { | |
| 21 | host: ip.host, | |
| 22 | port: ip.port | |
| 23 | } | |
| 24 | } | |
| 25 | } | |
| 26 | ||
| 27 | const ipv6Array = c.array(ipv6) | |
| 28 | ||
| 29 | exports.handshake = { | |
| 30 | preencode(state, m) { | |
| 31 | state.end += 1 + 1 + (m.peerAddress ? 6 : 0) + (m.relayAddress ? 6 : 0) | |
| 32 | c.buffer.preencode(state, m.noise) | |
| 33 | }, | |
| 34 | encode(state, m) { | |
| 35 | const flags = (m.peerAddress ? 1 : 0) | (m.relayAddress ? 2 : 0) | |
| 36 | ||
| 37 | c.uint.encode(state, flags) | |
| 38 | c.uint.encode(state, m.mode) | |
| 39 | c.buffer.encode(state, m.noise) | |
| 40 | ||
| 41 | if (m.peerAddress) ipv4.encode(state, m.peerAddress) | |
| 42 | if (m.relayAddress) ipv4.encode(state, m.relayAddress) | |
| 43 | }, | |
| 44 | decode(state) { | |
| 45 | const flags = c.uint.decode(state) | |
| 46 | ||
| 47 | return { | |
| 48 | mode: c.uint.decode(state), | |
| 49 | noise: c.buffer.decode(state), | |
| 50 | peerAddress: flags & 1 ? ipv4.decode(state) : null, | |
| 51 | relayAddress: flags & 2 ? ipv4.decode(state) : null | |
| 52 | } | |
| 53 | } | |
| 54 | } | |
| 55 | ||
| 56 | const relayInfo = { | |
| 57 | preencode(state, m) { | |
| 58 | state.end += 12 | |
| 59 | }, | |
| 60 | encode(state, m) { | |
| 61 | ipv4.encode(state, m.relayAddress) | |
| 62 | ipv4.encode(state, m.peerAddress) | |
| 63 | }, | |
| 64 | decode(state) { | |
| 65 | return { | |
| 66 | relayAddress: ipv4.decode(state), | |
| 67 | peerAddress: ipv4.decode(state) | |
| 68 | } | |
| 69 | } | |
| 70 | } | |
| 71 | ||
| 72 | const relayInfoArray = c.array(relayInfo) | |
| 73 | ||
| 74 | const holepunchInfo = { | |
| 75 | preencode(state, m) { | |
| 76 | c.uint.preencode(state, m.id) | |
| 77 | relayInfoArray.preencode(state, m.relays) | |
| 78 | }, | |
| 79 | encode(state, m) { | |
| 80 | c.uint.encode(state, m.id) | |
| 81 | relayInfoArray.encode(state, m.relays) | |
| 82 | }, | |
| 83 | decode(state) { | |
| 84 | return { | |
| 85 | id: c.uint.decode(state), | |
| 86 | relays: relayInfoArray.decode(state) | |
| 87 | } | |
| 88 | } | |
| 89 | } | |
| 90 | ||
| 91 | const udxInfo = { | |
| 92 | preencode(state, m) { | |
| 93 | state.end += 2 // version + features | |
| 94 | c.uint.preencode(state, m.id) | |
| 95 | c.uint.preencode(state, m.seq) | |
| 96 | }, | |
| 97 | encode(state, m) { | |
| 98 | c.uint.encode(state, 1) | |
| 99 | c.uint.encode(state, m.reusableSocket ? 1 : 0) | |
| 100 | c.uint.encode(state, m.id) | |
| 101 | c.uint.encode(state, m.seq) | |
| 102 | }, | |
| 103 | decode(state) { | |
| 104 | const version = c.uint.decode(state) | |
| 105 | const features = c.uint.decode(state) | |
| 106 | ||
| 107 | return { | |
| 108 | version, | |
| 109 | reusableSocket: (features & 1) !== 0, | |
| 110 | id: c.uint.decode(state), | |
| 111 | seq: c.uint.decode(state) | |
| 112 | } | |
| 113 | } | |
| 114 | } | |
| 115 | ||
| 116 | const secretStreamInfo = { | |
| 117 | preencode(state, m) { | |
| 118 | c.uint.preencode(state, 1) | |
| 119 | }, | |
| 120 | encode(state, m) { | |
| 121 | c.uint.encode(state, 1) | |
| 122 | }, | |
| 123 | decode(state) { | |
| 124 | return { | |
| 125 | version: c.uint.decode(state) | |
| 126 | } | |
| 127 | } | |
| 128 | } | |
| 129 | ||
| 130 | const relayThroughInfo = { | |
| 131 | preencode(state, m) { | |
| 132 | c.uint.preencode(state, 1) // version | |
| 133 | c.uint.preencode(state, 0) // flags | |
| 134 | c.fixed32.preencode(state, m.publicKey) | |
| 135 | c.fixed32.preencode(state, m.token) | |
| 136 | }, | |
| 137 | encode(state, m) { | |
| 138 | c.uint.encode(state, 1) | |
| 139 | c.uint.encode(state, 0) | |
| 140 | c.fixed32.encode(state, m.publicKey) | |
| 141 | c.fixed32.encode(state, m.token) | |
| 142 | }, | |
| 143 | decode(state) { | |
| 144 | const version = c.uint.decode(state) | |
| 145 | c.uint.decode(state) | |
| 146 | ||
| 147 | return { | |
| 148 | version, | |
| 149 | publicKey: c.fixed32.decode(state), | |
| 150 | token: c.fixed32.decode(state) | |
| 151 | } | |
| 152 | } | |
| 153 | } | |
| 154 | ||
| 155 | exports.noisePayload = { | |
| 156 | preencode(state, m) { | |
| 157 | state.end += 4 // version + flags + error + firewall | |
| 158 | if (m.holepunch) holepunchInfo.preencode(state, m.holepunch) | |
| 159 | if (m.addresses4 && m.addresses4.length) ipv4Array.preencode(state, m.addresses4) | |
| 160 | if (m.addresses6 && m.addresses6.length) ipv6Array.preencode(state, m.addresses6) | |
| 161 | if (m.udx) udxInfo.preencode(state, m.udx) | |
| 162 | if (m.secretStream) secretStreamInfo.preencode(state, m.secretStream) | |
| 163 | if (m.relayThrough) relayThroughInfo.preencode(state, m.relayThrough) | |
| 164 | if (m.relayAddresses) ipv4Array.preencode(state, m.relayAddresses) | |
| 165 | }, | |
| 166 | encode(state, m) { | |
| 167 | let flags = 0 | |
| 168 | ||
| 169 | if (m.holepunch) flags |= 1 | |
| 170 | if (m.addresses4 && m.addresses4.length) flags |= 2 | |
| 171 | if (m.addresses6 && m.addresses6.length) flags |= 4 | |
| 172 | if (m.udx) flags |= 8 | |
| 173 | if (m.secretStream) flags |= 16 | |
| 174 | if (m.relayThrough) flags |= 32 | |
| 175 | if (m.relayAddresses) flags |= 64 | |
| 176 | ||
| 177 | c.uint.encode(state, 1) // version | |
| 178 | c.uint.encode(state, flags) | |
| 179 | c.uint.encode(state, m.error) | |
| 180 | c.uint.encode(state, m.firewall) | |
| 181 | ||
| 182 | if (m.holepunch) holepunchInfo.encode(state, m.holepunch) | |
| 183 | if (m.addresses4 && m.addresses4.length) ipv4Array.encode(state, m.addresses4) | |
| 184 | if (m.addresses6 && m.addresses6.length) ipv6Array.encode(state, m.addresses6) | |
| 185 | if (m.udx) udxInfo.encode(state, m.udx) | |
| 186 | if (m.secretStream) secretStreamInfo.encode(state, m.secretStream) | |
| 187 | if (m.relayThrough) relayThroughInfo.encode(state, m.relayThrough) | |
| 188 | if (m.relayAddresses) ipv4Array.encode(state, m.relayAddresses) | |
| 189 | }, | |
| 190 | decode(state) { | |
| 191 | const version = c.uint.decode(state) | |
| 192 | ||
| 193 | if (version !== 1) { | |
| 194 | // Do not attempt to decode but return this back to the user so they can | |
| 195 | // actually handle it | |
| 196 | return { | |
| 197 | version, | |
| 198 | error: 0, | |
| 199 | firewall: 0, | |
| 200 | holepunch: null, | |
| 201 | addresses4: [], | |
| 202 | addresses6: [], | |
| 203 | udx: null, | |
| 204 | secretStream: null, | |
| 205 | relayThrough: null, | |
| 206 | relayAddresses: null | |
| 207 | } | |
| 208 | } | |
| 209 | ||
| 210 | const flags = c.uint.decode(state) | |
| 211 | ||
| 212 | return { | |
| 213 | version, | |
| 214 | error: c.uint.decode(state), | |
| 215 | firewall: c.uint.decode(state), | |
| 216 | holepunch: (flags & 1) !== 0 ? holepunchInfo.decode(state) : null, | |
| 217 | addresses4: (flags & 2) !== 0 ? ipv4Array.decode(state) : [], | |
| 218 | addresses6: (flags & 4) !== 0 ? ipv6Array.decode(state) : [], | |
| 219 | udx: (flags & 8) !== 0 ? udxInfo.decode(state) : null, | |
| 220 | secretStream: (flags & 16) !== 0 ? secretStreamInfo.decode(state) : null, | |
| 221 | relayThrough: (flags & 32) !== 0 ? relayThroughInfo.decode(state) : null, | |
| 222 | relayAddresses: (flags & 64) !== 0 ? ipv4Array.decode(state) : null | |
| 223 | } | |
| 224 | } | |
| 225 | } | |
| 226 | ||
| 227 | exports.holepunch = { | |
| 228 | preencode(state, m) { | |
| 229 | state.end += 2 | |
| 230 | c.uint.preencode(state, m.id) | |
| 231 | c.buffer.preencode(state, m.payload) | |
| 232 | if (m.peerAddress) ipv4.preencode(state, m.peerAddress) | |
| 233 | }, | |
| 234 | encode(state, m) { | |
| 235 | const flags = m.peerAddress ? 1 : 0 | |
| 236 | c.uint.encode(state, flags) | |
| 237 | c.uint.encode(state, m.mode) | |
| 238 | c.uint.encode(state, m.id) | |
| 239 | c.buffer.encode(state, m.payload) | |
| 240 | if (m.peerAddress) ipv4.encode(state, m.peerAddress) | |
| 241 | }, | |
| 242 | decode(state) { | |
| 243 | const flags = c.uint.decode(state) | |
| 244 | return { | |
| 245 | mode: c.uint.decode(state), | |
| 246 | id: c.uint.decode(state), | |
| 247 | payload: c.buffer.decode(state), | |
| 248 | peerAddress: flags & 1 ? ipv4.decode(state) : null | |
| 249 | } | |
| 250 | } | |
| 251 | } | |
| 252 | ||
| 253 | exports.holepunchPayload = { | |
| 254 | preencode(state, m) { | |
| 255 | state.end += 4 // flags + error + firewall + round | |
| 256 | if (m.addresses) ipv4Array.preencode(state, m.addresses) | |
| 257 | if (m.remoteAddress) state.end += 6 | |
| 258 | if (m.token) state.end += 32 | |
| 259 | if (m.remoteToken) state.end += 32 | |
| 260 | }, | |
| 261 | encode(state, m) { | |
| 262 | const flags = | |
| 263 | (m.connected ? 1 : 0) | | |
| 264 | (m.punching ? 2 : 0) | | |
| 265 | (m.addresses ? 4 : 0) | | |
| 266 | (m.remoteAddress ? 8 : 0) | | |
| 267 | (m.token ? 16 : 0) | | |
| 268 | (m.remoteToken ? 32 : 0) | |
| 269 | ||
| 270 | c.uint.encode(state, flags) | |
| 271 | c.uint.encode(state, m.error) | |
| 272 | c.uint.encode(state, m.firewall) | |
| 273 | c.uint.encode(state, m.round) | |
| 274 | ||
| 275 | if (m.addresses) ipv4Array.encode(state, m.addresses) | |
| 276 | if (m.remoteAddress) ipv4.encode(state, m.remoteAddress) | |
| 277 | if (m.token) c.fixed32.encode(state, m.token) | |
| 278 | if (m.remoteToken) c.fixed32.encode(state, m.remoteToken) | |
| 279 | }, | |
| 280 | decode(state) { | |
| 281 | const flags = c.uint.decode(state) | |
| 282 | ||
| 283 | return { | |
| 284 | error: c.uint.decode(state), | |
| 285 | firewall: c.uint.decode(state), | |
| 286 | round: c.uint.decode(state), | |
| 287 | connected: (flags & 1) !== 0, | |
| 288 | punching: (flags & 2) !== 0, | |
| 289 | addresses: (flags & 4) !== 0 ? ipv4Array.decode(state) : null, | |
| 290 | remoteAddress: (flags & 8) !== 0 ? ipv4.decode(state) : null, | |
| 291 | token: (flags & 16) !== 0 ? c.fixed32.decode(state) : null, | |
| 292 | remoteToken: (flags & 32) !== 0 ? c.fixed32.decode(state) : null | |
| 293 | } | |
| 294 | } | |
| 295 | } | |
| 296 | ||
| 297 | const peer = (exports.peer = { | |
| 298 | preencode(state, m) { | |
| 299 | state.end += 32 | |
| 300 | ipv4Array.preencode(state, m.relayAddresses) | |
| 301 | }, | |
| 302 | encode(state, m) { | |
| 303 | c.fixed32.encode(state, m.publicKey) | |
| 304 | ipv4Array.encode(state, m.relayAddresses) | |
| 305 | }, | |
| 306 | decode(state) { | |
| 307 | return { | |
| 308 | publicKey: c.fixed32.decode(state), | |
| 309 | relayAddresses: ipv4Array.decode(state) | |
| 310 | } | |
| 311 | } | |
| 312 | }) | |
| 313 | ||
| 314 | const peers = (exports.peers = c.array(peer)) | |
| 315 | ||
| 316 | const rawPeers = c.array(c.raw) | |
| 317 | ||
| 318 | exports.lookupRawReply = { | |
| 319 | preencode(state, m) { | |
| 320 | rawPeers.preencode(state, m.peers) | |
| 321 | c.uint.preencode(state, m.bump) | |
| 322 | }, | |
| 323 | encode(state, m) { | |
| 324 | rawPeers.encode(state, m.peers) | |
| 325 | c.uint.encode(state, m.bump) | |
| 326 | }, | |
| 327 | decode(state) { | |
| 328 | return { | |
| 329 | peers: peers.decode(state), | |
| 330 | bump: state.start < state.end ? c.uint.decode(state) : 0 | |
| 331 | } | |
| 332 | } | |
| 333 | } | |
| 334 | ||
| 335 | exports.announce = { | |
| 336 | preencode(state, m) { | |
| 337 | state.end++ // flags | |
| 338 | if (m.peer) peer.preencode(state, m.peer) | |
| 339 | if (m.refresh) state.end += 32 | |
| 340 | if (m.signature) state.end += 64 | |
| 341 | if (m.bump) c.uint.preencode(state, m.bump) | |
| 342 | }, | |
| 343 | encode(state, m) { | |
| 344 | const flags = (m.peer ? 1 : 0) | (m.refresh ? 2 : 0) | (m.signature ? 4 : 0) | (m.bump ? 8 : 0) | |
| 345 | c.uint.encode(state, flags) | |
| 346 | if (m.peer) peer.encode(state, m.peer) | |
| 347 | if (m.refresh) c.fixed32.encode(state, m.refresh) | |
| 348 | if (m.signature) c.fixed64.encode(state, m.signature) | |
| 349 | if (m.bump) c.uint.encode(state, m.bump) | |
| 350 | }, | |
| 351 | decode(state) { | |
| 352 | const flags = c.uint.decode(state) | |
| 353 | ||
| 354 | return { | |
| 355 | peer: (flags & 1) !== 0 ? peer.decode(state) : null, | |
| 356 | refresh: (flags & 2) !== 0 ? c.fixed32.decode(state) : null, | |
| 357 | signature: (flags & 4) !== 0 ? c.fixed64.decode(state) : null, | |
| 358 | bump: (flags & 8) !== 0 ? c.uint.decode(state) : 0 | |
| 359 | } | |
| 360 | } | |
| 361 | } | |
| 362 | ||
| 363 | exports.mutableSignable = { | |
| 364 | preencode(state, m) { | |
| 365 | c.uint.preencode(state, m.seq) | |
| 366 | c.buffer.preencode(state, m.value) | |
| 367 | }, | |
| 368 | encode(state, m) { | |
| 369 | c.uint.encode(state, m.seq) | |
| 370 | c.buffer.encode(state, m.value) | |
| 371 | }, | |
| 372 | decode(state) { | |
| 373 | return { | |
| 374 | seq: c.uint.decode(state), | |
| 375 | value: c.buffer.decode(state) | |
| 376 | } | |
| 377 | } | |
| 378 | } | |
| 379 | ||
| 380 | exports.mutablePutRequest = { | |
| 381 | preencode(state, m) { | |
| 382 | c.fixed32.preencode(state, m.publicKey) | |
| 383 | c.uint.preencode(state, m.seq) | |
| 384 | c.buffer.preencode(state, m.value) | |
| 385 | c.fixed64.preencode(state, m.signature) | |
| 386 | }, | |
| 387 | encode(state, m) { | |
| 388 | c.fixed32.encode(state, m.publicKey) | |
| 389 | c.uint.encode(state, m.seq) | |
| 390 | c.buffer.encode(state, m.value) | |
| 391 | c.fixed64.encode(state, m.signature) | |
| 392 | }, | |
| 393 | decode(state) { | |
| 394 | return { | |
| 395 | publicKey: c.fixed32.decode(state), | |
| 396 | seq: c.uint.decode(state), | |
| 397 | value: c.buffer.decode(state), | |
| 398 | signature: c.fixed64.decode(state) | |
| 399 | } | |
| 400 | } | |
| 401 | } | |
| 402 | ||
| 403 | exports.mutableGetResponse = { | |
| 404 | preencode(state, m) { | |
| 405 | c.uint.preencode(state, m.seq) | |
| 406 | c.buffer.preencode(state, m.value) | |
| 407 | c.fixed64.preencode(state, m.signature) | |
| 408 | }, | |
| 409 | encode(state, m) { | |
| 410 | c.uint.encode(state, m.seq) | |
| 411 | c.buffer.encode(state, m.value) | |
| 412 | c.fixed64.encode(state, m.signature) | |
| 413 | }, | |
| 414 | decode(state) { | |
| 415 | return { | |
| 416 | seq: c.uint.decode(state), | |
| 417 | value: c.buffer.decode(state), | |
| 418 | signature: c.fixed64.decode(state) | |
| 419 | } | |
| 420 | } | |
| 421 | } |
| … | … | @@ -0,0 +1,207 @@ |
| 1 | const { FIREWALL } = require('../lib/constants') | |
| 2 | ||
| 3 | module.exports = class Nat { | |
| 4 | constructor(dht, session, socket) { | |
| 5 | this._samplesHost = [] | |
| 6 | this._samplesFull = [] | |
| 7 | this._visited = new Map() | |
| 8 | this._resolve = null | |
| 9 | this._minSamples = 4 | |
| 10 | this._autoSampling = false | |
| 11 | ||
| 12 | this.dht = dht | |
| 13 | this.session = session | |
| 14 | this.socket = socket | |
| 15 | ||
| 16 | this.sampled = 0 | |
| 17 | this.firewall = dht.firewalled ? FIREWALL.UNKNOWN : FIREWALL.OPEN | |
| 18 | this.addresses = null | |
| 19 | ||
| 20 | this.analyzing = new Promise((resolve) => { | |
| 21 | this._resolve = resolve | |
| 22 | }) | |
| 23 | } | |
| 24 | ||
| 25 | autoSample(retry = true) { | |
| 26 | if (this._autoSampling) return | |
| 27 | this._autoSampling = true | |
| 28 | ||
| 29 | const self = this | |
| 30 | const socket = this.socket | |
| 31 | const maxPings = this._minSamples | |
| 32 | ||
| 33 | let skip = this.dht.nodes.length >= 8 ? 5 : 0 | |
| 34 | let pending = 0 | |
| 35 | ||
| 36 | // TODO: it would be best to pick the nodes to help us based on latency to us | |
| 37 | // That should reduce connect latency in general. We should investigate tracking that later on. | |
| 38 | ||
| 39 | // TODO 2: try to pick nodes with different IPs as well, as that'll help multi IP cell connections... | |
| 40 | // If we expose this from the nat sampler then the DHT should be able to help us filter out scams as well... | |
| 41 | ||
| 42 | for ( | |
| 43 | let node = this.dht.nodes.latest; | |
| 44 | node && this.sampled + pending < maxPings; | |
| 45 | node = node.prev | |
| 46 | ) { | |
| 47 | if (skip > 0) { | |
| 48 | skip-- | |
| 49 | continue | |
| 50 | } | |
| 51 | ||
| 52 | const ref = node.host + ':' + node.port | |
| 53 | ||
| 54 | if (this._visited.has(ref)) continue | |
| 55 | this._visited.set(ref, 1) | |
| 56 | ||
| 57 | pending++ | |
| 58 | this.session.ping(node, { socket, retry: false }).then(onpong, onskip) | |
| 59 | } | |
| 60 | ||
| 61 | pending++ | |
| 62 | onskip() | |
| 63 | ||
| 64 | function onpong(res) { | |
| 65 | self.add(res.to, res.from) | |
| 66 | onskip() | |
| 67 | } | |
| 68 | ||
| 69 | function onskip() { | |
| 70 | if (--pending === 0 && self.sampled < self._minSamples) { | |
| 71 | if (retry) { | |
| 72 | self._autoSampling = false | |
| 73 | self.autoSample(false) | |
| 74 | return | |
| 75 | } | |
| 76 | self._resolve() | |
| 77 | } | |
| 78 | } | |
| 79 | } | |
| 80 | ||
| 81 | destroy() { | |
| 82 | this._autoSampling = true | |
| 83 | this._minSamples = 0 | |
| 84 | this._resolve() | |
| 85 | } | |
| 86 | ||
| 87 | unfreeze() { | |
| 88 | this.frozen = false | |
| 89 | this._updateFirewall() | |
| 90 | this._updateAddresses() | |
| 91 | } | |
| 92 | ||
| 93 | freeze() { | |
| 94 | this.frozen = true | |
| 95 | } | |
| 96 | ||
| 97 | _updateFirewall() { | |
| 98 | if (!this.dht.firewalled) { | |
| 99 | this.firewall = FIREWALL.OPEN | |
| 100 | return | |
| 101 | } | |
| 102 | ||
| 103 | if (this.sampled < 3) return | |
| 104 | ||
| 105 | const max = this._samplesFull[0].hits | |
| 106 | ||
| 107 | if (max >= 3) { | |
| 108 | this.firewall = FIREWALL.CONSISTENT | |
| 109 | return | |
| 110 | } | |
| 111 | ||
| 112 | if (max === 1) { | |
| 113 | this.firewall = FIREWALL.RANDOM | |
| 114 | return | |
| 115 | } | |
| 116 | ||
| 117 | // else max === 2 | |
| 118 | ||
| 119 | // 1 host, >= 4 total samples ie, 2 bad ones -> random | |
| 120 | if (this._samplesHost.length === 1 && this.sampled > 3) { | |
| 121 | this.firewall = FIREWALL.RANDOM | |
| 122 | return | |
| 123 | } | |
| 124 | ||
| 125 | // double hit on two different ips -> assume consistent | |
| 126 | if (this._samplesHost.length > 1 && this._samplesFull[1].hits > 1) { | |
| 127 | this.firewall = FIREWALL.CONSISTENT | |
| 128 | return | |
| 129 | } | |
| 130 | ||
| 131 | // (4 is just means - all the samples we expect) - no decision - assume random | |
| 132 | if (this.sampled > 4) { | |
| 133 | this.firewall = FIREWALL.RANDOM | |
| 134 | } | |
| 135 | } | |
| 136 | ||
| 137 | _updateAddresses() { | |
| 138 | if (this.firewall === FIREWALL.UNKNOWN) { | |
| 139 | this.addresses = null | |
| 140 | return | |
| 141 | } | |
| 142 | ||
| 143 | if (this.firewall === FIREWALL.RANDOM) { | |
| 144 | this.addresses = [this._samplesHost[0]] | |
| 145 | return | |
| 146 | } | |
| 147 | ||
| 148 | if (this.firewall === FIREWALL.CONSISTENT) { | |
| 149 | this.addresses = [] | |
| 150 | for (const addr of this._samplesFull) { | |
| 151 | if (addr.hits >= 2 || this.addresses.length < 2) this.addresses.push(addr) | |
| 152 | } | |
| 153 | } | |
| 154 | } | |
| 155 | ||
| 156 | update() { | |
| 157 | if (this.dht.firewalled && this.firewall === FIREWALL.OPEN) { | |
| 158 | this.firewall = FIREWALL.UNKNOWN | |
| 159 | } | |
| 160 | this._updateFirewall() | |
| 161 | this._updateAddresses() | |
| 162 | } | |
| 163 | ||
| 164 | add(addr, from) { | |
| 165 | const ref = from.host + ':' + from.port | |
| 166 | ||
| 167 | if (this._visited.get(ref) === 2) return | |
| 168 | this._visited.set(ref, 2) | |
| 169 | ||
| 170 | addSample(this._samplesHost, addr.host, 0) | |
| 171 | addSample(this._samplesFull, addr.host, addr.port) | |
| 172 | ||
| 173 | if ((++this.sampled >= 3 || !this.dht.firewalled) && !this.frozen) { | |
| 174 | this.update() | |
| 175 | } | |
| 176 | ||
| 177 | if (this.firewall === FIREWALL.CONSISTENT || this.firewall === FIREWALL.OPEN) { | |
| 178 | this._resolve() | |
| 179 | } else if (this.sampled >= this._minSamples) { | |
| 180 | this._resolve() | |
| 181 | } | |
| 182 | } | |
| 183 | } | |
| 184 | ||
| 185 | function addSample(samples, host, port) { | |
| 186 | for (let i = 0; i < samples.length; i++) { | |
| 187 | const s = samples[i] | |
| 188 | ||
| 189 | if (s.port !== port || s.host !== host) continue | |
| 190 | s.hits++ | |
| 191 | ||
| 192 | for (; i > 0; i--) { | |
| 193 | const prev = samples[i - 1] | |
| 194 | if (prev.hits >= s.hits) return | |
| 195 | samples[i - 1] = s | |
| 196 | samples[i] = prev | |
| 197 | } | |
| 198 | ||
| 199 | return | |
| 200 | } | |
| 201 | ||
| 202 | samples.push({ | |
| 203 | host, | |
| 204 | port, | |
| 205 | hits: 1 | |
| 206 | }) | |
| 207 | } |
| … | … | @@ -0,0 +1,52 @@ |
| 1 | const NoiseSecretStream = require('@hyperswarm/secret-stream') | |
| 2 | const NoiseHandshake = require('noise-handshake') | |
| 3 | const curve = require('noise-curve-ed') | |
| 4 | const c = require('compact-encoding') | |
| 5 | const b4a = require('b4a') | |
| 6 | const sodium = require('sodium-universal') | |
| 7 | const m = require('./messages') | |
| 8 | const { NS } = require('./constants') | |
| 9 | const { HANDSHAKE_UNFINISHED } = require('./errors') | |
| 10 | ||
| 11 | const NOISE_PROLOUGE = NS.PEER_HANDSHAKE | |
| 12 | ||
| 13 | module.exports = class NoiseWrap { | |
| 14 | constructor(keyPair, remotePublicKey) { | |
| 15 | this.isInitiator = !!remotePublicKey | |
| 16 | this.remotePublicKey = remotePublicKey | |
| 17 | this.keyPair = keyPair | |
| 18 | this.handshake = new NoiseHandshake('IK', this.isInitiator, keyPair, { curve }) | |
| 19 | this.handshake.initialise(NOISE_PROLOUGE, remotePublicKey) | |
| 20 | } | |
| 21 | ||
| 22 | send(payload) { | |
| 23 | const buf = c.encode(m.noisePayload, payload) | |
| 24 | return this.handshake.send(buf) | |
| 25 | } | |
| 26 | ||
| 27 | recv(buf) { | |
| 28 | const payload = c.decode(m.noisePayload, this.handshake.recv(buf)) | |
| 29 | this.remotePublicKey = b4a.toBuffer(this.handshake.rs) | |
| 30 | return payload | |
| 31 | } | |
| 32 | ||
| 33 | final() { | |
| 34 | if (!this.handshake.complete) throw HANDSHAKE_UNFINISHED() | |
| 35 | ||
| 36 | const holepunchSecret = b4a.allocUnsafe(32) | |
| 37 | ||
| 38 | sodium.crypto_generichash(holepunchSecret, NS.PEER_HOLEPUNCH, this.handshake.hash) | |
| 39 | ||
| 40 | return { | |
| 41 | isInitiator: this.isInitiator, | |
| 42 | publicKey: this.keyPair.publicKey, | |
| 43 | streamId: this.streamId, | |
| 44 | remotePublicKey: this.remotePublicKey, | |
| 45 | remoteId: NoiseSecretStream.id(this.handshake.hash, !this.isInitiator), | |
| 46 | holepunchSecret, | |
| 47 | hash: b4a.toBuffer(this.handshake.hash), | |
| 48 | rx: b4a.toBuffer(this.handshake.rx), | |
| 49 | tx: b4a.toBuffer(this.handshake.tx) | |
| 50 | } | |
| 51 | } | |
| 52 | } |
| … | … | @@ -0,0 +1,303 @@ |
| 1 | const c = require('compact-encoding') | |
| 2 | const sodium = require('sodium-universal') | |
| 3 | const RecordCache = require('record-cache') | |
| 4 | const Cache = require('xache') | |
| 5 | const b4a = require('b4a') | |
| 6 | const unslab = require('unslab') | |
| 7 | ||
| 8 | const { encodeUnslab } = require('./encode') | |
| 9 | const m = require('./messages') | |
| 10 | const { NS, ERROR } = require('./constants') | |
| 11 | ||
| 12 | const EMPTY = b4a.alloc(0) | |
| 13 | const TMP = b4a.allocUnsafe(32) | |
| 14 | const MAX_BUMP_DRIFT = 60_000 | |
| 15 | ||
| 16 | module.exports = class Persistent { | |
| 17 | constructor(dht, opts) { | |
| 18 | this.dht = dht | |
| 19 | this.records = new RecordCache(opts.records) | |
| 20 | this.bumps = new Cache(opts.bumps) | |
| 21 | this.refreshes = new Cache(opts.refreshes) | |
| 22 | this.mutables = new Cache(opts.mutables) | |
| 23 | this.immutables = new Cache(opts.immutables) | |
| 24 | } | |
| 25 | ||
| 26 | onlookup(req) { | |
| 27 | if (!req.target) return | |
| 28 | ||
| 29 | const k = b4a.toString(req.target, 'hex') | |
| 30 | const records = this.records.get(k, 20) | |
| 31 | const bump = this.bumps.get(k) || 0 | |
| 32 | const fwd = this.dht._router.get(k) | |
| 33 | ||
| 34 | if (fwd && records.length < 20) records.push(fwd.record) | |
| 35 | ||
| 36 | req.reply(records.length ? c.encode(m.lookupRawReply, { peers: records, bump }) : null) | |
| 37 | } | |
| 38 | ||
| 39 | onfindpeer(req) { | |
| 40 | if (!req.target) return | |
| 41 | const fwd = this.dht._router.get(req.target) | |
| 42 | req.reply(fwd ? fwd.record : null) | |
| 43 | } | |
| 44 | ||
| 45 | unannounce(target, publicKey) { | |
| 46 | const k = b4a.toString(target, 'hex') | |
| 47 | sodium.crypto_generichash(TMP, publicKey) | |
| 48 | ||
| 49 | if (b4a.equals(TMP, target)) this.dht._router.delete(k) | |
| 50 | this.records.remove(k, publicKey) | |
| 51 | } | |
| 52 | ||
| 53 | onunannounce(req) { | |
| 54 | if (!req.target || !req.token) return | |
| 55 | ||
| 56 | const unann = decode(m.announce, req.value) | |
| 57 | if (unann === null) return | |
| 58 | ||
| 59 | const { peer, signature } = unann | |
| 60 | if (!peer || !signature) return | |
| 61 | ||
| 62 | const signable = annSignable(req.target, req.token, this.dht.id, unann, NS.UNANNOUNCE) | |
| 63 | ||
| 64 | if (!sodium.crypto_sign_verify_detached(signature, signable, peer.publicKey)) { | |
| 65 | return | |
| 66 | } | |
| 67 | ||
| 68 | this.unannounce(req.target, peer.publicKey) | |
| 69 | req.reply(null, { token: false, closerNodes: false }) | |
| 70 | } | |
| 71 | ||
| 72 | _onrefresh(token, req) { | |
| 73 | sodium.crypto_generichash(TMP, token) | |
| 74 | const activeRefresh = b4a.toString(TMP, 'hex') | |
| 75 | ||
| 76 | const r = this.refreshes.get(activeRefresh) | |
| 77 | if (!r) return | |
| 78 | ||
| 79 | const { announceSelf, k, record } = r | |
| 80 | const publicKey = record.subarray(0, 32) | |
| 81 | ||
| 82 | if (announceSelf) { | |
| 83 | this.dht._router.set(k, { | |
| 84 | relay: req.from, | |
| 85 | record, | |
| 86 | onconnect: null, | |
| 87 | onholepunch: null | |
| 88 | }) | |
| 89 | this.records.remove(k, publicKey) | |
| 90 | } else { | |
| 91 | this.records.add(k, publicKey, record) | |
| 92 | } | |
| 93 | ||
| 94 | this.refreshes.delete(activeRefresh) | |
| 95 | this.refreshes.set(b4a.toString(token, 'hex'), r) | |
| 96 | ||
| 97 | req.reply(null, { token: false, closerNodes: false }) | |
| 98 | } | |
| 99 | ||
| 100 | onannounce(req) { | |
| 101 | if (!req.target || !req.token || !this.dht.id) return | |
| 102 | ||
| 103 | const ann = decode(m.announce, req.value) | |
| 104 | if (ann === null) return | |
| 105 | ||
| 106 | const { peer, refresh, signature, bump } = ann | |
| 107 | ||
| 108 | if (!peer) { | |
| 109 | if (!refresh) return | |
| 110 | this._onrefresh(refresh, req) | |
| 111 | return | |
| 112 | } | |
| 113 | ||
| 114 | const signable = annSignable(req.target, req.token, this.dht.id, ann, NS.ANNOUNCE) | |
| 115 | ||
| 116 | if (!signature || !sodium.crypto_sign_verify_detached(signature, signable, peer.publicKey)) { | |
| 117 | return | |
| 118 | } | |
| 119 | ||
| 120 | // TODO: it would be potentially be more optimal to allow more than 3 addresses here for a findPeer response | |
| 121 | // and only use max 3 for a lookup reply | |
| 122 | if (peer.relayAddresses.length > 3) { | |
| 123 | peer.relayAddresses = peer.relayAddresses.slice(0, 3) | |
| 124 | } | |
| 125 | ||
| 126 | sodium.crypto_generichash(TMP, peer.publicKey) | |
| 127 | ||
| 128 | const k = b4a.toString(req.target, 'hex') | |
| 129 | const announceSelf = b4a.equals(TMP, req.target) | |
| 130 | const record = encodeUnslab(m.peer, peer) | |
| 131 | ||
| 132 | if (announceSelf) { | |
| 133 | this.dht._router.set(k, { | |
| 134 | relay: req.from, | |
| 135 | record, | |
| 136 | onconnect: null, | |
| 137 | onholepunch: null | |
| 138 | }) | |
| 139 | this.records.remove(k, peer.publicKey) | |
| 140 | } else { | |
| 141 | const currentBump = this.bumps.get(k) || 0 | |
| 142 | if (bump > currentBump && bump <= Date.now() + MAX_BUMP_DRIFT) this.bumps.set(k, bump) | |
| 143 | this.records.add(k, peer.publicKey, record) | |
| 144 | } | |
| 145 | ||
| 146 | if (refresh) { | |
| 147 | this.refreshes.set(b4a.toString(refresh, 'hex'), { k, record, announceSelf }) | |
| 148 | } | |
| 149 | ||
| 150 | req.reply(null, { token: false, closerNodes: false }) | |
| 151 | } | |
| 152 | ||
| 153 | onmutableget(req) { | |
| 154 | if (!req.target || !req.value) return | |
| 155 | ||
| 156 | let seq = 0 | |
| 157 | try { | |
| 158 | seq = c.decode(c.uint, req.value) | |
| 159 | } catch { | |
| 160 | return | |
| 161 | } | |
| 162 | ||
| 163 | const k = b4a.toString(req.target, 'hex') | |
| 164 | const value = this.mutables.get(k) | |
| 165 | ||
| 166 | if (!value) { | |
| 167 | req.reply(null) | |
| 168 | return | |
| 169 | } | |
| 170 | ||
| 171 | const localSeq = c.decode(c.uint, value) | |
| 172 | req.reply(localSeq < seq ? null : value) | |
| 173 | } | |
| 174 | ||
| 175 | onmutableput(req) { | |
| 176 | if (!req.target || !req.token || !req.value) return | |
| 177 | ||
| 178 | const p = decode(m.mutablePutRequest, req.value) | |
| 179 | if (!p) return | |
| 180 | ||
| 181 | const { publicKey, seq, value, signature } = p | |
| 182 | ||
| 183 | const hash = b4a.allocUnsafe(32) | |
| 184 | sodium.crypto_generichash(hash, publicKey) | |
| 185 | if (!b4a.equals(hash, req.target)) return | |
| 186 | ||
| 187 | if (!value || !verifyMutable(signature, seq, value, publicKey)) return | |
| 188 | ||
| 189 | const k = b4a.toString(hash, 'hex') | |
| 190 | const local = this.mutables.get(k) | |
| 191 | ||
| 192 | if (local) { | |
| 193 | const existing = c.decode(m.mutableGetResponse, local) | |
| 194 | if (existing.value && existing.seq === seq && b4a.compare(value, existing.value) !== 0) { | |
| 195 | req.error(ERROR.SEQ_REUSED) | |
| 196 | return | |
| 197 | } | |
| 198 | if (seq < existing.seq) { | |
| 199 | req.error(ERROR.SEQ_TOO_LOW) | |
| 200 | return | |
| 201 | } | |
| 202 | } | |
| 203 | ||
| 204 | this.mutables.set(k, encodeUnslab(m.mutableGetResponse, { seq, value, signature })) | |
| 205 | req.reply(null) | |
| 206 | } | |
| 207 | ||
| 208 | onimmutableget(req) { | |
| 209 | if (!req.target) return | |
| 210 | ||
| 211 | const k = b4a.toString(req.target, 'hex') | |
| 212 | const value = this.immutables.get(k) | |
| 213 | ||
| 214 | req.reply(value || null) | |
| 215 | } | |
| 216 | ||
| 217 | onimmutableput(req) { | |
| 218 | if (!req.target || !req.token || !req.value) return | |
| 219 | ||
| 220 | const hash = b4a.alloc(32) | |
| 221 | sodium.crypto_generichash(hash, req.value) | |
| 222 | if (!b4a.equals(hash, req.target)) return | |
| 223 | ||
| 224 | const k = b4a.toString(hash, 'hex') | |
| 225 | this.immutables.set(k, unslab(req.value)) | |
| 226 | ||
| 227 | req.reply(null) | |
| 228 | } | |
| 229 | ||
| 230 | destroy() { | |
| 231 | this.records.destroy() | |
| 232 | this.refreshes.destroy() | |
| 233 | this.mutables.destroy() | |
| 234 | this.immutables.destroy() | |
| 235 | } | |
| 236 | ||
| 237 | static signMutable(seq, value, keyPair) { | |
| 238 | const signable = b4a.allocUnsafe(32 + 32) | |
| 239 | const hash = signable.subarray(32) | |
| 240 | ||
| 241 | signable.set(NS.MUTABLE_PUT, 0) | |
| 242 | ||
| 243 | sodium.crypto_generichash(hash, c.encode(m.mutableSignable, { seq, value })) | |
| 244 | return sign(signable, keyPair) | |
| 245 | } | |
| 246 | ||
| 247 | static verifyMutable(signature, seq, value, publicKey) { | |
| 248 | return verifyMutable(signature, seq, value, publicKey) | |
| 249 | } | |
| 250 | ||
| 251 | static signAnnounce(target, token, id, ann, keyPair) { | |
| 252 | return sign(annSignable(target, token, id, ann, NS.ANNOUNCE), keyPair) | |
| 253 | } | |
| 254 | ||
| 255 | static signUnannounce(target, token, id, ann, keyPair) { | |
| 256 | return sign(annSignable(target, token, id, ann, NS.UNANNOUNCE), keyPair) | |
| 257 | } | |
| 258 | } | |
| 259 | ||
| 260 | function verifyMutable(signature, seq, value, publicKey) { | |
| 261 | const signable = b4a.allocUnsafe(32 + 32) | |
| 262 | const hash = signable.subarray(32) | |
| 263 | ||
| 264 | signable.set(NS.MUTABLE_PUT, 0) | |
| 265 | ||
| 266 | sodium.crypto_generichash(hash, c.encode(m.mutableSignable, { seq, value })) | |
| 267 | return sodium.crypto_sign_verify_detached(signature, signable, publicKey) | |
| 268 | } | |
| 269 | ||
| 270 | function annSignable(target, token, id, ann, ns) { | |
| 271 | const signable = b4a.allocUnsafe(32 + 32) | |
| 272 | const hash = signable.subarray(32) | |
| 273 | ||
| 274 | signable.set(ns, 0) | |
| 275 | ||
| 276 | sodium.crypto_generichash_batch(hash, [ | |
| 277 | target, | |
| 278 | id, | |
| 279 | token, | |
| 280 | c.encode(m.peer, ann.peer), // note that this is the partial encoding of the announce message so we could just use that for perf | |
| 281 | ann.refresh || EMPTY | |
| 282 | ]) | |
| 283 | ||
| 284 | return signable | |
| 285 | } | |
| 286 | ||
| 287 | function sign(signable, keyPair) { | |
| 288 | if (keyPair.sign) { | |
| 289 | return keyPair.sign(signable) | |
| 290 | } | |
| 291 | const secretKey = keyPair.secretKey ? keyPair.secretKey : keyPair | |
| 292 | const signature = b4a.allocUnsafe(64) | |
| 293 | sodium.crypto_sign_detached(signature, signable, secretKey) | |
| 294 | return signature | |
| 295 | } | |
| 296 | ||
| 297 | function decode(enc, val) { | |
| 298 | try { | |
| 299 | return val && c.decode(enc, val) | |
| 300 | } catch (err) { | |
| 301 | return null | |
| 302 | } | |
| 303 | } |
| … | … | @@ -0,0 +1,66 @@ |
| 1 | module.exports = class RawStreamSet { | |
| 2 | constructor(dht) { | |
| 3 | this._dht = dht | |
| 4 | ||
| 5 | this._prefix = 16 - 1 // 16 is the default stream-set side in udx | |
| 6 | this._streams = new Map() | |
| 7 | } | |
| 8 | ||
| 9 | get size() { | |
| 10 | return this._streams.size | |
| 11 | } | |
| 12 | ||
| 13 | [Symbol.iterator]() { | |
| 14 | return this._streams.values() | |
| 15 | } | |
| 16 | ||
| 17 | add(opts) { | |
| 18 | const self = this | |
| 19 | ||
| 20 | // TODO: we should prob have a udx helper for id generation, given the slight complexity | |
| 21 | // of the below. requires a PRNG in udx tho. | |
| 22 | ||
| 23 | let id = 0 | |
| 24 | ||
| 25 | while (true) { | |
| 26 | id = (Math.random() * 0x100000000) >>> 0 | |
| 27 | ||
| 28 | if (this._streams.has(id & this._prefix)) continue | |
| 29 | break | |
| 30 | } | |
| 31 | ||
| 32 | // always have ~50% change of rolling a free one | |
| 33 | if (2 * this._streams.size >= this._prefix) { | |
| 34 | // ie 0b11111 = 0b1111 + 1 + 0b1111 | |
| 35 | this._prefix = 2 * this._prefix + 1 | |
| 36 | ||
| 37 | // move the prefixes over | |
| 38 | const next = new Map() | |
| 39 | for (const stream of this._streams.values()) { | |
| 40 | next.set(stream.id & this._prefix, stream) | |
| 41 | } | |
| 42 | this._streams = next | |
| 43 | } | |
| 44 | ||
| 45 | const stream = this._dht.udx.createStream(id, opts) | |
| 46 | this._streams.set(id & this._prefix, stream) | |
| 47 | ||
| 48 | stream.on('close', onclose) | |
| 49 | ||
| 50 | return stream | |
| 51 | ||
| 52 | function onclose() { | |
| 53 | self._streams.delete(id & self._prefix) | |
| 54 | } | |
| 55 | } | |
| 56 | ||
| 57 | async clear() { | |
| 58 | const destroying = [] | |
| 59 | ||
| 60 | for (const stream of this._streams.values()) { | |
| 61 | destroying.push(new Promise((resolve) => stream.once('close', resolve).destroy())) | |
| 62 | } | |
| 63 | ||
| 64 | await Promise.allSettled(destroying) | |
| 65 | } | |
| 66 | } |
| … | … | @@ -0,0 +1,20 @@ |
| 1 | const sodium = require('sodium-universal') | |
| 2 | const b4a = require('b4a') | |
| 3 | ||
| 4 | module.exports = function createRefreshChain(cnt) { | |
| 5 | const blocks = new Array(cnt) | |
| 6 | if (!blocks.length) return blocks | |
| 7 | ||
| 8 | const all = b4a.allocUnsafe(cnt * 32 + 32) | |
| 9 | ||
| 10 | let prev = all.subarray(all.byteLength - 32) | |
| 11 | sodium.randombytes_buf(prev) | |
| 12 | ||
| 13 | for (let i = cnt - 1; i >= 0; i--) { | |
| 14 | blocks[i] = all.subarray(32 * i, 32 * i + 32) | |
| 15 | sodium.crypto_generichash(blocks[i], prev) | |
| 16 | prev = blocks[i] | |
| 17 | } | |
| 18 | ||
| 19 | return blocks | |
| 20 | } |
| … | … | @@ -0,0 +1,261 @@ |
| 1 | const c = require('compact-encoding') | |
| 2 | const Cache = require('xache') | |
| 3 | const safetyCatch = require('safety-catch') | |
| 4 | const b4a = require('b4a') | |
| 5 | const { handshake, holepunch } = require('./messages') | |
| 6 | const { COMMANDS } = require('./constants') | |
| 7 | const { BAD_HANDSHAKE_REPLY, BAD_HOLEPUNCH_REPLY } = require('./errors') | |
| 8 | ||
| 9 | const FROM_CLIENT = 0 | |
| 10 | const FROM_SERVER = 1 | |
| 11 | const FROM_RELAY = 2 | |
| 12 | const FROM_SECOND_RELAY = 3 | |
| 13 | const REPLY = 4 | |
| 14 | ||
| 15 | // TODO: While the current design is very trustless in regards to clients/servers trusting the DHT, | |
| 16 | // we should add a bunch of rate limits everywhere, especially including here to avoid bad users | |
| 17 | // using a DHT node to relay traffic indiscriminately using the connect/holepunch messages. | |
| 18 | // That's mostly from an abuse POV as none of the messsages do amplication. | |
| 19 | ||
| 20 | module.exports = class Router { | |
| 21 | constructor(dht, opts) { | |
| 22 | this.dht = dht | |
| 23 | this.forwards = new Cache(opts.forwards) | |
| 24 | } | |
| 25 | ||
| 26 | set(target, state) { | |
| 27 | if (state.onpeerhandshake) { | |
| 28 | this.forwards.retain(toString(target), state) | |
| 29 | } else { | |
| 30 | this.forwards.set(toString(target), state) | |
| 31 | } | |
| 32 | } | |
| 33 | ||
| 34 | get(target) { | |
| 35 | return this.forwards.get(toString(target)) | |
| 36 | } | |
| 37 | ||
| 38 | delete(target) { | |
| 39 | this.forwards.delete(toString(target)) | |
| 40 | } | |
| 41 | ||
| 42 | destroy() { | |
| 43 | this.forwards.destroy() | |
| 44 | } | |
| 45 | ||
| 46 | async peerHandshake(target, { noise, peerAddress, relayAddress, socket, session }, to) { | |
| 47 | const dht = this.dht | |
| 48 | ||
| 49 | const requestValue = c.encode(handshake, { | |
| 50 | mode: FROM_CLIENT, | |
| 51 | noise, | |
| 52 | peerAddress, | |
| 53 | relayAddress | |
| 54 | }) | |
| 55 | ||
| 56 | const res = await dht.request( | |
| 57 | { command: COMMANDS.PEER_HANDSHAKE, target, value: requestValue }, | |
| 58 | to, | |
| 59 | { socket, session } | |
| 60 | ) | |
| 61 | ||
| 62 | const hs = decode(handshake, res.value) | |
| 63 | if ( | |
| 64 | !hs || | |
| 65 | hs.mode !== REPLY || | |
| 66 | to.host !== res.from.host || | |
| 67 | to.port !== res.from.port || | |
| 68 | !hs.noise | |
| 69 | ) { | |
| 70 | throw BAD_HANDSHAKE_REPLY() | |
| 71 | } | |
| 72 | ||
| 73 | return { | |
| 74 | noise: hs.noise, | |
| 75 | relayed: !!hs.peerAddress, | |
| 76 | serverAddress: hs.peerAddress || to, | |
| 77 | clientAddress: res.to | |
| 78 | } | |
| 79 | } | |
| 80 | ||
| 81 | async onpeerhandshake(req) { | |
| 82 | const hs = req.value && decode(handshake, req.value) | |
| 83 | if (!hs) return | |
| 84 | ||
| 85 | const { mode, noise, peerAddress, relayAddress } = hs | |
| 86 | ||
| 87 | const state = req.target && this.get(req.target) | |
| 88 | const isServer = !!(state && state.onpeerhandshake) | |
| 89 | const relay = state && state.relay | |
| 90 | ||
| 91 | if (isServer) { | |
| 92 | let reply = null | |
| 93 | try { | |
| 94 | reply = noise && (await state.onpeerhandshake({ noise, peerAddress }, req)) | |
| 95 | } catch (e) { | |
| 96 | safetyCatch(e) | |
| 97 | return | |
| 98 | } | |
| 99 | if (!reply || !reply.noise) return | |
| 100 | const opts = { socket: reply.socket, closerNodes: false, token: false } | |
| 101 | ||
| 102 | switch (mode) { | |
| 103 | case FROM_CLIENT: { | |
| 104 | req.reply( | |
| 105 | c.encode(handshake, { mode: REPLY, noise: reply.noise, peerAddress: null }), | |
| 106 | opts | |
| 107 | ) | |
| 108 | return | |
| 109 | } | |
| 110 | case FROM_RELAY: { | |
| 111 | req.relay( | |
| 112 | c.encode(handshake, { mode: FROM_SERVER, noise: reply.noise, peerAddress }), | |
| 113 | req.from, | |
| 114 | opts | |
| 115 | ) | |
| 116 | return | |
| 117 | } | |
| 118 | case FROM_SECOND_RELAY: { | |
| 119 | if (!relayAddress) return | |
| 120 | req.relay( | |
| 121 | c.encode(handshake, { mode: FROM_SERVER, noise: reply.noise, peerAddress }), | |
| 122 | relayAddress, | |
| 123 | opts | |
| 124 | ) | |
| 125 | return // eslint-disable-line | |
| 126 | } | |
| 127 | } | |
| 128 | } else { | |
| 129 | switch (mode) { | |
| 130 | case FROM_CLIENT: { | |
| 131 | // TODO: if no relay is known route closer to the target instead of timing out | |
| 132 | if (!noise) return | |
| 133 | if (!relay && !relayAddress) { | |
| 134 | // help the user route | |
| 135 | req.reply(null, { token: false, closerNodes: true }) | |
| 136 | return | |
| 137 | } | |
| 138 | req.relay( | |
| 139 | c.encode(handshake, { | |
| 140 | mode: FROM_RELAY, | |
| 141 | noise, | |
| 142 | peerAddress: req.from, | |
| 143 | relayAddress: null | |
| 144 | }), | |
| 145 | relayAddress || relay | |
| 146 | ) | |
| 147 | return | |
| 148 | } | |
| 149 | case FROM_RELAY: { | |
| 150 | if (!relay || !noise) return | |
| 151 | req.relay( | |
| 152 | c.encode(handshake, { | |
| 153 | mode: FROM_SECOND_RELAY, | |
| 154 | noise, | |
| 155 | peerAddress, | |
| 156 | relayAddress: req.from | |
| 157 | }), | |
| 158 | relay | |
| 159 | ) | |
| 160 | return | |
| 161 | } | |
| 162 | case FROM_SERVER: { | |
| 163 | if (!peerAddress || !noise) return | |
| 164 | req.reply( | |
| 165 | c.encode(handshake, { mode: REPLY, noise, peerAddress: req.from, relayAddress: null }), | |
| 166 | { to: peerAddress, closerNodes: false, token: false } | |
| 167 | ) | |
| 168 | return // eslint-disable-line | |
| 169 | } | |
| 170 | } | |
| 171 | } | |
| 172 | } | |
| 173 | ||
| 174 | async peerHolepunch(target, { id, payload, peerAddress, socket, session }, to) { | |
| 175 | const dht = this.dht | |
| 176 | const requestValue = c.encode(holepunch, { | |
| 177 | mode: FROM_CLIENT, | |
| 178 | id, | |
| 179 | payload, | |
| 180 | peerAddress | |
| 181 | }) | |
| 182 | ||
| 183 | const res = await dht.request( | |
| 184 | { command: COMMANDS.PEER_HOLEPUNCH, target, value: requestValue }, | |
| 185 | to, | |
| 186 | { socket, session } | |
| 187 | ) | |
| 188 | ||
| 189 | const hp = decode(holepunch, res.value) | |
| 190 | if (!hp || hp.mode !== REPLY || to.host !== res.from.host || to.port !== res.from.port) { | |
| 191 | throw BAD_HOLEPUNCH_REPLY() | |
| 192 | } | |
| 193 | ||
| 194 | return { | |
| 195 | from: res.from, | |
| 196 | to: res.to, | |
| 197 | payload: hp.payload, | |
| 198 | peerAddress: hp.peerAddress || to | |
| 199 | } | |
| 200 | } | |
| 201 | ||
| 202 | async onpeerholepunch(req) { | |
| 203 | const hp = req.value && decode(holepunch, req.value) | |
| 204 | if (!hp) return | |
| 205 | ||
| 206 | const { mode, id, payload, peerAddress } = hp | |
| 207 | ||
| 208 | const state = req.target && this.get(req.target) | |
| 209 | const isServer = !!(state && state.onpeerholepunch) | |
| 210 | const relay = state && state.relay | |
| 211 | ||
| 212 | switch (mode) { | |
| 213 | case FROM_CLIENT: { | |
| 214 | if (!peerAddress && !relay) return | |
| 215 | req.relay( | |
| 216 | c.encode(holepunch, { mode: FROM_RELAY, id, payload, peerAddress: req.from }), | |
| 217 | peerAddress || relay | |
| 218 | ) | |
| 219 | return | |
| 220 | } | |
| 221 | case FROM_RELAY: { | |
| 222 | if (!isServer || !peerAddress) return | |
| 223 | let reply = null | |
| 224 | try { | |
| 225 | reply = await state.onpeerholepunch({ id, payload, peerAddress }, req) | |
| 226 | } catch (e) { | |
| 227 | safetyCatch(e) | |
| 228 | return | |
| 229 | } | |
| 230 | if (!reply) return | |
| 231 | const opts = { socket: reply.socket, closerNodes: false, token: false } | |
| 232 | req.relay( | |
| 233 | c.encode(holepunch, { mode: FROM_SERVER, id: 0, payload: reply.payload, peerAddress }), | |
| 234 | req.from, | |
| 235 | opts | |
| 236 | ) | |
| 237 | return | |
| 238 | } | |
| 239 | case FROM_SERVER: { | |
| 240 | req.reply(c.encode(holepunch, { mode: REPLY, id, payload, peerAddress: req.from }), { | |
| 241 | to: peerAddress, | |
| 242 | closerNodes: false, | |
| 243 | token: false | |
| 244 | }) | |
| 245 | return // eslint-disable-line | |
| 246 | } | |
| 247 | } | |
| 248 | } | |
| 249 | } | |
| 250 | ||
| 251 | function decode(enc, val) { | |
| 252 | try { | |
| 253 | return c.decode(enc, val) | |
| 254 | } catch { | |
| 255 | return null | |
| 256 | } | |
| 257 | } | |
| 258 | ||
| 259 | function toString(t) { | |
| 260 | return typeof t === 'string' ? t : b4a.toString(t, 'hex') | |
| 261 | } |
| … | … | @@ -0,0 +1,52 @@ |
| 1 | const sodium = require('sodium-universal') | |
| 2 | const b4a = require('b4a') | |
| 3 | const { holepunchPayload } = require('./messages') | |
| 4 | ||
| 5 | module.exports = class HolepunchPayload { | |
| 6 | constructor(holepunchSecret) { | |
| 7 | this._sharedSecret = holepunchSecret | |
| 8 | this._localSecret = b4a.allocUnsafe(32) | |
| 9 | ||
| 10 | sodium.randombytes_buf(this._localSecret) | |
| 11 | } | |
| 12 | ||
| 13 | decrypt(buffer) { | |
| 14 | const state = { start: 24, end: buffer.byteLength - 16, buffer } | |
| 15 | ||
| 16 | if (state.end <= state.start) return null | |
| 17 | ||
| 18 | const nonce = buffer.subarray(0, 24) | |
| 19 | const msg = state.buffer.subarray(state.start, state.end) | |
| 20 | const cipher = state.buffer.subarray(state.start) | |
| 21 | ||
| 22 | if (!sodium.crypto_secretbox_open_easy(msg, cipher, nonce, this._sharedSecret)) return null | |
| 23 | ||
| 24 | try { | |
| 25 | return holepunchPayload.decode(state) | |
| 26 | } catch { | |
| 27 | return null | |
| 28 | } | |
| 29 | } | |
| 30 | ||
| 31 | encrypt(payload) { | |
| 32 | const state = { start: 24, end: 24, buffer: null } | |
| 33 | holepunchPayload.preencode(state, payload) | |
| 34 | state.buffer = b4a.allocUnsafe(state.end + 16) | |
| 35 | ||
| 36 | const nonce = state.buffer.subarray(0, 24) | |
| 37 | const msg = state.buffer.subarray(state.start, state.end) | |
| 38 | const cipher = state.buffer.subarray(state.start) | |
| 39 | ||
| 40 | holepunchPayload.encode(state, payload) | |
| 41 | sodium.randombytes_buf(nonce) | |
| 42 | sodium.crypto_secretbox_easy(cipher, msg, nonce, this._sharedSecret) | |
| 43 | ||
| 44 | return state.buffer | |
| 45 | } | |
| 46 | ||
| 47 | token(addr) { | |
| 48 | const out = b4a.allocUnsafe(32) | |
| 49 | sodium.crypto_generichash(out, b4a.from(addr.host), this._localSecret) | |
| 50 | return out | |
| 51 | } | |
| 52 | } |
| … | … | @@ -0,0 +1,69 @@ |
| 1 | const DONE = Promise.resolve(true) | |
| 2 | const DESTROYED = Promise.resolve(false) | |
| 3 | ||
| 4 | module.exports = class Semaphore { | |
| 5 | constructor(limit = 1) { | |
| 6 | this.limit = limit | |
| 7 | this.active = 0 | |
| 8 | this.waiting = [] | |
| 9 | ||
| 10 | this.flushedPromise = null | |
| 11 | this.flushedResolve = null | |
| 12 | ||
| 13 | this.destroyed = false | |
| 14 | ||
| 15 | this._onwait = this._queueWaiting.bind(this) | |
| 16 | this._onflush = this._queueFlushed.bind(this) | |
| 17 | } | |
| 18 | ||
| 19 | _queueWaiting(resolve) { | |
| 20 | this.waiting.push(resolve) | |
| 21 | } | |
| 22 | ||
| 23 | _queueFlushed(resolve) { | |
| 24 | this.flushedResolve = resolve | |
| 25 | } | |
| 26 | ||
| 27 | wait() { | |
| 28 | if (this.destroyed === true) return DESTROYED | |
| 29 | ||
| 30 | if (this.active < this.limit && this.waiting.length === 0) { | |
| 31 | this.active++ | |
| 32 | return DONE | |
| 33 | } | |
| 34 | ||
| 35 | return new Promise(this._onwait) | |
| 36 | } | |
| 37 | ||
| 38 | signal() { | |
| 39 | if (this.destroyed === true) return | |
| 40 | ||
| 41 | this.active-- | |
| 42 | while (this.active < this.limit && this.waiting.length > 0 && this.destroyed === false) { | |
| 43 | this.active++ | |
| 44 | this.waiting.shift()(true) | |
| 45 | } | |
| 46 | ||
| 47 | if (this.active === 0 && this.flushedResolve) { | |
| 48 | const resolve = this.flushedResolve | |
| 49 | this.flushedResolve = null | |
| 50 | this.flushedPromise = null | |
| 51 | resolve(true) | |
| 52 | } | |
| 53 | } | |
| 54 | ||
| 55 | async flush() { | |
| 56 | if (this.destroyed === true) return | |
| 57 | if (this.active === 0) return | |
| 58 | if (this.flushedPromise) return this.flushedPromise | |
| 59 | this.flushedPromise = new Promise(this._onflush) | |
| 60 | return this.flushedPromise | |
| 61 | } | |
| 62 | ||
| 63 | destroy() { | |
| 64 | this.destroyed = true | |
| 65 | this.active = 0 | |
| 66 | while (this.waiting.length) this.waiting.pop()(false) | |
| 67 | if (this.flushedResolve) this.flushedResolve(false) | |
| 68 | } | |
| 69 | } |
| … | … | @@ -0,0 +1,736 @@ |
| 1 | const { EventEmitter } = require('events') | |
| 2 | const safetyCatch = require('safety-catch') | |
| 3 | const NoiseSecretStream = require('@hyperswarm/secret-stream') | |
| 4 | const b4a = require('b4a') | |
| 5 | const relay = require('blind-relay') | |
| 6 | const NoiseWrap = require('./noise-wrap') | |
| 7 | const Announcer = require('./announcer') | |
| 8 | const { FIREWALL, ERROR } = require('./constants') | |
| 9 | const { unslabbedHash } = require('./crypto') | |
| 10 | const SecurePayload = require('./secure-payload') | |
| 11 | const Holepuncher = require('./holepuncher') | |
| 12 | const { isPrivate } = require('bogon') | |
| 13 | const { ALREADY_LISTENING, NODE_DESTROYED, KEYPAIR_ALREADY_USED } = require('./errors') | |
| 14 | ||
| 15 | const HANDSHAKE_CLEAR_WAIT = 10000 | |
| 16 | const HANDSHAKE_INITIAL_TIMEOUT = 10000 | |
| 17 | ||
| 18 | module.exports = class Server extends EventEmitter { | |
| 19 | constructor(dht, opts = {}) { | |
| 20 | super() | |
| 21 | ||
| 22 | this.dht = dht | |
| 23 | this.target = null | |
| 24 | ||
| 25 | this.closed = false | |
| 26 | this.firewall = opts.firewall || (() => false) | |
| 27 | this.holepunch = opts.holepunch || (() => true) | |
| 28 | this.relayThrough = opts.relayThrough || null | |
| 29 | this.relayKeepAlive = opts.relayKeepAlive || 5000 | |
| 30 | this.pool = opts.pool || null | |
| 31 | this.createHandshake = opts.createHandshake || defaultCreateHandshake | |
| 32 | this.createSecretStream = opts.createSecretStream || defaultCreateSecretStream | |
| 33 | this.suspended = false | |
| 34 | this.handshakeClearWait = opts.handshakeClearWait || HANDSHAKE_CLEAR_WAIT | |
| 35 | ||
| 36 | this._shareLocalAddress = opts.shareLocalAddress !== false | |
| 37 | this._reusableSocket = !!opts.reusableSocket | |
| 38 | this._neverPunch = opts.holepunch === false // useful for fully disabling punching | |
| 39 | this._keyPair = null | |
| 40 | this._announcer = null | |
| 41 | this._connects = new Map() | |
| 42 | this._holepunches = [] | |
| 43 | this._listening = null | |
| 44 | this._closing = null | |
| 45 | } | |
| 46 | ||
| 47 | get listening() { | |
| 48 | return this._listening !== null | |
| 49 | } | |
| 50 | ||
| 51 | get publicKey() { | |
| 52 | return this._keyPair && this._keyPair.publicKey | |
| 53 | } | |
| 54 | ||
| 55 | get relayAddresses() { | |
| 56 | return this._announcer ? this._announcer.relayAddresses : [] | |
| 57 | } | |
| 58 | ||
| 59 | onconnection(encryptedSocket) { | |
| 60 | this.emit('connection', encryptedSocket) | |
| 61 | } | |
| 62 | ||
| 63 | async suspend({ log = noop } = {}) { | |
| 64 | log('Suspending hyperdht server') | |
| 65 | if (this._listening !== null) await this._listening | |
| 66 | log('Suspending hyperdht server (post listening)') | |
| 67 | this.suspended = true | |
| 68 | this._clearAll() | |
| 69 | return this._announcer ? this._announcer.suspend({ log }) : Promise.resolve() | |
| 70 | } | |
| 71 | ||
| 72 | async resume() { | |
| 73 | if (this._listening !== null) await this._listening | |
| 74 | this.suspended = false | |
| 75 | return this._announcer ? this._announcer.resume() : Promise.resolve() | |
| 76 | } | |
| 77 | ||
| 78 | address() { | |
| 79 | if (!this._keyPair) return null | |
| 80 | ||
| 81 | return { | |
| 82 | publicKey: this._keyPair.publicKey, | |
| 83 | host: this.dht.host, | |
| 84 | port: this.dht.port | |
| 85 | } | |
| 86 | } | |
| 87 | ||
| 88 | close() { | |
| 89 | if (this._closing) return this._closing | |
| 90 | this._closing = this._close() | |
| 91 | return this._closing | |
| 92 | } | |
| 93 | ||
| 94 | _gc() { | |
| 95 | this.dht.listening.delete(this) | |
| 96 | if (this.target) this.dht._router.delete(this.target) | |
| 97 | } | |
| 98 | ||
| 99 | async _stopListening() { | |
| 100 | try { | |
| 101 | if (this._announcer) await this._announcer.stop() | |
| 102 | } catch { | |
| 103 | // ignore | |
| 104 | } | |
| 105 | ||
| 106 | this._announcer = null | |
| 107 | this._listening = null | |
| 108 | this._keyPair = null | |
| 109 | } | |
| 110 | ||
| 111 | async _close() { | |
| 112 | if (this._listening === null) { | |
| 113 | this.closed = true | |
| 114 | this.emit('close') | |
| 115 | return | |
| 116 | } | |
| 117 | ||
| 118 | try { | |
| 119 | await this._listening | |
| 120 | } catch {} | |
| 121 | ||
| 122 | this._gc() | |
| 123 | this._clearAll() | |
| 124 | ||
| 125 | await this._stopListening() | |
| 126 | ||
| 127 | this.closed = true | |
| 128 | this.emit('close') | |
| 129 | } | |
| 130 | ||
| 131 | _clearAll() { | |
| 132 | while (this._holepunches.length > 0) { | |
| 133 | const h = this._holepunches.pop() | |
| 134 | if (h && h.puncher) h.puncher.destroy() | |
| 135 | if (h && h.clearing) clearTimeout(h.clearing) | |
| 136 | if (h && h.prepunching) clearTimeout(h.prepunching) | |
| 137 | if (h && h.rawStream) h.rawStream.destroy() | |
| 138 | } | |
| 139 | ||
| 140 | this._connects.clear() | |
| 141 | } | |
| 142 | ||
| 143 | async listen(keyPair = this.dht.defaultKeyPair, opts = {}) { | |
| 144 | if (this._listening !== null) throw ALREADY_LISTENING() | |
| 145 | if (this.dht.destroyed) throw NODE_DESTROYED() | |
| 146 | ||
| 147 | this._listening = this._listen(keyPair, opts) | |
| 148 | await this._listening | |
| 149 | return this | |
| 150 | } | |
| 151 | ||
| 152 | async _listen(keyPair, opts) { | |
| 153 | // From now on, the DHT object which created me is responsible for closing me | |
| 154 | this.dht.listening.add(this) | |
| 155 | ||
| 156 | try { | |
| 157 | await this.dht.bind() | |
| 158 | if (this._closing) return | |
| 159 | ||
| 160 | for (const s of this.dht.listening) { | |
| 161 | if (s._keyPair && b4a.equals(s._keyPair.publicKey, keyPair.publicKey)) { | |
| 162 | throw KEYPAIR_ALREADY_USED() | |
| 163 | } | |
| 164 | } | |
| 165 | ||
| 166 | this.target = unslabbedHash(keyPair.publicKey) | |
| 167 | this._keyPair = keyPair | |
| 168 | this._announcer = new Announcer(this.dht, keyPair, this.target, opts) | |
| 169 | ||
| 170 | this.dht._router.set(this.target, { | |
| 171 | relay: null, | |
| 172 | record: this._announcer.record, | |
| 173 | onpeerhandshake: this._onpeerhandshake.bind(this), | |
| 174 | onpeerholepunch: this._onpeerholepunch.bind(this) | |
| 175 | }) | |
| 176 | ||
| 177 | // warm it up for now | |
| 178 | this._localAddresses().catch(safetyCatch) | |
| 179 | ||
| 180 | await this._announcer.start() | |
| 181 | } catch (err) { | |
| 182 | await this._stopListening() | |
| 183 | this._gc() | |
| 184 | throw err | |
| 185 | } | |
| 186 | ||
| 187 | if (this._closing) return | |
| 188 | if (this.suspended) await this._announcer.suspend() | |
| 189 | ||
| 190 | if (this._closing) return | |
| 191 | if (this.dht.destroyed) throw NODE_DESTROYED() | |
| 192 | ||
| 193 | if (this.pool) this.pool._attachServer(this) | |
| 194 | ||
| 195 | this.emit('listening') | |
| 196 | } | |
| 197 | ||
| 198 | refresh() { | |
| 199 | if (this._announcer && !this.suspended) this._announcer.refresh() | |
| 200 | } | |
| 201 | ||
| 202 | notifyOnline() { | |
| 203 | if (this._announcer) this._announcer.online.notify() | |
| 204 | } | |
| 205 | ||
| 206 | _localAddresses() { | |
| 207 | return this.dht.validateLocalAddresses(Holepuncher.localAddresses(this.dht.io.serverSocket)) | |
| 208 | } | |
| 209 | ||
| 210 | async _addHandshake(k, noise, clientAddress, { from, to: serverAddress, socket }, direct) { | |
| 211 | let id = this._holepunches.indexOf(null) | |
| 212 | if (id === -1) id = this._holepunches.push(null) - 1 | |
| 213 | ||
| 214 | const hs = { | |
| 215 | round: 0, | |
| 216 | reply: null, | |
| 217 | puncher: null, | |
| 218 | payload: null, | |
| 219 | rawStream: null, | |
| 220 | encryptedSocket: null, | |
| 221 | prepunching: null, | |
| 222 | firewalled: true, | |
| 223 | clearing: null, | |
| 224 | onsocket: null, | |
| 225 | aborted: false, | |
| 226 | ||
| 227 | // Relay state | |
| 228 | relayTimeout: null, | |
| 229 | relayToken: null, | |
| 230 | relaySocket: null, | |
| 231 | relayClient: null, | |
| 232 | relayPaired: false | |
| 233 | } | |
| 234 | ||
| 235 | this._holepunches[id] = hs | |
| 236 | ||
| 237 | const handshake = this.createHandshake(this._keyPair, null) | |
| 238 | ||
| 239 | let remotePayload | |
| 240 | try { | |
| 241 | remotePayload = await handshake.recv(noise) | |
| 242 | } catch (err) { | |
| 243 | safetyCatch(err) | |
| 244 | this._clearLater(hs, id, k) | |
| 245 | return null | |
| 246 | } | |
| 247 | ||
| 248 | if (this._closing || this.suspended) return null | |
| 249 | ||
| 250 | try { | |
| 251 | hs.firewalled = await this.firewall(handshake.remotePublicKey, remotePayload, clientAddress) | |
| 252 | } catch (err) { | |
| 253 | safetyCatch(err) | |
| 254 | } | |
| 255 | ||
| 256 | if (this._closing || this.suspended) return null | |
| 257 | ||
| 258 | if (hs.firewalled) { | |
| 259 | this._clearLater(hs, id, k) | |
| 260 | return null | |
| 261 | } | |
| 262 | ||
| 263 | const error = | |
| 264 | remotePayload.version === 1 | |
| 265 | ? remotePayload.udx | |
| 266 | ? ERROR.NONE | |
| 267 | : ERROR.ABORTED | |
| 268 | : ERROR.VERSION_MISMATCH | |
| 269 | ||
| 270 | const addresses = [] | |
| 271 | const ourRemoteAddr = this.dht.remoteAddress() | |
| 272 | const ourLocalAddrs = this._shareLocalAddress ? await this._localAddresses() : null | |
| 273 | ||
| 274 | if (this._closing || this.suspended) return null | |
| 275 | ||
| 276 | if (ourRemoteAddr) addresses.push(ourRemoteAddr) | |
| 277 | if (ourLocalAddrs) addresses.push(...ourLocalAddrs) | |
| 278 | ||
| 279 | if (error === ERROR.NONE) { | |
| 280 | hs.rawStream = this.dht.createRawStream({ | |
| 281 | framed: true, | |
| 282 | firewall(socket, port, host) { | |
| 283 | if (!(port > 0 && port < 65536)) return true | |
| 284 | ||
| 285 | // Check if the traffic originated from the socket on which we're expecting relay traffic. If so, | |
| 286 | // we haven't hole punched yet and the other side is just sending us traffic through the relay. | |
| 287 | if (hs.relaySocket && isRelay(hs.relaySocket, socket, port, host)) { | |
| 288 | return false | |
| 289 | } | |
| 290 | ||
| 291 | hs.onsocket(socket, port, host) | |
| 292 | return false | |
| 293 | } | |
| 294 | }) | |
| 295 | ||
| 296 | hs.rawStream.on('error', autoDestroy) | |
| 297 | ||
| 298 | // Handles the case where onsocket is never called, but the stream got setup | |
| 299 | // This can happen on a relayed connection which never connects directly | |
| 300 | // (onsocket is called there only when the direct connection is established) | |
| 301 | const onrawstreamclose = () => { | |
| 302 | if (this._closing) return | |
| 303 | this._clearLater(hs, id, k) | |
| 304 | } | |
| 305 | hs.rawStream.on('close', onrawstreamclose) | |
| 306 | ||
| 307 | hs.onsocket = (socket, port, host) => { | |
| 308 | if (hs.rawStream === null) return // Already hole punched | |
| 309 | ||
| 310 | this._clearLater(hs, id, k) | |
| 311 | ||
| 312 | if (hs.prepunching) { | |
| 313 | clearTimeout(hs.prepunching) | |
| 314 | hs.prepunching = null | |
| 315 | } | |
| 316 | ||
| 317 | if (this._reusableSocket && remotePayload.udx.reusableSocket) { | |
| 318 | this.dht._socketPool.routes.add(handshake.remotePublicKey, hs.rawStream) | |
| 319 | } | |
| 320 | ||
| 321 | hs.rawStream.removeListener('error', autoDestroy) | |
| 322 | hs.rawStream.removeListener('close', onrawstreamclose) | |
| 323 | ||
| 324 | if (hs.rawStream.connected) { | |
| 325 | const remoteChanging = hs.rawStream.changeRemote(socket, remotePayload.udx.id, port, host) | |
| 326 | ||
| 327 | if (remoteChanging) remoteChanging.catch(safetyCatch) | |
| 328 | } else { | |
| 329 | hs.rawStream.connect(socket, remotePayload.udx.id, port, host) | |
| 330 | hs.encryptedSocket = this.createSecretStream(false, hs.rawStream, { | |
| 331 | handshake: h, | |
| 332 | keepAlive: this.dht.connectionKeepAlive | |
| 333 | }) | |
| 334 | ||
| 335 | this.onconnection(hs.encryptedSocket) | |
| 336 | } | |
| 337 | ||
| 338 | if (hs.puncher) { | |
| 339 | hs.puncher.onabort = noop | |
| 340 | hs.puncher.destroy() | |
| 341 | } | |
| 342 | ||
| 343 | hs.rawStream = null | |
| 344 | } | |
| 345 | ||
| 346 | function autoDestroy() { | |
| 347 | if (hs.puncher) hs.puncher.destroy() | |
| 348 | } | |
| 349 | } | |
| 350 | ||
| 351 | const relayAddresses = this.relayAddresses | |
| 352 | const relayThrough = selectRelay(this.relayThrough) | |
| 353 | ||
| 354 | if (relayThrough) hs.relayToken = relay.token() | |
| 355 | ||
| 356 | try { | |
| 357 | hs.reply = await handshake.send({ | |
| 358 | error, | |
| 359 | firewall: ourRemoteAddr ? FIREWALL.OPEN : FIREWALL.UNKNOWN, | |
| 360 | holepunch: ourRemoteAddr ? null : { id, relays: this._announcer.relays }, | |
| 361 | addresses4: addresses, | |
| 362 | addresses6: null, | |
| 363 | udx: { | |
| 364 | reusableSocket: this._reusableSocket, | |
| 365 | id: hs.rawStream ? hs.rawStream.id : 0, | |
| 366 | seq: 0 | |
| 367 | }, | |
| 368 | secretStream: {}, | |
| 369 | relayThrough: relayThrough ? { publicKey: relayThrough, token: hs.relayToken } : null, | |
| 370 | relayAddresses: relayAddresses.length ? relayAddresses : null | |
| 371 | }) | |
| 372 | } catch (err) { | |
| 373 | safetyCatch(err) | |
| 374 | if (hs.rawStream) hs.rawStream.destroy() | |
| 375 | this._clearLater(hs, id, k) | |
| 376 | return null | |
| 377 | } | |
| 378 | ||
| 379 | if (this._closing || this.suspended) { | |
| 380 | if (hs.rawStream) hs.rawStream.destroy() | |
| 381 | return null | |
| 382 | } | |
| 383 | ||
| 384 | const h = handshake.final() | |
| 385 | ||
| 386 | if (error !== ERROR.NONE) { | |
| 387 | if (hs.rawStream) hs.rawStream.destroy() | |
| 388 | this._clearLater(hs, id, k) | |
| 389 | return hs | |
| 390 | } | |
| 391 | ||
| 392 | if (remotePayload.firewall === FIREWALL.OPEN || direct) { | |
| 393 | const sock = direct ? socket : this.dht.socket | |
| 394 | this.dht.stats.punches.open++ | |
| 395 | hs.onsocket(sock, clientAddress.port, clientAddress.host) | |
| 396 | return hs | |
| 397 | } | |
| 398 | ||
| 399 | if (relayThrough || remotePayload.relayThrough) { | |
| 400 | this._relayConnection(hs, relayThrough, remotePayload, h) | |
| 401 | } | |
| 402 | ||
| 403 | const onabort = () => { | |
| 404 | hs.aborted = true | |
| 405 | if (hs.prepunching) clearTimeout(hs.prepunching) | |
| 406 | hs.prepunching = null | |
| 407 | if (hs.rawStream.destroyed) { | |
| 408 | this._clearLater(hs, id, k) | |
| 409 | return | |
| 410 | } | |
| 411 | ||
| 412 | hs.rawStream.on('close', () => this._clearLater(hs, id, k)) | |
| 413 | if (hs.relayToken === null) hs.rawStream.destroy() | |
| 414 | } | |
| 415 | ||
| 416 | if (!direct && clientAddress.host === serverAddress.host) { | |
| 417 | const clientAddresses = remotePayload.addresses4.filter(onlyPrivateHosts) | |
| 418 | ||
| 419 | if (clientAddresses.length > 0 && this._shareLocalAddress) { | |
| 420 | const myAddresses = await this._localAddresses() | |
| 421 | const addr = Holepuncher.matchAddress(myAddresses, clientAddresses) | |
| 422 | ||
| 423 | if (addr) { | |
| 424 | hs.prepunching = setTimeout(onabort, HANDSHAKE_INITIAL_TIMEOUT) | |
| 425 | return hs | |
| 426 | } | |
| 427 | } | |
| 428 | } | |
| 429 | ||
| 430 | if (this._closing || this.suspended) return null | |
| 431 | ||
| 432 | if (ourRemoteAddr || this._neverPunch) { | |
| 433 | hs.prepunching = setTimeout(onabort, HANDSHAKE_INITIAL_TIMEOUT) | |
| 434 | return hs | |
| 435 | } | |
| 436 | ||
| 437 | hs.payload = new SecurePayload(h.holepunchSecret) | |
| 438 | hs.puncher = new Holepuncher(this.dht, this.dht.session(), false, remotePayload.firewall) | |
| 439 | ||
| 440 | hs.puncher.onconnect = hs.onsocket | |
| 441 | hs.puncher.onabort = onabort | |
| 442 | hs.prepunching = setTimeout(hs.puncher.destroy.bind(hs.puncher), HANDSHAKE_INITIAL_TIMEOUT) | |
| 443 | ||
| 444 | return hs | |
| 445 | } | |
| 446 | ||
| 447 | _clearLater(hs, id, k) { | |
| 448 | if (hs.clearing) return | |
| 449 | hs.clearing = setTimeout(() => this._clear(hs, id, k), this.handshakeClearWait) | |
| 450 | } | |
| 451 | ||
| 452 | _clear(hs, id, k) { | |
| 453 | if (id >= this._holepunches.length || this._holepunches[id] !== hs) return | |
| 454 | if (hs.clearing) clearTimeout(hs.clearing) | |
| 455 | ||
| 456 | this._holepunches[id] = null | |
| 457 | while ( | |
| 458 | this._holepunches.length > 0 && | |
| 459 | this._holepunches[this._holepunches.length - 1] === null | |
| 460 | ) { | |
| 461 | this._holepunches.pop() | |
| 462 | } | |
| 463 | this._connects.delete(k) | |
| 464 | } | |
| 465 | ||
| 466 | async _onpeerhandshake({ noise, peerAddress }, req) { | |
| 467 | const k = b4a.toString(noise, 'hex') | |
| 468 | ||
| 469 | // The next couple of statements MUST run within the same tick to prevent | |
| 470 | // a malicious peer from flooding us with handshakes. | |
| 471 | let p = this._connects.get(k) | |
| 472 | if (!p) { | |
| 473 | p = this._addHandshake(k, noise, peerAddress || req.from, req, !peerAddress) | |
| 474 | this._connects.set(k, p) | |
| 475 | } | |
| 476 | ||
| 477 | const h = await p | |
| 478 | if (!h) return null | |
| 479 | ||
| 480 | if (this._closing !== null || this.suspended) return null | |
| 481 | ||
| 482 | return { socket: h.puncher && h.puncher.socket, noise: h.reply } | |
| 483 | } | |
| 484 | ||
| 485 | async _onpeerholepunch({ id, peerAddress, payload }, req) { | |
| 486 | const h = id < this._holepunches.length ? this._holepunches[id] : null | |
| 487 | if (!h) return null | |
| 488 | ||
| 489 | if (!peerAddress || this._closing !== null || this.suspended) return null | |
| 490 | ||
| 491 | const p = h.puncher | |
| 492 | if (!p || !p.socket) return this._abort(h) // not opened | |
| 493 | ||
| 494 | const remotePayload = h.payload.decrypt(payload) | |
| 495 | if (!remotePayload) return null | |
| 496 | ||
| 497 | const isServerRelay = this._announcer.isRelay(req.from) | |
| 498 | const { error, firewall, round, punching, addresses, remoteAddress, remoteToken } = | |
| 499 | remotePayload | |
| 500 | ||
| 501 | if (error !== ERROR.NONE) { | |
| 502 | // We actually do not need to set the round here, but just do it for consistency. | |
| 503 | if (round >= h.round) h.round = round | |
| 504 | return this._abort(h) | |
| 505 | } | |
| 506 | ||
| 507 | const token = h.payload.token(peerAddress) | |
| 508 | const echoed = isServerRelay && !!remoteToken && b4a.equals(token, remoteToken) | |
| 509 | ||
| 510 | // Update our heuristics here | |
| 511 | if (req.socket === p.socket) { | |
| 512 | p.nat.add(req.to, req.from) | |
| 513 | } | |
| 514 | ||
| 515 | if (round >= h.round) { | |
| 516 | h.round = round | |
| 517 | p.updateRemote({ punching, firewall, addresses, verified: echoed ? peerAddress.host : null }) | |
| 518 | } | |
| 519 | ||
| 520 | // Wait for the analyzer to reach a conclusion... | |
| 521 | let stable = await p.analyze(false) | |
| 522 | if (p.destroyed) return null | |
| 523 | ||
| 524 | if (!p.remoteHolepunching && !stable) { | |
| 525 | stable = await p.analyze(true) | |
| 526 | if (p.destroyed) return null | |
| 527 | if (!stable) return this._abort(h) | |
| 528 | } | |
| 529 | ||
| 530 | // Fast mode! If we are consistent and the remote has opened a session to us (remoteAddress) | |
| 531 | // then fire a quick punch back. Note the await here just waits for the udp socket to flush. | |
| 532 | if ( | |
| 533 | isConsistent(p.nat.firewall) && | |
| 534 | remoteAddress && | |
| 535 | hasSameAddr(p.nat.addresses, remoteAddress) | |
| 536 | ) { | |
| 537 | await p.ping(peerAddress) | |
| 538 | if (p.destroyed) return null | |
| 539 | } | |
| 540 | ||
| 541 | // Remote said they are punching (or willing to), so we will punch as well. | |
| 542 | // Note that this returns when the punching has STARTED, so no guarantee | |
| 543 | // we will have a connection after this promise etc. | |
| 544 | if (p.remoteHolepunching) { | |
| 545 | // TODO: still continue here if a local connection might work, but then do not holepunch... | |
| 546 | if (!this.holepunch(p.remoteFirewall, p.nat.firewall, p.remoteAddresses, p.nat.addresses)) { | |
| 547 | return p.destroyed ? null : this._abort(h) | |
| 548 | } | |
| 549 | ||
| 550 | if (h.prepunching) { | |
| 551 | clearTimeout(h.prepunching) | |
| 552 | h.prepunching = null | |
| 553 | } | |
| 554 | ||
| 555 | if (p.remoteFirewall >= FIREWALL.RANDOM || p.nat.firewall >= FIREWALL.RANDOM) { | |
| 556 | if ( | |
| 557 | this.dht._randomPunches >= this.dht._randomPunchLimit || | |
| 558 | Date.now() - this.dht._lastRandomPunch < this.dht._randomPunchInterval | |
| 559 | ) { | |
| 560 | if (!h.relayToken) return this._abort(h, ERROR.TRY_LATER) | |
| 561 | return { | |
| 562 | socket: p.socket, | |
| 563 | payload: h.payload.encrypt({ | |
| 564 | error: ERROR.TRY_LATER, | |
| 565 | firewall: p.nat.firewall, | |
| 566 | round: h.round, | |
| 567 | connected: p.connected, | |
| 568 | punching: p.punching, | |
| 569 | addresses: p.nat.addresses, | |
| 570 | remoteAddress: null, | |
| 571 | token: isServerRelay ? token : null, | |
| 572 | remoteToken: remotePayload.token | |
| 573 | }) | |
| 574 | } | |
| 575 | } | |
| 576 | } | |
| 577 | ||
| 578 | const punching = await p.punch() | |
| 579 | if (p.destroyed) return null | |
| 580 | if (!punching) return this._abort(h) | |
| 581 | } | |
| 582 | ||
| 583 | // Freeze that analysis as soon as we have a result we are giving to the other peer | |
| 584 | if (p.nat.firewall !== FIREWALL.UNKNOWN) { | |
| 585 | p.nat.freeze() | |
| 586 | } | |
| 587 | ||
| 588 | return { | |
| 589 | socket: p.socket, | |
| 590 | payload: h.payload.encrypt({ | |
| 591 | error: ERROR.NONE, | |
| 592 | firewall: p.nat.firewall, | |
| 593 | round: h.round, | |
| 594 | connected: p.connected, | |
| 595 | punching: p.punching, | |
| 596 | addresses: p.nat.addresses, | |
| 597 | remoteAddress: null, | |
| 598 | token: isServerRelay ? token : null, | |
| 599 | remoteToken: remotePayload.token | |
| 600 | }) | |
| 601 | } | |
| 602 | } | |
| 603 | ||
| 604 | _abort(h, error = ERROR.ABORTED) { | |
| 605 | if (!h.payload) { | |
| 606 | if (h.puncher) h.puncher.destroy() | |
| 607 | return null | |
| 608 | } | |
| 609 | ||
| 610 | const payload = h.payload.encrypt({ | |
| 611 | error, | |
| 612 | firewall: FIREWALL.UNKNOWN, | |
| 613 | round: h.round, | |
| 614 | connected: false, | |
| 615 | punching: false, | |
| 616 | addresses: null, | |
| 617 | remoteAddress: null, | |
| 618 | token: null, | |
| 619 | remoteToken: null | |
| 620 | }) | |
| 621 | ||
| 622 | h.puncher.destroy() | |
| 623 | ||
| 624 | return { socket: this.dht.socket, payload } | |
| 625 | } | |
| 626 | ||
| 627 | _relayConnection(hs, relayThrough, remotePayload, h) { | |
| 628 | this.dht.stats.relaying.attempts++ | |
| 629 | ||
| 630 | let isInitiator | |
| 631 | let publicKey | |
| 632 | let token | |
| 633 | ||
| 634 | if (relayThrough) { | |
| 635 | isInitiator = true | |
| 636 | publicKey = relayThrough | |
| 637 | token = hs.relayToken | |
| 638 | } else { | |
| 639 | isInitiator = false | |
| 640 | publicKey = remotePayload.relayThrough.publicKey | |
| 641 | token = remotePayload.relayThrough.token | |
| 642 | } | |
| 643 | ||
| 644 | hs.relayToken = token | |
| 645 | hs.relaySocket = this.dht.connect(publicKey) | |
| 646 | hs.relaySocket.setKeepAlive(this.relayKeepAlive) | |
| 647 | hs.relayClient = relay.Client.from(hs.relaySocket, { id: hs.relaySocket.publicKey }) | |
| 648 | hs.relayTimeout = setTimeout(onabort, 15000) | |
| 649 | ||
| 650 | hs.relayClient | |
| 651 | .pair(isInitiator, token, hs.rawStream) | |
| 652 | .on('error', onabort) | |
| 653 | .on('data', (remoteId) => { | |
| 654 | if (hs.relayTimeout) clearRelayTimeout(hs) | |
| 655 | if (hs.rawStream === null) { | |
| 656 | onabort(null) | |
| 657 | return | |
| 658 | } | |
| 659 | ||
| 660 | hs.relayPaired = true | |
| 661 | this.dht.stats.relaying.successes++ | |
| 662 | ||
| 663 | if (hs.prepunching) clearTimeout(hs.prepunching) | |
| 664 | hs.prepunching = null | |
| 665 | ||
| 666 | const { remotePort, remoteHost, socket } = hs.relaySocket.rawStream | |
| 667 | ||
| 668 | hs.rawStream | |
| 669 | .on('close', () => hs.relaySocket.destroy()) | |
| 670 | .connect(socket, remoteId, remotePort, remoteHost) | |
| 671 | ||
| 672 | hs.encryptedSocket = this.createSecretStream(false, hs.rawStream, { handshake: h }) | |
| 673 | ||
| 674 | this.onconnection(hs.encryptedSocket) | |
| 675 | }) | |
| 676 | ||
| 677 | const dht = this.dht | |
| 678 | function onabort() { | |
| 679 | if (!hs.relayPaired) dht.stats.relaying.aborts++ | |
| 680 | if (hs.relayTimeout) clearRelayTimeout(hs) | |
| 681 | const socket = hs.relaySocket | |
| 682 | hs.relayToken = null | |
| 683 | hs.relaySocket = null | |
| 684 | if (socket) socket.destroy() | |
| 685 | if (hs.aborted && hs.rawStream) hs.rawStream.destroy() | |
| 686 | } | |
| 687 | } | |
| 688 | } | |
| 689 | ||
| 690 | function clearRelayTimeout(hs) { | |
| 691 | clearTimeout(hs.relayTimeout) | |
| 692 | hs.relayTimeout = null | |
| 693 | } | |
| 694 | ||
| 695 | function isConsistent(fw) { | |
| 696 | return fw === FIREWALL.OPEN || fw === FIREWALL.CONSISTENT | |
| 697 | } | |
| 698 | ||
| 699 | function hasSameAddr(addrs, other) { | |
| 700 | if (addrs === null) return false | |
| 701 | ||
| 702 | for (const addr of addrs) { | |
| 703 | if (addr.port === other.port && addr.host === other.host) return true | |
| 704 | } | |
| 705 | return false | |
| 706 | } | |
| 707 | ||
| 708 | function defaultCreateHandshake(keyPair, remotePublicKey) { | |
| 709 | return new NoiseWrap(keyPair, remotePublicKey) | |
| 710 | } | |
| 711 | ||
| 712 | function defaultCreateSecretStream(isInitiator, rawStream, opts) { | |
| 713 | return new NoiseSecretStream(isInitiator, rawStream, opts) | |
| 714 | } | |
| 715 | ||
| 716 | function onlyPrivateHosts(addr) { | |
| 717 | return isPrivate(addr.host) | |
| 718 | } | |
| 719 | ||
| 720 | function isRelay(relaySocket, socket, port, host) { | |
| 721 | const stream = relaySocket.rawStream | |
| 722 | if (!stream) return false | |
| 723 | if (stream.socket !== socket) return false | |
| 724 | return port === stream.remotePort && host === stream.remoteHost | |
| 725 | } | |
| 726 | ||
| 727 | function selectRelay(relayThrough) { | |
| 728 | if (typeof relayThrough === 'function') relayThrough = relayThrough() | |
| 729 | if (relayThrough === null) return null | |
| 730 | if (Array.isArray(relayThrough)) { | |
| 731 | return relayThrough[Math.floor(Math.random() * relayThrough.length)] | |
| 732 | } | |
| 733 | return relayThrough | |
| 734 | } | |
| 735 | ||
| 736 | function noop() {} |
| … | … | @@ -0,0 +1,35 @@ |
| 1 | module.exports = class Sleeper { | |
| 2 | constructor() { | |
| 3 | this._timeout = null | |
| 4 | this._resolve = null | |
| 5 | ||
| 6 | this._start = (resolve) => { | |
| 7 | this._resolve = resolve | |
| 8 | } | |
| 9 | ||
| 10 | this._trigger = () => { | |
| 11 | if (this._resolve === null) return | |
| 12 | const resolve = this._resolve | |
| 13 | this._timeout = null | |
| 14 | this._resolve = null | |
| 15 | resolve() | |
| 16 | } | |
| 17 | } | |
| 18 | ||
| 19 | pause(ms) { | |
| 20 | const p = new Promise(this._start) | |
| 21 | if (this._timeout !== null) { | |
| 22 | clearTimeout(this._timeout) | |
| 23 | this._trigger() | |
| 24 | } | |
| 25 | this._timeout = setTimeout(this._trigger, ms) | |
| 26 | return p | |
| 27 | } | |
| 28 | ||
| 29 | resume() { | |
| 30 | if (this._timeout !== null) { | |
| 31 | clearTimeout(this._timeout) | |
| 32 | this._trigger() | |
| 33 | } | |
| 34 | } | |
| 35 | } |
| … | … | @@ -0,0 +1,219 @@ |
| 1 | const b4a = require('b4a') | |
| 2 | ||
| 3 | const LINGER_TIME = 3000 | |
| 4 | ||
| 5 | module.exports = class SocketPool { | |
| 6 | constructor(dht, host) { | |
| 7 | this._dht = dht | |
| 8 | this._sockets = new Map() | |
| 9 | this._lingering = new Set() // updated by the ref | |
| 10 | this._host = host | |
| 11 | ||
| 12 | this.routes = new SocketRoutes(this) | |
| 13 | } | |
| 14 | ||
| 15 | _onmessage(ref, data, address) { | |
| 16 | this._dht.onmessage(ref.socket, data, address) | |
| 17 | } | |
| 18 | ||
| 19 | _add(ref) { | |
| 20 | this._sockets.set(ref.socket, ref) | |
| 21 | } | |
| 22 | ||
| 23 | _remove(ref) { | |
| 24 | this._sockets.delete(ref.socket) | |
| 25 | this._lingering.delete(ref) | |
| 26 | } | |
| 27 | ||
| 28 | lookup(socket) { | |
| 29 | return this._sockets.get(socket) || null | |
| 30 | } | |
| 31 | ||
| 32 | setReusable(socket, bool) { | |
| 33 | const ref = this.lookup(socket) | |
| 34 | if (ref) ref.reusable = bool | |
| 35 | } | |
| 36 | ||
| 37 | acquire() { | |
| 38 | // TODO: Enable socket reuse | |
| 39 | return new SocketRef(this) | |
| 40 | } | |
| 41 | ||
| 42 | async destroy() { | |
| 43 | const closing = [] | |
| 44 | ||
| 45 | for (const ref of this._sockets.values()) { | |
| 46 | ref._unlinger() | |
| 47 | closing.push(ref.socket.close()) | |
| 48 | } | |
| 49 | ||
| 50 | await Promise.allSettled(closing) | |
| 51 | } | |
| 52 | } | |
| 53 | ||
| 54 | class SocketRoutes { | |
| 55 | constructor(pool) { | |
| 56 | this._pool = pool | |
| 57 | this._routes = new Map() | |
| 58 | } | |
| 59 | ||
| 60 | add(publicKey, rawStream) { | |
| 61 | if (rawStream.socket) this._onconnect(publicKey, rawStream) | |
| 62 | else rawStream.on('connect', this._onconnect.bind(this, publicKey, rawStream)) | |
| 63 | } | |
| 64 | ||
| 65 | get(publicKey) { | |
| 66 | const id = b4a.toString(publicKey, 'hex') | |
| 67 | const route = this._routes.get(id) | |
| 68 | if (!route) return null | |
| 69 | return route | |
| 70 | } | |
| 71 | ||
| 72 | _onconnect(publicKey, rawStream) { | |
| 73 | const id = b4a.toString(publicKey, 'hex') | |
| 74 | const socket = rawStream.socket | |
| 75 | ||
| 76 | let route = this._routes.get(id) | |
| 77 | ||
| 78 | if (!route) { | |
| 79 | const gc = () => { | |
| 80 | if (this._routes.get(id) === route) this._routes.delete(id) | |
| 81 | socket.removeListener('close', gc) | |
| 82 | } | |
| 83 | ||
| 84 | route = { | |
| 85 | socket, | |
| 86 | address: { host: rawStream.remoteHost, port: rawStream.remotePort }, | |
| 87 | gc | |
| 88 | } | |
| 89 | ||
| 90 | this._routes.set(id, route) | |
| 91 | socket.on('close', gc) | |
| 92 | } | |
| 93 | ||
| 94 | this._pool.setReusable(socket, true) | |
| 95 | ||
| 96 | rawStream.on('error', () => { | |
| 97 | this._pool.setReusable(socket, false) | |
| 98 | if (!route) route = this._routes.get(id) | |
| 99 | if (route && route.socket === socket) route.gc() | |
| 100 | }) | |
| 101 | } | |
| 102 | } | |
| 103 | ||
| 104 | // TODO: we should just make some "user data" object on udx to allow to attach this info | |
| 105 | class SocketRef { | |
| 106 | constructor(pool) { | |
| 107 | this._pool = pool | |
| 108 | ||
| 109 | // Events | |
| 110 | this.onholepunchmessage = noop | |
| 111 | ||
| 112 | // Whether it should teardown immediately or wait a bit | |
| 113 | this.reusable = false | |
| 114 | ||
| 115 | this.socket = pool._dht.udx.createSocket() | |
| 116 | this.socket | |
| 117 | .on('close', this._onclose.bind(this)) | |
| 118 | .on('message', this._onmessage.bind(this)) | |
| 119 | .on('idle', this._onidle.bind(this)) | |
| 120 | .on('busy', this._onbusy.bind(this)) | |
| 121 | .bind(0, this._pool._host) | |
| 122 | ||
| 123 | this._refs = 1 | |
| 124 | this._released = false | |
| 125 | this._closed = false | |
| 126 | ||
| 127 | this._timeout = null | |
| 128 | this._wasBusy = false | |
| 129 | ||
| 130 | this._pool._add(this) | |
| 131 | } | |
| 132 | ||
| 133 | _onclose() { | |
| 134 | this._pool._remove(this) | |
| 135 | } | |
| 136 | ||
| 137 | _onmessage(data, address) { | |
| 138 | if (data.byteLength > 1) { | |
| 139 | this._pool._onmessage(this, data, address) | |
| 140 | } else { | |
| 141 | this.onholepunchmessage(data, address, this) | |
| 142 | } | |
| 143 | } | |
| 144 | ||
| 145 | _onidle() { | |
| 146 | this._closeMaybe() | |
| 147 | } | |
| 148 | ||
| 149 | _onbusy() { | |
| 150 | this._wasBusy = true | |
| 151 | this._unlinger() | |
| 152 | } | |
| 153 | ||
| 154 | _reset() { | |
| 155 | this.onholepunchmessage = noop | |
| 156 | } | |
| 157 | ||
| 158 | _closeMaybe() { | |
| 159 | if (this._refs === 0 && this.socket.idle && !this._timeout) this._close() | |
| 160 | } | |
| 161 | ||
| 162 | _lingeringClose() { | |
| 163 | this._pool._lingering.delete(this) | |
| 164 | this._timeout = null | |
| 165 | this._closeMaybe() | |
| 166 | } | |
| 167 | ||
| 168 | _close() { | |
| 169 | this._unlinger() | |
| 170 | ||
| 171 | if (this.reusable && this._wasBusy) { | |
| 172 | this._wasBusy = false | |
| 173 | this._pool._lingering.add(this) | |
| 174 | this._timeout = setTimeout(this._lingeringClose.bind(this), LINGER_TIME) | |
| 175 | return | |
| 176 | } | |
| 177 | ||
| 178 | this._closed = true | |
| 179 | this.socket.close() | |
| 180 | } | |
| 181 | ||
| 182 | _unlinger() { | |
| 183 | if (this._timeout !== null) { | |
| 184 | clearTimeout(this._timeout) | |
| 185 | this._pool._lingering.delete(this) | |
| 186 | this._timeout = null | |
| 187 | } | |
| 188 | } | |
| 189 | ||
| 190 | get free() { | |
| 191 | return this._refs === 0 | |
| 192 | } | |
| 193 | ||
| 194 | active() { | |
| 195 | this._refs++ | |
| 196 | this._unlinger() | |
| 197 | } | |
| 198 | ||
| 199 | inactive() { | |
| 200 | this._refs-- | |
| 201 | this._closeMaybe() | |
| 202 | } | |
| 203 | ||
| 204 | address() { | |
| 205 | return this.socket.address() | |
| 206 | } | |
| 207 | ||
| 208 | release() { | |
| 209 | if (this._released) return | |
| 210 | ||
| 211 | this._released = true | |
| 212 | this._reset() | |
| 213 | ||
| 214 | this._refs-- | |
| 215 | this._closeMaybe() | |
| 216 | } | |
| 217 | } | |
| 218 | ||
| 219 | function noop() {} |
| … | … | @@ -0,0 +1,78 @@ |
| 1 | { | |
| 2 | "name": "hyperdht", | |
| 3 | "version": "6.29.6", | |
| 4 | "description": "The DHT powering Hyperswarm", | |
| 5 | "main": "index.js", | |
| 6 | "browser": "browser.js", | |
| 7 | "bin": { | |
| 8 | "hyperdht": "./bin.js" | |
| 9 | }, | |
| 10 | "files": [ | |
| 11 | "index.js", | |
| 12 | "browser.js", | |
| 13 | "testnet.js", | |
| 14 | "bin.js", | |
| 15 | "lib/**.js" | |
| 16 | ], | |
| 17 | "imports": { | |
| 18 | "events": { | |
| 19 | "bare": "bare-events", | |
| 20 | "default": "events" | |
| 21 | }, | |
| 22 | "child_process": { | |
| 23 | "bare": "bare-node-child-process", | |
| 24 | "default": "child_process" | |
| 25 | } | |
| 26 | }, | |
| 27 | "dependencies": { | |
| 28 | "@hyperswarm/secret-stream": "^6.6.2", | |
| 29 | "b4a": "^1.3.1", | |
| 30 | "bare-events": "^2.2.0", | |
| 31 | "blind-relay": "^1.3.0", | |
| 32 | "bogon": "^1.0.0", | |
| 33 | "compact-encoding": "^2.4.1", | |
| 34 | "dht-rpc": "^6.15.1", | |
| 35 | "hypercore-crypto": "^3.3.0", | |
| 36 | "hypercore-id-encoding": "^1.2.0", | |
| 37 | "noise-curve-ed": "^2.0.0", | |
| 38 | "noise-handshake": "^4.0.0", | |
| 39 | "record-cache": "^1.1.1", | |
| 40 | "safety-catch": "^1.0.1", | |
| 41 | "signal-promise": "^1.0.3", | |
| 42 | "sodium-universal": "^5.0.1", | |
| 43 | "streamx": "^2.16.1", | |
| 44 | "unslab": "^1.3.0", | |
| 45 | "xache": "^1.1.0" | |
| 46 | }, | |
| 47 | "devDependencies": { | |
| 48 | "bare-node-child-process": "^1.0.1", | |
| 49 | "brittle": "^3.0.0", | |
| 50 | "graceful-goodbye": "^1.3.0", | |
| 51 | "newline-decoder": "^1.0.2", | |
| 52 | "prettier": "^3.6.2", | |
| 53 | "prettier-config-holepunch": "^2.0.0" | |
| 54 | }, | |
| 55 | "scripts": { | |
| 56 | "format": "prettier --write .", | |
| 57 | "test": "prettier --check . && node test/all.js", | |
| 58 | "test:bare": "bare test/all.js", | |
| 59 | "test:generate": "brittle -r test/all.js test/*.js", | |
| 60 | "integration": "brittle test/integration/*.js", | |
| 61 | "end-to-end": "brittle test/end-to-end/*.js" | |
| 62 | }, | |
| 63 | "author": "Mathias Buus (@mafintosh)", | |
| 64 | "license": "MIT", | |
| 65 | "directories": { | |
| 66 | "lib": "lib", | |
| 67 | "test": "test" | |
| 68 | }, | |
| 69 | "repository": { | |
| 70 | "type": "git", | |
| 71 | "url": "git+https://github.com/holepunchto/hyperdht.git" | |
| 72 | }, | |
| 73 | "keywords": [], | |
| 74 | "bugs": { | |
| 75 | "url": "https://github.com/holepunchto/hyperdht/issues" | |
| 76 | }, | |
| 77 | "homepage": "https://github.com/holepunchto/hyperdht#readme" | |
| 78 | } |
| … | … | @@ -0,0 +1,81 @@ |
| 1 | const DHT = require('.') | |
| 2 | ||
| 3 | module.exports = async function createTestnet(size = 10, opts = {}) { | |
| 4 | const swarm = [] | |
| 5 | const teardown = | |
| 6 | typeof opts === 'function' ? opts : opts.teardown ? opts.teardown.bind(opts) : noop | |
| 7 | const host = opts.host || '127.0.0.1' | |
| 8 | const port = opts.port || 0 | |
| 9 | const bootstrap = opts.bootstrap ? [...opts.bootstrap] : [] | |
| 10 | const bindHost = host === '127.0.0.1' ? '127.0.0.1' : '0.0.0.0' | |
| 11 | ||
| 12 | if (size === 0) return new Testnet(swarm) | |
| 13 | ||
| 14 | const first = new DHT({ | |
| 15 | ephemeral: false, | |
| 16 | firewalled: false, | |
| 17 | bootstrap, | |
| 18 | port, | |
| 19 | host: bindHost | |
| 20 | }) | |
| 21 | ||
| 22 | await first.fullyBootstrapped() | |
| 23 | ||
| 24 | if (bootstrap.length === 0) bootstrap.push({ host, port: first.address().port }) | |
| 25 | ||
| 26 | swarm.push(first) | |
| 27 | ||
| 28 | while (swarm.length < size) { | |
| 29 | const node = new DHT({ | |
| 30 | ephemeral: false, | |
| 31 | firewalled: false, | |
| 32 | bootstrap, | |
| 33 | host: bindHost | |
| 34 | }) | |
| 35 | ||
| 36 | await node.fullyBootstrapped() | |
| 37 | swarm.push(node) | |
| 38 | } | |
| 39 | ||
| 40 | const testnet = new Testnet(swarm, bootstrap) | |
| 41 | ||
| 42 | teardown(() => testnet.destroy(), { order: Infinity }) | |
| 43 | ||
| 44 | return testnet | |
| 45 | } | |
| 46 | ||
| 47 | class Testnet { | |
| 48 | constructor(nodes, bootstrap = []) { | |
| 49 | this.nodes = nodes | |
| 50 | this.bootstrap = bootstrap | |
| 51 | } | |
| 52 | ||
| 53 | createNode(opts = {}) { | |
| 54 | const node = new DHT({ | |
| 55 | ephemeral: true, | |
| 56 | bootstrap: this.bootstrap, | |
| 57 | host: '127.0.0.1', | |
| 58 | ...opts | |
| 59 | }) | |
| 60 | ||
| 61 | this.nodes.push(node) | |
| 62 | ||
| 63 | return node | |
| 64 | } | |
| 65 | ||
| 66 | async destroy() { | |
| 67 | for (const node of this.nodes) { | |
| 68 | for (const server of node.listening) await server.close() | |
| 69 | } | |
| 70 | ||
| 71 | for (let i = this.nodes.length - 1; i >= 0; i--) { | |
| 72 | await this.nodes[i].destroy() | |
| 73 | } | |
| 74 | } | |
| 75 | ||
| 76 | [Symbol.iterator]() { | |
| 77 | return this.nodes[Symbol.iterator]() | |
| 78 | } | |
| 79 | } | |
| 80 | ||
| 81 | function noop() {} |
| … | … | @@ -0,0 +1,21 @@ |
| 1 | The MIT License (MIT) | |
| 2 | ||
| 3 | Copyright (c) 2019 Mathias Buus, Paul Frazee, David Mark Clements and contributors | |
| 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. |
| … | … | @@ -0,0 +1,208 @@ |
| 1 | # hyperswarm | |
| 2 | ||
| 3 | ### [See the full API docs at docs.pears.com](https://docs.pears.com/building-blocks/hyperswarm) | |
| 4 | ||
| 5 | A high-level API for finding and connecting to peers who are interested in a "topic." | |
| 6 | ||
| 7 | ## Installation | |
| 8 | ||
| 9 | ``` | |
| 10 | npm install hyperswarm | |
| 11 | ``` | |
| 12 | ||
| 13 | ## Usage | |
| 14 | ||
| 15 | ```js | |
| 16 | const Hyperswarm = require('hyperswarm') | |
| 17 | ||
| 18 | const swarm1 = new Hyperswarm() | |
| 19 | const swarm2 = new Hyperswarm() | |
| 20 | ||
| 21 | swarm1.on('connection', (conn, info) => { | |
| 22 | // swarm1 will receive server connections | |
| 23 | conn.write('this is a server connection') | |
| 24 | conn.end() | |
| 25 | }) | |
| 26 | ||
| 27 | swarm2.on('connection', (conn, info) => { | |
| 28 | conn.on('data', (data) => console.log('client got message:', data.toString())) | |
| 29 | }) | |
| 30 | ||
| 31 | const topic = Buffer.alloc(32).fill('hello world') // A topic must be 32 bytes | |
| 32 | const discovery = swarm1.join(topic, { server: true, client: false }) | |
| 33 | await discovery.flushed() // Waits for the topic to be fully announced on the DHT | |
| 34 | ||
| 35 | swarm2.join(topic, { server: false, client: true }) | |
| 36 | await swarm2.flush() // Waits for the swarm to connect to pending peers. | |
| 37 | ||
| 38 | // After this point, both client and server should have connections | |
| 39 | ``` | |
| 40 | ||
| 41 | ## Hyperswarm API | |
| 42 | ||
| 43 | #### `const swarm = new Hyperswarm(opts = {})` | |
| 44 | ||
| 45 | Construct a new Hyperswarm instance. | |
| 46 | ||
| 47 | `opts` can include: | |
| 48 | ||
| 49 | - `keyPair`: A Noise keypair that will be used to listen/connect on the DHT. Defaults to a new key pair. | |
| 50 | - `seed`: A unique, 32-byte, random seed that can be used to deterministically generate the key pair. | |
| 51 | - `maxPeers`: The maximum number of peer connections to allow. | |
| 52 | - `firewall`: A sync function of the form `remotePublicKey => (true|false)`. If true, the connection will be rejected. Defaults to allowing all connections. | |
| 53 | - `dht`: A DHT instance. Defaults to a new instance. | |
| 54 | ||
| 55 | #### `swarm.connecting` | |
| 56 | ||
| 57 | Number that indicates connections in progress. | |
| 58 | ||
| 59 | #### `swarm.connections` | |
| 60 | ||
| 61 | A set of all active client/server connections. | |
| 62 | ||
| 63 | #### `swarm.peers` | |
| 64 | ||
| 65 | A Map containing all connected peers, of the form: `(Noise public key hex string) -> PeerInfo object` | |
| 66 | ||
| 67 | See the [`PeerInfo`](#peerinfo-api) API for more details. | |
| 68 | ||
| 69 | #### `swarm.dht` | |
| 70 | ||
| 71 | A [`hyperdht`](https://github.com/holepunchto/hyperdht) instance. Useful if you want lower-level control over Hyperswarm's networking. | |
| 72 | ||
| 73 | #### `swarm.on('connection', (socket, peerInfo) => {})` | |
| 74 | ||
| 75 | Emitted whenever the swarm connects to a new peer. | |
| 76 | ||
| 77 | `socket` is an end-to-end (Noise) encrypted Duplex stream | |
| 78 | ||
| 79 | `peerInfo` is a [`PeerInfo`](#peerinfo-api) instance | |
| 80 | ||
| 81 | #### `swarm.on('update', () => {})` | |
| 82 | ||
| 83 | Emitted when internal values are changed, useful for user interfaces. | |
| 84 | ||
| 85 | For example: emitted when `swarm.connecting` or `swarm.connections` changes. | |
| 86 | ||
| 87 | #### `const discovery = swarm.join(topic, opts = {})` | |
| 88 | ||
| 89 | Start discovering and connecting to peers sharing a common topic. As new peers are connected to, they will be emitted from the swarm as `connection` events. | |
| 90 | ||
| 91 | `topic` must be a 32-byte Buffer | |
| 92 | `opts` can include: | |
| 93 | ||
| 94 | - `server`: Accept server connections for this topic by announcing yourself to the DHT. Defaults to `true`. | |
| 95 | - `client`: Actively search for and connect to discovered servers. Defaults to `true`. | |
| 96 | - `limit`: Set the max number of peers to connect to when joining the topic. Defaults to `Infinity`. | |
| 97 | ||
| 98 | Returns a [`PeerDiscovery`](#peerdiscovery-api) object. | |
| 99 | ||
| 100 | #### Clients and Servers | |
| 101 | ||
| 102 | In Hyperswarm, there are two ways for peers to join the swarm: client mode and server mode. If you've previously used Hyperswarm v2, these were called "lookup" and "announce", but we now think "client" and "server" are more descriptive. | |
| 103 | ||
| 104 | When you join a topic as a server, the swarm will start accepting incoming connections from clients (peers that have joined the same topic in client mode). Server mode will announce your keypair to the DHT, so that other peers can discover your server. When server connections are emitted, they are not associated with a specific topic -- the server only knows it received an incoming connection. | |
| 105 | ||
| 106 | When you join a topic as a client, the swarm will do a query to discover available servers, and will eagerly connect to them. As with server mode, these connections will be emitted as `connection` events, but in client mode they **will** be associated with the topic (`info.topics` will be set in the `connection` event). | |
| 107 | ||
| 108 | #### `await swarm.leave(topic)` | |
| 109 | ||
| 110 | Stop discovering peers for the given topic. | |
| 111 | ||
| 112 | `topic` must be a 32-byte Buffer | |
| 113 | ||
| 114 | If a topic was previously joined in server mode, `leave` will stop announcing the topic on the DHT. If a topic was previously joined in client mode, `leave` will stop searching for servers announcing the topic. | |
| 115 | ||
| 116 | `leave` will **not** close any existing connections. | |
| 117 | ||
| 118 | #### `swarm.joinPeer(noisePublicKey)` | |
| 119 | ||
| 120 | Establish a direct connection to a known peer. | |
| 121 | ||
| 122 | `noisePublicKey` must be a 32-byte Buffer | |
| 123 | ||
| 124 | As with the standard `join` method, `joinPeer` will ensure that peer connections are reestablished in the event of failures. | |
| 125 | ||
| 126 | #### `swarm.leavePeer(noisePublicKey)` | |
| 127 | ||
| 128 | Stop attempting direct connections to a known peer. | |
| 129 | ||
| 130 | `noisePublicKey` must be a 32-byte Buffer | |
| 131 | ||
| 132 | If a direct connection is already established, that connection will **not** be destroyed by `leavePeer`. | |
| 133 | ||
| 134 | #### `const discovery = swarm.status(topic)` | |
| 135 | ||
| 136 | Get the [`PeerDiscovery`](#peerdiscovery-api) object associated with the topic, if it exists. | |
| 137 | ||
| 138 | #### `await swarm.listen()` | |
| 139 | ||
| 140 | Explicitly start listening for incoming connections. This will be called internally after the first `join`, so it rarely needs to be called manually. | |
| 141 | ||
| 142 | #### `await swarm.flush()` | |
| 143 | ||
| 144 | Wait for any pending DHT announces, and for the swarm to connect to any pending peers (peers that have been discovered, but are still in the queue awaiting processing). | |
| 145 | ||
| 146 | Once a `flush()` has completed, the swarm will have connected to every peer it can discover from the current set of topics it's managing. | |
| 147 | ||
| 148 | `flush()` is not topic-specific, so it will wait for every pending DHT operation and connection to be processed -- it's quite heavyweight, so it could take a while. In most cases, it's not necessary, as connections are emitted by `swarm.on('connection')` immediately after they're opened. | |
| 149 | ||
| 150 | #### `await swarm.suspend({ log: () => {} })` | |
| 151 | ||
| 152 | Suspend the swarm disconnecting all peers, suspends server listening and stops discovery of new peers. Useful for suspending when the runtime suspends to pause networking. | |
| 153 | ||
| 154 | `log` is a logging function, which defaults to a noop function. | |
| 155 | ||
| 156 | #### `await swarm.resume({ log: () => {} })` | |
| 157 | ||
| 158 | Resume a suspended swarm refreshing discovery of new peers and servers. Useful for reannouncing to the DHT and reconnecting to peers when the runtime resumes. | |
| 159 | ||
| 160 | `log` is a logging function, which defaults to a noop function. | |
| 161 | ||
| 162 | #### `swarm.on('ban', peerInfo, err)` | |
| 163 | ||
| 164 | Emitted when a peer gets banned. `err` is an error object describing the reason for the ban (e.g. firewalled). | |
| 165 | ||
| 166 | ## PeerDiscovery API | |
| 167 | ||
| 168 | `swarm.join` returns a `PeerDiscovery` instance which allows you to both control discovery behavior, and respond to lifecycle changes during discovery. | |
| 169 | ||
| 170 | #### `await discovery.flushed()` | |
| 171 | ||
| 172 | Wait until the topic has been fully announced to the DHT. This method is only relevant in server mode. When `flushed()` has completed, the server will be available to the network. | |
| 173 | ||
| 174 | #### `await discovery.refresh({ client, server })` | |
| 175 | ||
| 176 | Update the `PeerDiscovery` configuration, optionally toggling client and server modes. This will also trigger an immediate re-announce of the topic, when the `PeerDiscovery` is in server mode. | |
| 177 | ||
| 178 | #### `await discovery.destroy()` | |
| 179 | ||
| 180 | Stop discovering peers for the given topic. | |
| 181 | ||
| 182 | If a topic was previously joined in server mode, `leave` will stop announcing the topic on the DHT. If a topic was previously joined in client mode, `leave` will stop searching for servers announcing the topic. | |
| 183 | ||
| 184 | ## PeerInfo API | |
| 185 | ||
| 186 | `swarm.on('connection', ...)` emits a `PeerInfo` instance whenever a new connection is established. | |
| 187 | ||
| 188 | There is a one-to-one relationship between connections and `PeerInfo` objects -- if a single peer announces multiple topics, those topics will be multiplexed over a single connection. | |
| 189 | ||
| 190 | #### `peerInfo.publicKey` | |
| 191 | ||
| 192 | The peer's Noise public key. | |
| 193 | ||
| 194 | #### `peerInfo.topics` | |
| 195 | ||
| 196 | An Array of topics that this Peer is associated with -- `topics` will only be updated when the Peer is in client mode. | |
| 197 | ||
| 198 | #### `peerInfo.prioritized` | |
| 199 | ||
| 200 | If true, the swarm will rapidly attempt to reconnect to this peer. | |
| 201 | ||
| 202 | #### `peerInfo.ban(banStatus = false)` | |
| 203 | ||
| 204 | Ban or unban the peer. Banning will prevent any future reconnection attempts, but it will **not** close any existing connections. | |
| 205 | ||
| 206 | ## License | |
| 207 | ||
| 208 | MIT |
| … | … | @@ -0,0 +1,687 @@ |
| 1 | const { EventEmitter } = require('events') | |
| 2 | const { getStreamError } = require('streamx') | |
| 3 | const DHT = require('hyperdht') | |
| 4 | const spq = require('shuffled-priority-queue') | |
| 5 | const b4a = require('b4a') | |
| 6 | const unslab = require('unslab') | |
| 7 | ||
| 8 | const PeerInfo = require('./lib/peer-info') | |
| 9 | const RetryTimer = require('./lib/retry-timer') | |
| 10 | const ConnectionSet = require('./lib/connection-set') | |
| 11 | const PeerDiscovery = require('./lib/peer-discovery') | |
| 12 | ||
| 13 | const MAX_PEERS = 64 | |
| 14 | const MAX_PARALLEL = 3 | |
| 15 | const MAX_CLIENT_CONNECTIONS = Infinity // TODO: Change | |
| 16 | const MAX_SERVER_CONNECTIONS = Infinity | |
| 17 | ||
| 18 | const ERR_MISSING_TOPIC = 'Topic is required and must be a 32-byte buffer' | |
| 19 | const ERR_DESTROYED = 'Swarm has been destroyed' | |
| 20 | const ERR_DUPLICATE = 'Duplicate connection' | |
| 21 | const ERR_FIREWALL = 'Peer is firewalled' | |
| 22 | ||
| 23 | module.exports = class Hyperswarm extends EventEmitter { | |
| 24 | constructor(opts = {}) { | |
| 25 | super() | |
| 26 | const { | |
| 27 | seed, | |
| 28 | relayThrough, | |
| 29 | keyPair = DHT.keyPair(seed), | |
| 30 | maxPeers = MAX_PEERS, | |
| 31 | maxClientConnections = MAX_CLIENT_CONNECTIONS, | |
| 32 | maxServerConnections = MAX_SERVER_CONNECTIONS, | |
| 33 | maxParallel = MAX_PARALLEL, | |
| 34 | firewall = allowAll | |
| 35 | } = opts | |
| 36 | this.keyPair = keyPair | |
| 37 | ||
| 38 | this.dht = | |
| 39 | opts.dht || | |
| 40 | new DHT({ | |
| 41 | bootstrap: opts.bootstrap, | |
| 42 | nodes: opts.nodes, | |
| 43 | port: opts.port, | |
| 44 | deferRandomPunch: opts.deferRandomPunch, | |
| 45 | randomPunchInterval: opts.randomPunchInterval | |
| 46 | }) | |
| 47 | this.server = this.dht.createServer( | |
| 48 | { | |
| 49 | firewall: this._handleFirewall.bind(this), | |
| 50 | relayThrough: this._maybeRelayConnection.bind(this), | |
| 51 | handshakeClearWait: opts.handshakeClearWait | |
| 52 | }, | |
| 53 | this._handleServerConnection.bind(this) | |
| 54 | ) | |
| 55 | ||
| 56 | this.destroyed = false | |
| 57 | this.suspended = false | |
| 58 | this.maxPeers = maxPeers | |
| 59 | this.maxClientConnections = maxClientConnections | |
| 60 | this.maxServerConnections = maxServerConnections | |
| 61 | this.maxParallel = maxParallel | |
| 62 | this.relayThrough = relayThrough ? toRelayFunction(relayThrough) : null | |
| 63 | ||
| 64 | this.connecting = 0 | |
| 65 | this.connections = new Set() | |
| 66 | this.peers = new Map() | |
| 67 | this.explicitPeers = new Set() | |
| 68 | this.listening = null | |
| 69 | this.stats = { | |
| 70 | updates: 0, | |
| 71 | connects: { | |
| 72 | client: { | |
| 73 | opened: 0, | |
| 74 | closed: 0, | |
| 75 | attempted: 0 | |
| 76 | }, | |
| 77 | server: { | |
| 78 | // Note: there is no notion of 'attempts' for server connections | |
| 79 | opened: 0, | |
| 80 | closed: 0 | |
| 81 | } | |
| 82 | }, | |
| 83 | bannedPeers: 0 | |
| 84 | } | |
| 85 | ||
| 86 | this._discovery = new Map() | |
| 87 | this._timer = new RetryTimer(this._requeue.bind(this), { | |
| 88 | backoffs: opts.backoffs, | |
| 89 | jitter: opts.jitter | |
| 90 | }) | |
| 91 | this._queue = spq() | |
| 92 | ||
| 93 | this._allConnections = new ConnectionSet() | |
| 94 | this._pendingFlushes = [] | |
| 95 | this._flushTick = 0 | |
| 96 | ||
| 97 | this._drainingQueue = false | |
| 98 | this._clientConnections = 0 | |
| 99 | this._serverConnections = 0 | |
| 100 | this._firewall = firewall | |
| 101 | ||
| 102 | this.dht.on('network-change', this._handleNetworkChange.bind(this)) | |
| 103 | this.dht.on('network-update', this._handleNetworkUpdate.bind(this)) | |
| 104 | this.on('update', this._handleUpdate) | |
| 105 | } | |
| 106 | ||
| 107 | _maybeRelayConnection(force) { | |
| 108 | if (!this.relayThrough) return null | |
| 109 | return this.relayThrough(force, this) | |
| 110 | } | |
| 111 | ||
| 112 | _enqueue(peerInfo) { | |
| 113 | if (peerInfo.queued) return | |
| 114 | peerInfo.queued = true | |
| 115 | peerInfo._flushTick = this._flushTick | |
| 116 | this._queue.add(peerInfo) | |
| 117 | ||
| 118 | this._attemptClientConnections() | |
| 119 | } | |
| 120 | ||
| 121 | _requeue(batch) { | |
| 122 | if (this.suspended) return | |
| 123 | for (const peerInfo of batch) { | |
| 124 | peerInfo.waiting = false | |
| 125 | ||
| 126 | if ( | |
| 127 | peerInfo._updatePriority() === false || | |
| 128 | this._allConnections.has(peerInfo.publicKey) || | |
| 129 | peerInfo.queued | |
| 130 | ) { | |
| 131 | continue | |
| 132 | } | |
| 133 | peerInfo.queued = true | |
| 134 | peerInfo._flushTick = this._flushTick | |
| 135 | this._queue.add(peerInfo) | |
| 136 | } | |
| 137 | ||
| 138 | this._attemptClientConnections() | |
| 139 | } | |
| 140 | ||
| 141 | _flushMaybe(peerInfo) { | |
| 142 | for (let i = 0; i < this._pendingFlushes.length; i++) { | |
| 143 | const flush = this._pendingFlushes[i] | |
| 144 | if (peerInfo._flushTick > flush.tick) continue | |
| 145 | if (--flush.missing > 0) continue | |
| 146 | flush.onflush(true) | |
| 147 | this._pendingFlushes.splice(i--, 1) | |
| 148 | } | |
| 149 | } | |
| 150 | ||
| 151 | _withinMaxPeers() { | |
| 152 | const e = this.explicitPeers.size | |
| 153 | const factor = e === 0 ? 1 : e < 2 ? 2 : e < 4 ? 3 : 4 | |
| 154 | return factor * this._allConnections.size < this.maxPeers | |
| 155 | } | |
| 156 | ||
| 157 | _flushAllMaybe() { | |
| 158 | if ( | |
| 159 | this.connecting > 0 || | |
| 160 | (this._withinMaxPeers() && this._clientConnections < this.maxClientConnections) | |
| 161 | ) { | |
| 162 | return false | |
| 163 | } | |
| 164 | ||
| 165 | while (this._pendingFlushes.length) { | |
| 166 | const flush = this._pendingFlushes.pop() | |
| 167 | flush.onflush(true) | |
| 168 | } | |
| 169 | ||
| 170 | return true | |
| 171 | } | |
| 172 | ||
| 173 | _shouldConnectExplicit() { | |
| 174 | return !this.destroyed && !this.suspended && this.connecting < this.maxParallel | |
| 175 | } | |
| 176 | ||
| 177 | _shouldConnect() { | |
| 178 | return ( | |
| 179 | !this.destroyed && | |
| 180 | !this.suspended && | |
| 181 | this.connecting < this.maxParallel && | |
| 182 | this._withinMaxPeers() && | |
| 183 | this._clientConnections < this.maxClientConnections | |
| 184 | ) | |
| 185 | } | |
| 186 | ||
| 187 | _shouldRequeue(peerInfo) { | |
| 188 | if (this.suspended) return false | |
| 189 | if (peerInfo.explicit) return true | |
| 190 | for (const topic of peerInfo.topics) { | |
| 191 | if (this._discovery.has(b4a.toString(topic, 'hex')) && !this.destroyed) { | |
| 192 | return true | |
| 193 | } | |
| 194 | } | |
| 195 | return false | |
| 196 | } | |
| 197 | ||
| 198 | _connect(peerInfo, queued) { | |
| 199 | if (peerInfo.banned || this._allConnections.has(peerInfo.publicKey)) { | |
| 200 | if (queued) this._flushMaybe(peerInfo) | |
| 201 | return | |
| 202 | } | |
| 203 | ||
| 204 | // TODO: Support async firewalling at some point. | |
| 205 | if (this._handleFirewall(peerInfo.publicKey, null)) { | |
| 206 | if (queued) this._flushMaybe(peerInfo) | |
| 207 | return | |
| 208 | } | |
| 209 | ||
| 210 | const relayThrough = this._maybeRelayConnection(peerInfo.forceRelaying) | |
| 211 | const conn = this.dht.connect(peerInfo.publicKey, { | |
| 212 | relayAddresses: peerInfo.relayAddresses, | |
| 213 | keyPair: this.keyPair, | |
| 214 | relayThrough | |
| 215 | }) | |
| 216 | this._allConnections.add(conn) | |
| 217 | ||
| 218 | this.stats.connects.client.attempted++ | |
| 219 | ||
| 220 | this.connecting++ | |
| 221 | this._clientConnections++ | |
| 222 | let opened = false | |
| 223 | ||
| 224 | const onerror = (err) => { | |
| 225 | if (this.relayThrough && shouldForceRelaying(err.code)) { | |
| 226 | peerInfo.forceRelaying = true | |
| 227 | // Reset the attempts in order to fast connect to relay | |
| 228 | peerInfo.attempts = 0 | |
| 229 | } | |
| 230 | } | |
| 231 | ||
| 232 | // Removed once a connection is opened | |
| 233 | conn.on('error', onerror) | |
| 234 | ||
| 235 | conn.on('open', () => { | |
| 236 | opened = true | |
| 237 | this.stats.connects.client.opened++ | |
| 238 | ||
| 239 | this._connectDone() | |
| 240 | this.connections.add(conn) | |
| 241 | conn.removeListener('error', onerror) | |
| 242 | peerInfo._connected() | |
| 243 | peerInfo.client = true | |
| 244 | this.emit('connection', conn, peerInfo) | |
| 245 | if (queued) this._flushMaybe(peerInfo) | |
| 246 | ||
| 247 | this.emit('update') | |
| 248 | }) | |
| 249 | conn.on('close', () => { | |
| 250 | if (!opened) this._connectDone() | |
| 251 | this.stats.connects.client.closed++ | |
| 252 | ||
| 253 | const err = getStreamError(conn) | |
| 254 | if (shouldBan(err)) { | |
| 255 | this._banPeer(peerInfo, true, err) | |
| 256 | } | |
| 257 | ||
| 258 | this.connections.delete(conn) | |
| 259 | this._allConnections.delete(conn) | |
| 260 | this._clientConnections-- | |
| 261 | peerInfo._disconnected() | |
| 262 | ||
| 263 | peerInfo.waiting = this._shouldRequeue(peerInfo) && this._timer.add(peerInfo) | |
| 264 | this._maybeDeletePeer(peerInfo) | |
| 265 | ||
| 266 | if (!opened && queued) this._flushMaybe(peerInfo) | |
| 267 | ||
| 268 | this._attemptClientConnections() | |
| 269 | ||
| 270 | this.emit('update') | |
| 271 | }) | |
| 272 | ||
| 273 | this.emit('update') | |
| 274 | } | |
| 275 | ||
| 276 | _connectDone() { | |
| 277 | this.connecting-- | |
| 278 | ||
| 279 | if (this.connecting < this.maxParallel) this._attemptClientConnections() | |
| 280 | if (this.connecting === 0) this._flushAllMaybe() | |
| 281 | } | |
| 282 | ||
| 283 | // Called when the PeerQueue indicates a connection should be attempted. | |
| 284 | _attemptClientConnections() { | |
| 285 | // Guard against re-entries - unsure if it still needed but doesn't hurt | |
| 286 | if (this._drainingQueue || this.suspended) return | |
| 287 | this._drainingQueue = true | |
| 288 | ||
| 289 | for (const peerInfo of this.explicitPeers) { | |
| 290 | if (!this._shouldConnectExplicit()) break | |
| 291 | if ( | |
| 292 | peerInfo.attempts >= 5 || | |
| 293 | Date.now() - peerInfo.disconnectedTime < peerInfo.attempts * 1000 | |
| 294 | ) { | |
| 295 | continue | |
| 296 | } | |
| 297 | this._connect(peerInfo, false) | |
| 298 | } | |
| 299 | ||
| 300 | while (this._queue.length && this._shouldConnect()) { | |
| 301 | const peerInfo = this._queue.shift() | |
| 302 | peerInfo.queued = false | |
| 303 | this._connect(peerInfo, true) | |
| 304 | } | |
| 305 | this._drainingQueue = false | |
| 306 | if (this.connecting === 0) this._flushAllMaybe() | |
| 307 | } | |
| 308 | ||
| 309 | _handleFirewall(remotePublicKey, payload) { | |
| 310 | if (b4a.equals(remotePublicKey, this.keyPair.publicKey)) return true | |
| 311 | ||
| 312 | let peerInfo = this.peers.get(b4a.toString(remotePublicKey, 'hex')) | |
| 313 | if (peerInfo && peerInfo.banned) return true | |
| 314 | ||
| 315 | const firewalled = this._firewall(remotePublicKey, payload) | |
| 316 | if (firewalled) { | |
| 317 | if (!peerInfo) peerInfo = this._upsertPeer(remotePublicKey) | |
| 318 | this._banPeer(peerInfo, true, new Error(ERR_FIREWALL)) | |
| 319 | } | |
| 320 | ||
| 321 | return firewalled | |
| 322 | } | |
| 323 | ||
| 324 | _handleServerConnectionSwap(existing, conn) { | |
| 325 | let closed = false | |
| 326 | ||
| 327 | existing.on('close', () => { | |
| 328 | if (closed) return | |
| 329 | ||
| 330 | conn.removeListener('error', noop) | |
| 331 | conn.removeListener('close', onclose) | |
| 332 | ||
| 333 | this._handleServerConnection(conn) | |
| 334 | }) | |
| 335 | ||
| 336 | conn.on('error', noop) | |
| 337 | conn.on('close', onclose) | |
| 338 | ||
| 339 | function onclose() { | |
| 340 | closed = true | |
| 341 | } | |
| 342 | } | |
| 343 | ||
| 344 | // Called when the DHT receives a new server connection. | |
| 345 | _handleServerConnection(conn) { | |
| 346 | if (this.destroyed || this.suspended) { | |
| 347 | // TODO: Investigate why a final server connection can be received after close | |
| 348 | conn.on('error', noop) | |
| 349 | return conn.destroy(ERR_DESTROYED) | |
| 350 | } | |
| 351 | ||
| 352 | const existing = this._allConnections.get(conn.remotePublicKey) | |
| 353 | ||
| 354 | if (existing) { | |
| 355 | // If both connections are from the same peer, | |
| 356 | // - pick the new one if the existing stream is already established (has sent and received bytes), | |
| 357 | // because the other client must have lost that connection and be reconnecting | |
| 358 | // - otherwise, pick the one thats expected to initiate in a tie break | |
| 359 | const existingIsOutdated = existing.rawBytesRead > 0 && existing.rawBytesWritten > 0 | |
| 360 | const expectedInitiator = b4a.compare(conn.publicKey, conn.remotePublicKey) > 0 | |
| 361 | const keepNew = existingIsOutdated || expectedInitiator === conn.isInitiator | |
| 362 | ||
| 363 | if (keepNew === false) { | |
| 364 | existing.sendKeepAlive() | |
| 365 | conn.on('error', noop) | |
| 366 | conn.destroy(new Error(ERR_DUPLICATE)) | |
| 367 | return | |
| 368 | } | |
| 369 | ||
| 370 | existing.on('error', noop) | |
| 371 | existing.destroy(new Error(ERR_DUPLICATE)) | |
| 372 | this._handleServerConnectionSwap(existing, conn) | |
| 373 | return | |
| 374 | } | |
| 375 | ||
| 376 | // When reaching here, the connection will always be 'opened' next tick | |
| 377 | this.stats.connects.server.opened++ | |
| 378 | ||
| 379 | const peerInfo = this._upsertPeer(conn.remotePublicKey, null) | |
| 380 | ||
| 381 | this.connections.add(conn) | |
| 382 | this._allConnections.add(conn) | |
| 383 | this._serverConnections++ | |
| 384 | ||
| 385 | conn.on('close', () => { | |
| 386 | const err = getStreamError(conn) | |
| 387 | if (shouldBan(err)) { | |
| 388 | this._banPeer(peerInfo, true, err) | |
| 389 | } | |
| 390 | ||
| 391 | this.connections.delete(conn) | |
| 392 | this._allConnections.delete(conn) | |
| 393 | this._serverConnections-- | |
| 394 | this.stats.connects.server.closed++ | |
| 395 | ||
| 396 | this._maybeDeletePeer(peerInfo) | |
| 397 | ||
| 398 | this._attemptClientConnections() | |
| 399 | ||
| 400 | this.emit('update') | |
| 401 | }) | |
| 402 | peerInfo.client = false | |
| 403 | this.emit('connection', conn, peerInfo) | |
| 404 | ||
| 405 | this.emit('update') | |
| 406 | } | |
| 407 | ||
| 408 | _upsertPeer(publicKey, relayAddresses) { | |
| 409 | if (b4a.equals(publicKey, this.keyPair.publicKey)) return null | |
| 410 | const keyString = b4a.toString(publicKey, 'hex') | |
| 411 | let peerInfo = this.peers.get(keyString) | |
| 412 | ||
| 413 | if (peerInfo) { | |
| 414 | peerInfo.relayAddresses = relayAddresses // new is always better | |
| 415 | return peerInfo | |
| 416 | } | |
| 417 | ||
| 418 | peerInfo = new PeerInfo({ | |
| 419 | publicKey, | |
| 420 | relayAddresses | |
| 421 | }) | |
| 422 | ||
| 423 | this.peers.set(keyString, peerInfo) | |
| 424 | return peerInfo | |
| 425 | } | |
| 426 | ||
| 427 | _handleUpdate() { | |
| 428 | this.stats.updates++ | |
| 429 | } | |
| 430 | ||
| 431 | _maybeDeletePeer(peerInfo) { | |
| 432 | if (!peerInfo.shouldGC()) return | |
| 433 | ||
| 434 | const hasActiveConn = this._allConnections.has(peerInfo.publicKey) | |
| 435 | if (hasActiveConn) return | |
| 436 | ||
| 437 | const keyString = b4a.toString(peerInfo.publicKey, 'hex') | |
| 438 | this.peers.delete(keyString) | |
| 439 | } | |
| 440 | ||
| 441 | /* | |
| 442 | * Called when a peer is actively discovered during a lookup. | |
| 443 | * | |
| 444 | * Three conditions: | |
| 445 | * 1. Not a known peer -- insert into queue | |
| 446 | * 2. A known peer with normal priority -- do nothing | |
| 447 | * 3. A known peer with low priority -- bump priority, because it's been rediscovered | |
| 448 | */ | |
| 449 | _handlePeer(peer, topic) { | |
| 450 | const peerInfo = this._upsertPeer(peer.publicKey, peer.relayAddresses) | |
| 451 | if (peerInfo) peerInfo._topic(topic) | |
| 452 | if (!peerInfo || this._allConnections.has(peer.publicKey)) return | |
| 453 | if (!peerInfo.prioritized || peerInfo.server) peerInfo._reset() | |
| 454 | if (peerInfo._updatePriority()) { | |
| 455 | this._enqueue(peerInfo) | |
| 456 | } | |
| 457 | } | |
| 458 | ||
| 459 | async _handleNetworkUpdate() { | |
| 460 | if (!this.online) return | |
| 461 | this._handleNetworkChange() | |
| 462 | } | |
| 463 | ||
| 464 | async _handleNetworkChange() { | |
| 465 | if (this.suspended) return | |
| 466 | ||
| 467 | // prioritize figuring out if existing connections are dead | |
| 468 | for (const conn of this._allConnections) { | |
| 469 | conn.sendKeepAlive() | |
| 470 | } | |
| 471 | ||
| 472 | const refreshes = [] | |
| 473 | ||
| 474 | for (const discovery of this._discovery.values()) { | |
| 475 | refreshes.push(discovery.refresh()) | |
| 476 | } | |
| 477 | ||
| 478 | await Promise.allSettled(refreshes) | |
| 479 | } | |
| 480 | ||
| 481 | _banPeer(peerInfo, banned, err) { | |
| 482 | peerInfo.ban(banned) | |
| 483 | this.stats.bannedPeers++ | |
| 484 | this.emit('ban', peerInfo, err) | |
| 485 | } | |
| 486 | ||
| 487 | status(key) { | |
| 488 | return this._discovery.get(b4a.toString(key, 'hex')) || null | |
| 489 | } | |
| 490 | ||
| 491 | listen() { | |
| 492 | if (!this.listening) { | |
| 493 | if (this.destroyed) throw new Error('Swarm destroyed') | |
| 494 | this.listening = this.server.listen(this.keyPair) | |
| 495 | } | |
| 496 | return this.listening | |
| 497 | } | |
| 498 | ||
| 499 | // Object that exposes a cancellation method (destroy) | |
| 500 | // TODO: When you rejoin, it should reannounce + bump lookup priority | |
| 501 | join(topic, opts = {}) { | |
| 502 | if (this.destroyed) throw new Error('Swarm destroyed') | |
| 503 | if (!topic) throw new Error(ERR_MISSING_TOPIC) | |
| 504 | topic = unslab(topic) | |
| 505 | ||
| 506 | const topicString = b4a.toString(topic, 'hex') | |
| 507 | ||
| 508 | let discovery = this._discovery.get(topicString) | |
| 509 | ||
| 510 | if (discovery && !discovery.destroyed) { | |
| 511 | return discovery.session(opts) | |
| 512 | } | |
| 513 | ||
| 514 | discovery = new PeerDiscovery(this, topic, { | |
| 515 | limit: opts.limit, | |
| 516 | wait: discovery ? discovery.destroy() : null, | |
| 517 | suspended: this.suspended, | |
| 518 | onpeer: (peer) => this._handlePeer(peer, topic) | |
| 519 | }) | |
| 520 | this._discovery.set(topicString, discovery) | |
| 521 | return discovery.session(opts) | |
| 522 | } | |
| 523 | ||
| 524 | // Returns a promise | |
| 525 | async leave(topic) { | |
| 526 | if (!topic) throw new Error(ERR_MISSING_TOPIC) | |
| 527 | const topicString = b4a.toString(topic, 'hex') | |
| 528 | if (!this._discovery.has(topicString)) return Promise.resolve() | |
| 529 | ||
| 530 | const discovery = this._discovery.get(topicString) | |
| 531 | ||
| 532 | try { | |
| 533 | await discovery.destroy() | |
| 534 | } catch { | |
| 535 | // ignore, prop network | |
| 536 | } | |
| 537 | ||
| 538 | if (this._discovery.get(topicString) === discovery) { | |
| 539 | this._discovery.delete(topicString) | |
| 540 | } | |
| 541 | } | |
| 542 | ||
| 543 | joinPeer(publicKey) { | |
| 544 | const peerInfo = this._upsertPeer(publicKey, null) | |
| 545 | if (!peerInfo) return | |
| 546 | if (!this.explicitPeers.has(peerInfo)) { | |
| 547 | peerInfo.explicit = true | |
| 548 | this.explicitPeers.add(peerInfo) | |
| 549 | } | |
| 550 | if (this._allConnections.has(publicKey)) return | |
| 551 | if (peerInfo._updatePriority()) { | |
| 552 | this._enqueue(peerInfo) | |
| 553 | } | |
| 554 | } | |
| 555 | ||
| 556 | leavePeer(publicKey) { | |
| 557 | const keyString = b4a.toString(publicKey, 'hex') | |
| 558 | if (!this.peers.has(keyString)) return | |
| 559 | ||
| 560 | const peerInfo = this.peers.get(keyString) | |
| 561 | peerInfo.explicit = false | |
| 562 | this.explicitPeers.delete(peerInfo) | |
| 563 | this._maybeDeletePeer(peerInfo) | |
| 564 | } | |
| 565 | ||
| 566 | // Returns a promise | |
| 567 | async flush() { | |
| 568 | const allFlushed = [...this._discovery.values()].map((v) => v.flushed()) | |
| 569 | await Promise.all(allFlushed) | |
| 570 | if (this._flushAllMaybe()) return true | |
| 571 | const pendingSize = this._allConnections.size - this.connections.size | |
| 572 | if (!this._queue.length && !pendingSize) return true | |
| 573 | return new Promise((resolve) => { | |
| 574 | this._pendingFlushes.push({ | |
| 575 | onflush: resolve, | |
| 576 | missing: this._queue.length + pendingSize, | |
| 577 | tick: this._flushTick++ | |
| 578 | }) | |
| 579 | }) | |
| 580 | } | |
| 581 | ||
| 582 | async clear() { | |
| 583 | const cleared = Promise.allSettled([...this._discovery.values()].map((d) => d.destroy())) | |
| 584 | this._discovery.clear() | |
| 585 | return cleared | |
| 586 | } | |
| 587 | ||
| 588 | async destroy({ force } = {}) { | |
| 589 | if (this.destroyed && !force) return | |
| 590 | this.destroyed = true | |
| 591 | ||
| 592 | this._timer.destroy() | |
| 593 | ||
| 594 | if (!force) await this.clear() | |
| 595 | ||
| 596 | await this.server.close() | |
| 597 | ||
| 598 | while (this._pendingFlushes.length) { | |
| 599 | const flush = this._pendingFlushes.pop() | |
| 600 | flush.onflush(false) | |
| 601 | } | |
| 602 | ||
| 603 | await this.dht.destroy({ force }) | |
| 604 | } | |
| 605 | ||
| 606 | async suspend({ log = noop } = {}) { | |
| 607 | if (this.suspended) return | |
| 608 | ||
| 609 | const promises = [] | |
| 610 | ||
| 611 | promises.push(this.server.suspend({ log })) | |
| 612 | ||
| 613 | for (const discovery of this._discovery.values()) { | |
| 614 | promises.push(discovery.suspend({ log })) | |
| 615 | } | |
| 616 | ||
| 617 | const pending = [] | |
| 618 | for (const connection of this._allConnections) { | |
| 619 | connection.destroy() | |
| 620 | pending.push(new Promise((resolve) => connection.on('close', resolve))) | |
| 621 | } | |
| 622 | ||
| 623 | this.suspended = true | |
| 624 | ||
| 625 | log('Suspending server and discovery... (' + promises.length + ')') | |
| 626 | await Promise.allSettled(promises) | |
| 627 | log('Done, suspending the dht...') | |
| 628 | await this.dht.suspend({ log }) | |
| 629 | log('Done, swarm fully suspended') | |
| 630 | ||
| 631 | await Promise.all(pending) | |
| 632 | ||
| 633 | // reset queue | |
| 634 | this._timer.destroy() | |
| 635 | this._timer = new RetryTimer(this._requeue.bind(this), { | |
| 636 | backoffs: this._timer.backoffs, | |
| 637 | jitter: this._timer.jitter | |
| 638 | }) | |
| 639 | this._queue = spq() | |
| 640 | } | |
| 641 | ||
| 642 | async resume({ log = noop } = {}) { | |
| 643 | if (!this.suspended) return | |
| 644 | ||
| 645 | log('Resuming the dht') | |
| 646 | await this.dht.resume() | |
| 647 | log('Done, resuming the server') | |
| 648 | await this.server.resume() | |
| 649 | log('Done, all discovery') | |
| 650 | ||
| 651 | for (const discovery of this._discovery.values()) { | |
| 652 | discovery.resume() | |
| 653 | } | |
| 654 | ||
| 655 | this.suspended = false | |
| 656 | this._attemptClientConnections() | |
| 657 | } | |
| 658 | ||
| 659 | topics() { | |
| 660 | return this._discovery.values() | |
| 661 | } | |
| 662 | } | |
| 663 | ||
| 664 | function noop() {} | |
| 665 | ||
| 666 | function allowAll() { | |
| 667 | return false | |
| 668 | } | |
| 669 | ||
| 670 | function shouldForceRelaying(code) { | |
| 671 | return ( | |
| 672 | code === 'HOLEPUNCH_ABORTED' || | |
| 673 | code === 'HOLEPUNCH_DOUBLE_RANDOMIZED_NATS' || | |
| 674 | code === 'REMOTE_NOT_HOLEPUNCHABLE' | |
| 675 | ) | |
| 676 | } | |
| 677 | ||
| 678 | function shouldBan() { | |
| 679 | // return !!err && err.name === 'HypercoreError' && err.code === 'INVALID_OPERATION' | |
| 680 | return false | |
| 681 | } | |
| 682 | ||
| 683 | function toRelayFunction(relayThrough) { | |
| 684 | return typeof relayThrough === 'function' | |
| 685 | ? relayThrough | |
| 686 | : (force, swarm) => (force || swarm.dht.randomized ? relayThrough : null) | |
| 687 | } |
| … | … | @@ -0,0 +1,33 @@ |
| 1 | module.exports = class BulkTimer { | |
| 2 | constructor(time, fn) { | |
| 3 | this._time = time | |
| 4 | this._fn = fn | |
| 5 | this._interval = null | |
| 6 | this._next = [] | |
| 7 | this._pending = [] | |
| 8 | this._destroyed = false | |
| 9 | } | |
| 10 | ||
| 11 | destroy() { | |
| 12 | if (this._destroyed) return | |
| 13 | this._destroyed = true | |
| 14 | clearInterval(this._interval) | |
| 15 | this._interval = null | |
| 16 | } | |
| 17 | ||
| 18 | _ontick() { | |
| 19 | if (!this._next.length && !this._pending.length) return | |
| 20 | if (this._next.length) this._fn(this._next) | |
| 21 | this._next = this._pending | |
| 22 | this._pending = [] | |
| 23 | } | |
| 24 | ||
| 25 | add(info) { | |
| 26 | if (this._destroyed) return | |
| 27 | if (!this._interval) { | |
| 28 | this._interval = setInterval(this._ontick.bind(this), Math.floor(this._time * 0.66)) | |
| 29 | } | |
| 30 | ||
| 31 | this._pending.push(info) | |
| 32 | } | |
| 33 | } |
| … | … | @@ -0,0 +1,38 @@ |
| 1 | const b4a = require('b4a') | |
| 2 | ||
| 3 | module.exports = class ConnectionSet { | |
| 4 | constructor() { | |
| 5 | this._byPublicKey = new Map() | |
| 6 | } | |
| 7 | ||
| 8 | [Symbol.iterator]() { | |
| 9 | return this._byPublicKey.values() | |
| 10 | } | |
| 11 | ||
| 12 | get size() { | |
| 13 | return this._byPublicKey.size | |
| 14 | } | |
| 15 | ||
| 16 | has(publicKey) { | |
| 17 | return this._byPublicKey.has(toHex(publicKey)) | |
| 18 | } | |
| 19 | ||
| 20 | get(publicKey) { | |
| 21 | return this._byPublicKey.get(toHex(publicKey)) | |
| 22 | } | |
| 23 | ||
| 24 | add(connection) { | |
| 25 | this._byPublicKey.set(b4a.toString(connection.remotePublicKey, 'hex'), connection) | |
| 26 | } | |
| 27 | ||
| 28 | delete(connection) { | |
| 29 | const keyString = b4a.toString(connection.remotePublicKey, 'hex') | |
| 30 | const existing = this._byPublicKey.get(keyString) | |
| 31 | if (existing !== connection) return | |
| 32 | this._byPublicKey.delete(keyString) | |
| 33 | } | |
| 34 | } | |
| 35 | ||
| 36 | function toHex(b) { | |
| 37 | return typeof b === 'string' ? b : b4a.toString(b, 'hex') | |
| 38 | } |
| … | … | @@ -0,0 +1,358 @@ |
| 1 | const safetyCatch = require('safety-catch') | |
| 2 | const b4a = require('b4a') | |
| 3 | ||
| 4 | const REFRESH_INTERVAL = 1000 * 60 * 10 // 10 min | |
| 5 | const RANDOM_JITTER = 1000 * 60 * 2 // 2 min | |
| 6 | const DELAY_GRACE_PERIOD = 1000 * 30 // 30s | |
| 7 | ||
| 8 | const MAX_DISCOVERY_CACHE = 64 | |
| 9 | ||
| 10 | module.exports = class PeerDiscovery { | |
| 11 | constructor( | |
| 12 | swarm, | |
| 13 | topic, | |
| 14 | { limit = Infinity, wait = null, suspended = false, onpeer = noop, onerror = safetyCatch } | |
| 15 | ) { | |
| 16 | this.limit = limit | |
| 17 | this.swarm = swarm | |
| 18 | this.topic = topic | |
| 19 | this.isClient = false | |
| 20 | this.isServer = false | |
| 21 | this.destroyed = false | |
| 22 | this.destroying = null | |
| 23 | this.suspended = suspended | |
| 24 | ||
| 25 | this._sessions = [] | |
| 26 | this._clientSessions = 0 | |
| 27 | this._serverSessions = 0 | |
| 28 | ||
| 29 | this._onpeer = onpeer | |
| 30 | this._onerror = onerror | |
| 31 | ||
| 32 | this._discovered = new Set() | |
| 33 | this._activeQuery = null | |
| 34 | this._timer = null | |
| 35 | this._currentRefresh = null | |
| 36 | this._closestNodes = null | |
| 37 | this._firstAnnounce = true | |
| 38 | this._needsUnannounce = false | |
| 39 | this._refreshes = 0 | |
| 40 | this._wait = wait | |
| 41 | } | |
| 42 | ||
| 43 | session({ server = true, client = true, limit = Infinity, onerror = safetyCatch }) { | |
| 44 | if (this.destroyed) throw new Error('PeerDiscovery is destroyed') | |
| 45 | const session = new PeerDiscoverySession(this) | |
| 46 | session.refresh({ server, client, limit }).catch(onerror) | |
| 47 | this._sessions.push(session) | |
| 48 | return session | |
| 49 | } | |
| 50 | ||
| 51 | _refreshLater(eager) { | |
| 52 | const jitter = Math.round(Math.random() * RANDOM_JITTER) | |
| 53 | const delay = !eager ? REFRESH_INTERVAL + jitter : jitter | |
| 54 | ||
| 55 | if (this._timer) clearTimeout(this._timer) | |
| 56 | ||
| 57 | const startTime = Date.now() | |
| 58 | this._timer = setTimeout(() => { | |
| 59 | // If your laptop went to sleep, and is coming back online... | |
| 60 | const overdue = Date.now() - startTime > delay + DELAY_GRACE_PERIOD | |
| 61 | if (overdue) this._refreshLater(true) | |
| 62 | else this.refresh().catch(this._onerror) | |
| 63 | }, delay) | |
| 64 | } | |
| 65 | ||
| 66 | _isActive() { | |
| 67 | return !this.destroyed && !this.suspended | |
| 68 | } | |
| 69 | ||
| 70 | // TODO: Allow announce to be an argument to this | |
| 71 | // TODO: Maybe announce should be a setter? | |
| 72 | async _refresh() { | |
| 73 | if (this.suspended) return | |
| 74 | const clock = ++this._refreshes | |
| 75 | ||
| 76 | if (this._wait) { | |
| 77 | await this._wait | |
| 78 | this._wait = null | |
| 79 | if (clock !== this._refreshes || !this._isActive()) return | |
| 80 | } | |
| 81 | ||
| 82 | const clear = this.isServer && this._firstAnnounce | |
| 83 | if (clear) this._firstAnnounce = false | |
| 84 | ||
| 85 | const opts = { | |
| 86 | clear, | |
| 87 | closestNodes: this._closestNodes | |
| 88 | } | |
| 89 | ||
| 90 | if (this.isServer) { | |
| 91 | await this.swarm.listen() | |
| 92 | // if a parallel refresh is happening, yield to the new one | |
| 93 | if (clock !== this._refreshes || !this._isActive()) return | |
| 94 | this._needsUnannounce = true | |
| 95 | } | |
| 96 | ||
| 97 | let limit = this.limit | |
| 98 | ||
| 99 | if (limit < Infinity && limit > 0) { | |
| 100 | for (const id of this._discovered) { | |
| 101 | if (!this.swarm.connections.has(id)) continue | |
| 102 | if (--limit === 0) break | |
| 103 | } | |
| 104 | } | |
| 105 | ||
| 106 | this._discovered.clear() | |
| 107 | ||
| 108 | const announcing = this.isServer | |
| 109 | const query = (this._activeQuery = announcing | |
| 110 | ? this.swarm.dht.announce( | |
| 111 | this.topic, | |
| 112 | this.swarm.keyPair, | |
| 113 | this.swarm.server.relayAddresses, | |
| 114 | opts | |
| 115 | ) | |
| 116 | : this._needsUnannounce | |
| 117 | ? this.swarm.dht.lookupAndUnannounce(this.topic, this.swarm.keyPair, opts) | |
| 118 | : this.swarm.dht.lookup(this.topic, opts)) | |
| 119 | ||
| 120 | try { | |
| 121 | for await (const data of this._activeQuery) { | |
| 122 | if (!this.isClient || !this._isActive()) continue | |
| 123 | for (const peer of data.peers) { | |
| 124 | if (limit < Infinity) { | |
| 125 | const id = b4a.toString(peer.publicKey, 'hex') | |
| 126 | ||
| 127 | if (this._discovered.size < MAX_DISCOVERY_CACHE) { | |
| 128 | this._discovered.add(id) | |
| 129 | } | |
| 130 | ||
| 131 | if (limit === 0) continue | |
| 132 | ||
| 133 | // there is a chance it has updated a connection during discovery and we go over | |
| 134 | // the limit - thats acceptable to avoid a complexity spiral here. | |
| 135 | if (!this.swarm.connections.has(id)) limit-- | |
| 136 | } | |
| 137 | ||
| 138 | this._onpeer(peer, data) | |
| 139 | } | |
| 140 | } | |
| 141 | } catch (err) { | |
| 142 | if (this._isActive()) throw err | |
| 143 | } finally { | |
| 144 | if (this._activeQuery === query) { | |
| 145 | this._activeQuery = null | |
| 146 | if (!this.destroyed && !this.suspended) this._refreshLater(false) | |
| 147 | } | |
| 148 | } | |
| 149 | ||
| 150 | // This is set at the very end, when the query completes successfully. | |
| 151 | this._closestNodes = query.closestNodes | |
| 152 | ||
| 153 | if (clock !== this._refreshes) return | |
| 154 | ||
| 155 | // In this is the latest query, unannounce has been fulfilled as well | |
| 156 | if (!announcing) this._needsUnannounce = false | |
| 157 | } | |
| 158 | ||
| 159 | async refresh() { | |
| 160 | if (this.destroyed) throw new Error('PeerDiscovery is destroyed') | |
| 161 | ||
| 162 | const server = this._serverSessions > 0 | |
| 163 | const client = this._clientSessions > 0 | |
| 164 | ||
| 165 | if (this.suspended) return | |
| 166 | ||
| 167 | if (server === this.isServer && client === this.isClient) { | |
| 168 | if (this._currentRefresh) return this._currentRefresh | |
| 169 | this._currentRefresh = this._refresh() | |
| 170 | } else { | |
| 171 | if (this._activeQuery) this._activeQuery.destroy() | |
| 172 | this.isServer = server | |
| 173 | this.isClient = client | |
| 174 | this._currentRefresh = this._refresh() | |
| 175 | } | |
| 176 | ||
| 177 | const refresh = this._currentRefresh | |
| 178 | try { | |
| 179 | await refresh | |
| 180 | } catch { | |
| 181 | return false | |
| 182 | } finally { | |
| 183 | if (refresh === this._currentRefresh) { | |
| 184 | this._currentRefresh = null | |
| 185 | } | |
| 186 | } | |
| 187 | ||
| 188 | return true | |
| 189 | } | |
| 190 | ||
| 191 | async flushed() { | |
| 192 | if (this.swarm.listening) await this.swarm.listening | |
| 193 | ||
| 194 | try { | |
| 195 | await this._currentRefresh | |
| 196 | return true | |
| 197 | } catch { | |
| 198 | return false | |
| 199 | } | |
| 200 | } | |
| 201 | ||
| 202 | async _destroyMaybe() { | |
| 203 | if (this.destroyed) return | |
| 204 | ||
| 205 | try { | |
| 206 | if (this._sessions.length === 0) await this.swarm.leave(this.topic) | |
| 207 | else if (this._serverSessions === 0 && this._needsUnannounce) await this.refresh() | |
| 208 | } catch (err) { | |
| 209 | // ignore network failures here, as we are tearing down | |
| 210 | safetyCatch(err) | |
| 211 | } | |
| 212 | } | |
| 213 | ||
| 214 | destroy() { | |
| 215 | if (this.destroying) return this.destroying | |
| 216 | this.destroying = this._destroy() | |
| 217 | return this.destroying | |
| 218 | } | |
| 219 | ||
| 220 | async _abort(log) { | |
| 221 | const id = log === noop ? '' : b4a.toString(this.topic, 'hex') | |
| 222 | ||
| 223 | log('Aborting discovery', id) | |
| 224 | if (this._wait) await this._wait | |
| 225 | log('Aborting discovery (post wait)', id) | |
| 226 | ||
| 227 | if (this._activeQuery) { | |
| 228 | this._activeQuery.destroy() | |
| 229 | this._activeQuery = null | |
| 230 | } | |
| 231 | if (this._timer) { | |
| 232 | clearTimeout(this._timer) | |
| 233 | this._timer = null | |
| 234 | } | |
| 235 | ||
| 236 | let nodes = this._closestNodes | |
| 237 | ||
| 238 | if (this._currentRefresh) { | |
| 239 | try { | |
| 240 | await this._currentRefresh | |
| 241 | } catch { | |
| 242 | // If the destroy causes the refresh to fail, suppress it. | |
| 243 | } | |
| 244 | } | |
| 245 | ||
| 246 | log('Aborting discovery (post refresh)', id) | |
| 247 | if (this._isActive()) return | |
| 248 | ||
| 249 | if (!nodes) nodes = this._closestNodes | |
| 250 | else if (this._closestNodes !== nodes) { | |
| 251 | const len = nodes.length | |
| 252 | for (const newer of this._closestNodes) { | |
| 253 | if (newer.id && !hasNode(nodes, len, newer)) nodes.push(newer) | |
| 254 | } | |
| 255 | } | |
| 256 | ||
| 257 | if (this._needsUnannounce) { | |
| 258 | log('Unannouncing discovery', id) | |
| 259 | if (nodes && nodes.length) { | |
| 260 | await this.swarm.dht.unannounce(this.topic, this.swarm.keyPair, { | |
| 261 | closestNodes: nodes, | |
| 262 | onlyClosestNodes: true, | |
| 263 | force: true | |
| 264 | }) | |
| 265 | } | |
| 266 | this._needsUnannounce = false | |
| 267 | log('Unannouncing discovery (done)', id) | |
| 268 | } | |
| 269 | } | |
| 270 | ||
| 271 | _destroy() { | |
| 272 | if (this.destroyed) return | |
| 273 | this.destroyed = true | |
| 274 | return this._abort(noop) | |
| 275 | } | |
| 276 | ||
| 277 | async suspend({ log = noop } = {}) { | |
| 278 | if (this.suspended) return | |
| 279 | this.suspended = true | |
| 280 | try { | |
| 281 | await this._abort(log) | |
| 282 | } catch { | |
| 283 | // ignore | |
| 284 | } | |
| 285 | } | |
| 286 | ||
| 287 | resume() { | |
| 288 | if (!this.suspended) return | |
| 289 | this.suspended = false | |
| 290 | this.refresh().catch(noop) | |
| 291 | } | |
| 292 | } | |
| 293 | ||
| 294 | class PeerDiscoverySession { | |
| 295 | constructor(discovery) { | |
| 296 | this.discovery = discovery | |
| 297 | this.isClient = false | |
| 298 | this.isServer = false | |
| 299 | this.destroyed = false | |
| 300 | } | |
| 301 | ||
| 302 | get swarm() { | |
| 303 | return this.discovery.swarm | |
| 304 | } | |
| 305 | ||
| 306 | get topic() { | |
| 307 | return this.discovery.topic | |
| 308 | } | |
| 309 | ||
| 310 | async refresh({ client = this.isClient, server = this.isServer, limit = Infinity } = {}) { | |
| 311 | if (this.destroyed) throw new Error('PeerDiscovery is destroyed') | |
| 312 | if (!client && !server) throw new Error('Cannot refresh with neither client nor server option') | |
| 313 | ||
| 314 | if (client !== this.isClient) { | |
| 315 | this.isClient = client | |
| 316 | this.discovery._clientSessions += client ? 1 : -1 | |
| 317 | } | |
| 318 | ||
| 319 | if (server !== this.isServer) { | |
| 320 | this.isServer = server | |
| 321 | this.discovery._serverSessions += server ? 1 : -1 | |
| 322 | } | |
| 323 | ||
| 324 | this.discovery.limit = limit | |
| 325 | ||
| 326 | return this.discovery.refresh() | |
| 327 | } | |
| 328 | ||
| 329 | async flushed() { | |
| 330 | return this.discovery.flushed() | |
| 331 | } | |
| 332 | ||
| 333 | async destroy() { | |
| 334 | if (this.destroyed) return | |
| 335 | this.destroyed = true | |
| 336 | ||
| 337 | if (this.isClient) this.discovery._clientSessions-- | |
| 338 | if (this.isServer) this.discovery._serverSessions-- | |
| 339 | ||
| 340 | const index = this.discovery._sessions.indexOf(this) | |
| 341 | const head = this.discovery._sessions.pop() | |
| 342 | ||
| 343 | if (head !== this) this.discovery._sessions[index] = head | |
| 344 | ||
| 345 | return this.discovery._destroyMaybe() | |
| 346 | } | |
| 347 | } | |
| 348 | ||
| 349 | function hasNode(nodes, len, node) { | |
| 350 | for (let i = 0; i < len; i++) { | |
| 351 | const existing = nodes[i] | |
| 352 | if (existing.id && b4a.equals(existing.id, node.id)) return true | |
| 353 | } | |
| 354 | ||
| 355 | return false | |
| 356 | } | |
| 357 | ||
| 358 | function noop() {} |
| … | … | @@ -0,0 +1,116 @@ |
| 1 | const { EventEmitter } = require('events') | |
| 2 | const b4a = require('b4a') | |
| 3 | const unslab = require('unslab') | |
| 4 | ||
| 5 | const MIN_CONNECTION_TIME = 15000 | |
| 6 | ||
| 7 | const VERY_LOW_PRIORITY = 0 | |
| 8 | const LOW_PRIORITY = 1 | |
| 9 | const NORMAL_PRIORITY = 2 | |
| 10 | const HIGH_PRIORITY = 3 | |
| 11 | const VERY_HIGH_PRIORITY = 4 | |
| 12 | ||
| 13 | module.exports = class PeerInfo extends EventEmitter { | |
| 14 | constructor({ publicKey, relayAddresses }) { | |
| 15 | super() | |
| 16 | ||
| 17 | this.publicKey = unslab(publicKey) | |
| 18 | this.relayAddresses = relayAddresses | |
| 19 | ||
| 20 | this.reconnecting = true | |
| 21 | this.proven = false | |
| 22 | this.connectedTime = -1 | |
| 23 | this.disconnectedTime = 0 | |
| 24 | this.banned = false | |
| 25 | this.tried = false | |
| 26 | this.explicit = false | |
| 27 | this.waiting = false | |
| 28 | this.forceRelaying = false | |
| 29 | ||
| 30 | // Set by the Swarm | |
| 31 | this.queued = false | |
| 32 | this.client = false | |
| 33 | this.topics = [] // TODO: remove on next major (check with mafintosh for context) | |
| 34 | ||
| 35 | this.attempts = 0 | |
| 36 | this.priority = NORMAL_PRIORITY | |
| 37 | ||
| 38 | // Used by shuffled-priority-queue | |
| 39 | this._index = 0 | |
| 40 | ||
| 41 | // Used for flush management | |
| 42 | this._flushTick = 0 | |
| 43 | ||
| 44 | // Used for topic multiplexing | |
| 45 | this._seenTopics = new Set() | |
| 46 | } | |
| 47 | ||
| 48 | get server() { | |
| 49 | return !this.client | |
| 50 | } | |
| 51 | ||
| 52 | get prioritized() { | |
| 53 | return this.priority >= NORMAL_PRIORITY | |
| 54 | } | |
| 55 | ||
| 56 | _getPriority() { | |
| 57 | const peerIsStale = this.tried && !this.proven | |
| 58 | if (peerIsStale || this.attempts > 3) return VERY_LOW_PRIORITY | |
| 59 | if (this.attempts === 3) return LOW_PRIORITY | |
| 60 | if (this.attempts === 2) return HIGH_PRIORITY | |
| 61 | if (this.attempts === 1) return VERY_HIGH_PRIORITY | |
| 62 | return NORMAL_PRIORITY | |
| 63 | } | |
| 64 | ||
| 65 | _connected() { | |
| 66 | this.proven = true | |
| 67 | this.connectedTime = Date.now() | |
| 68 | } | |
| 69 | ||
| 70 | _disconnected() { | |
| 71 | this.disconnectedTime = Date.now() | |
| 72 | if (this.connectedTime > -1) { | |
| 73 | if (this.disconnectedTime - this.connectedTime >= MIN_CONNECTION_TIME) this.attempts = 0 // fast retry | |
| 74 | this.connectedTime = -1 | |
| 75 | } | |
| 76 | this.attempts++ | |
| 77 | } | |
| 78 | ||
| 79 | _deprioritize() { | |
| 80 | this.attempts = 3 | |
| 81 | } | |
| 82 | ||
| 83 | _reset() { | |
| 84 | this.client = false | |
| 85 | this.proven = false | |
| 86 | this.tried = false | |
| 87 | this.attempts = 0 | |
| 88 | } | |
| 89 | ||
| 90 | _updatePriority() { | |
| 91 | if (this.explicit && this.attempts > 3) this._deprioritize() | |
| 92 | if (this.banned || this.queued || this.attempts > 3) return false | |
| 93 | this.priority = this._getPriority() | |
| 94 | return true | |
| 95 | } | |
| 96 | ||
| 97 | _topic(topic) { | |
| 98 | const topicString = b4a.toString(topic, 'hex') | |
| 99 | if (this._seenTopics.has(topicString)) return | |
| 100 | this._seenTopics.add(topicString) | |
| 101 | this.topics.push(topic) | |
| 102 | this.emit('topic', topic) | |
| 103 | } | |
| 104 | ||
| 105 | reconnect(val) { | |
| 106 | this.reconnecting = !!val | |
| 107 | } | |
| 108 | ||
| 109 | ban(val) { | |
| 110 | this.banned = !!val | |
| 111 | } | |
| 112 | ||
| 113 | shouldGC() { | |
| 114 | return !(this.banned || this.queued || this.explicit || this.waiting) | |
| 115 | } | |
| 116 | } |
| … | … | @@ -0,0 +1,68 @@ |
| 1 | const BulkTimer = require('./bulk-timer') | |
| 2 | ||
| 3 | const BACKOFF_JITTER = 500 | |
| 4 | const BACKOFF_S = 1000 + Math.round(BACKOFF_JITTER * Math.random()) | |
| 5 | const BACKOFF_M = 5000 + Math.round(2 * BACKOFF_JITTER * Math.random()) | |
| 6 | const BACKOFF_L = 15000 + Math.round(4 * BACKOFF_JITTER * Math.random()) | |
| 7 | const BACKOFF_X = 1000 * 60 * 10 + Math.round(240 * BACKOFF_JITTER * Math.random()) | |
| 8 | ||
| 9 | module.exports = class RetryTimer { | |
| 10 | constructor( | |
| 11 | push, | |
| 12 | { backoffs = [BACKOFF_S, BACKOFF_M, BACKOFF_L, BACKOFF_X], jitter = BACKOFF_JITTER } = {} | |
| 13 | ) { | |
| 14 | this.jitter = jitter | |
| 15 | this.backoffs = backoffs | |
| 16 | ||
| 17 | this._sTimer = new BulkTimer(backoffs[0] + Math.round(jitter * Math.random()), push) | |
| 18 | this._mTimer = new BulkTimer(backoffs[1] + Math.round(jitter * Math.random()), push) | |
| 19 | this._lTimer = new BulkTimer(backoffs[2] + Math.round(jitter * Math.random()), push) | |
| 20 | this._xTimer = new BulkTimer(backoffs[3] + Math.round(jitter * Math.random()), push) | |
| 21 | } | |
| 22 | ||
| 23 | _selectRetryTimer(peerInfo) { | |
| 24 | if (peerInfo.banned || !peerInfo.reconnecting) return null | |
| 25 | ||
| 26 | if (peerInfo.attempts > 3) { | |
| 27 | return peerInfo.explicit ? this._xTimer : null | |
| 28 | } | |
| 29 | ||
| 30 | if (peerInfo.attempts === 0) return this._sTimer | |
| 31 | if (peerInfo.proven) { | |
| 32 | switch (peerInfo.attempts) { | |
| 33 | case 1: | |
| 34 | return this._sTimer | |
| 35 | case 2: | |
| 36 | return this._mTimer | |
| 37 | case 3: | |
| 38 | return this._lTimer | |
| 39 | } | |
| 40 | } else { | |
| 41 | switch (peerInfo.attempts) { | |
| 42 | case 1: | |
| 43 | return this._mTimer | |
| 44 | case 2: | |
| 45 | return this._lTimer | |
| 46 | case 3: | |
| 47 | return this._lTimer | |
| 48 | } | |
| 49 | } | |
| 50 | ||
| 51 | return null | |
| 52 | } | |
| 53 | ||
| 54 | add(peerInfo) { | |
| 55 | const timer = this._selectRetryTimer(peerInfo) | |
| 56 | if (!timer) return false | |
| 57 | ||
| 58 | timer.add(peerInfo) | |
| 59 | return true | |
| 60 | } | |
| 61 | ||
| 62 | destroy() { | |
| 63 | this._sTimer.destroy() | |
| 64 | this._mTimer.destroy() | |
| 65 | this._lTimer.destroy() | |
| 66 | this._xTimer.destroy() | |
| 67 | } | |
| 68 | } |
| … | … | @@ -0,0 +1,59 @@ |
| 1 | { | |
| 2 | "name": "hyperswarm", | |
| 3 | "version": "4.17.0", | |
| 4 | "description": "A distributed networking stack for connecting peers", | |
| 5 | "files": [ | |
| 6 | "index.js", | |
| 7 | "lib/**.js" | |
| 8 | ], | |
| 9 | "imports": { | |
| 10 | "events": { | |
| 11 | "bare": "bare-events", | |
| 12 | "default": "events" | |
| 13 | }, | |
| 14 | "process": { | |
| 15 | "bare": "bare-process", | |
| 16 | "default": "process" | |
| 17 | } | |
| 18 | }, | |
| 19 | "dependencies": { | |
| 20 | "b4a": "^1.3.1", | |
| 21 | "bare-events": "^2.2.0", | |
| 22 | "hyperdht": "^6.21.0", | |
| 23 | "safety-catch": "^1.0.2", | |
| 24 | "shuffled-priority-queue": "^2.1.0", | |
| 25 | "streamx": "^2.22.1", | |
| 26 | "unslab": "^1.3.0" | |
| 27 | }, | |
| 28 | "devDependencies": { | |
| 29 | "bare-process": "^4.2.2", | |
| 30 | "brittle": "^3.0.2", | |
| 31 | "hypercore-crypto": "^3.4.0", | |
| 32 | "lunte": "^1.3.0", | |
| 33 | "prettier": "^3.6.2", | |
| 34 | "prettier-config-holepunch": "^2.0.0", | |
| 35 | "which-runtime": "^1.3.2" | |
| 36 | }, | |
| 37 | "scripts": { | |
| 38 | "format": "prettier --write .", | |
| 39 | "lint": "prettier --check . && lunte", | |
| 40 | "test": "npm run test:node && npm run test:bare", | |
| 41 | "test:generate": "brittle -r test/all.js test/*.js", | |
| 42 | "test:node": "brittle-node --coverage test/*.js", | |
| 43 | "test:bare": "bare test/all.js" | |
| 44 | }, | |
| 45 | "repository": { | |
| 46 | "type": "git", | |
| 47 | "url": "https://github.com/holepunchto/hyperswarm.git" | |
| 48 | }, | |
| 49 | "author": "Mathias Buus (@mafintosh)", | |
| 50 | "contributors": [ | |
| 51 | "David Mark Clements (@davidmarkclem)", | |
| 52 | "Andrew Osheroff (@andrewosh)" | |
| 53 | ], | |
| 54 | "license": "MIT", | |
| 55 | "bugs": { | |
| 56 | "url": "https://github.com/holepunchto/hyperswarm/issues" | |
| 57 | }, | |
| 58 | "homepage": "https://github.com/holepunchto/hyperswarm" | |
| 59 | } |
| … | … | @@ -0,0 +1,21 @@ |
| 1 | The MIT License (MIT) | |
| 2 | ||
| 3 | Copyright (c) 2019 Mathias Buus | |
| 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 | |
| 13 | all 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 | |
| 21 | THE SOFTWARE. |
| … | … | @@ -0,0 +1,160 @@ |
| 1 | # kademlia-routing-table | |
| 2 | ||
| 3 | XOR distance based routing table used for P2P networks such as a Kademlia DHT. | |
| 4 | ||
| 5 | ``` | |
| 6 | npm install kademlia-routing-table | |
| 7 | ``` | |
| 8 | ||
| 9 | Similar to k-buckets, but implemented using the simplifications described in https://github.com/ethereum/wiki/wiki/Kademlia-Peer-Selection | |
| 10 | ||
| 11 | To understand the concept behind peer routing, DHTs, and the terms used here, | |
| 12 | I recommend reading the [Kademlia DHT paper](https://pdos.csail.mit.edu/~petar/papers/maymounkov-kademlia-lncs.pdf) as well. | |
| 13 | ||
| 14 | ## Usage | |
| 15 | ||
| 16 | ``` js | |
| 17 | const RoutingTable = require('kademlia-routing-table') | |
| 18 | const { randomBytes } = require('crypto') | |
| 19 | ||
| 20 | // Create a new table that stores nodes "close" to the passed in id. | |
| 21 | // The id should be uniformily distributed, ie a hash, random bytes etc. | |
| 22 | const table = new RoutingTable(randomBytes(32)) | |
| 23 | ||
| 24 | // Add a node to the routing table | |
| 25 | table.add({ | |
| 26 | id: randomBytes(32), // this field is required | |
| 27 | // populate with any other data you want to store | |
| 28 | }) | |
| 29 | ||
| 30 | table.on('row', function (row) { | |
| 31 | // A new row has been added to the routing table | |
| 32 | // This row represents row.index similar bits to the table.id | |
| 33 | ||
| 34 | row.on('full', function (node) { | |
| 35 | // The row is full and cannot be split, so node cannot be added. | |
| 36 | // If any of the nodes in the row are "worse", based on | |
| 37 | // some application specific metric then we should remove | |
| 38 | // the worst node from the row and re-add the node. | |
| 39 | }) | |
| 40 | }) | |
| 41 | ||
| 42 | // Get the 20 nodes "closest" to a passed in id | |
| 43 | const closest = table.closest(randomBytes(32), 20) | |
| 44 | ``` | |
| 45 | ||
| 46 | ## API | |
| 47 | ||
| 48 | #### `table = new RoutingTable(id, [options])` | |
| 49 | ||
| 50 | Create a new routing table. | |
| 51 | ||
| 52 | `id` should be a Buffer that is uniformily distributed. `options` include: | |
| 53 | ||
| 54 | ``` js | |
| 55 | { | |
| 56 | k: 20 // The max row size | |
| 57 | } | |
| 58 | ``` | |
| 59 | ||
| 60 | #### `bool = table.add(node)` | |
| 61 | ||
| 62 | Insert a new node. `node.id` must be a Buffer of same length as `table.id`. | |
| 63 | When inserting a node the XOR distance between the node and the table.id is | |
| 64 | calculated and used to figure which table row this node should be inserted into. | |
| 65 | ||
| 66 | Returns `true` if the node could be added to the corresponding row or `false` if not. | |
| 67 | If `false` is returned the onfullrow function is invoked for the corresponding row and node. | |
| 68 | ||
| 69 | #### `node = table.get(id)` | |
| 70 | ||
| 71 | Get a node from the table using its id. Returns `null` if no node has the passed in `id`. | |
| 72 | ||
| 73 | #### `bool = table.has(id)` | |
| 74 | ||
| 75 | Returns `true` if a node exists for the passed in `id` and `false` otherwise. | |
| 76 | ||
| 77 | #### `nodes = table.closest(id, [maxNodes])` | |
| 78 | ||
| 79 | Returns an array of the closest (in XOR distance) nodes to the passed in id. | |
| 80 | ||
| 81 | `id` should be Buffer of same length as `table.id`. Per default at max `k` | |
| 82 | nodes are returned, but this can be configured using the `maxNodes` argument. | |
| 83 | ||
| 84 | This method is normally used in a routing context, i.e. figuring out which nodes | |
| 85 | in a DHT should store a value based on its id. | |
| 86 | ||
| 87 | #### `bool = table.remove(id)` | |
| 88 | ||
| 89 | Remove a node using its id. Returns `true` if a node existed for the id and | |
| 90 | was removed and `false` otherwise. | |
| 91 | ||
| 92 | #### `node = table.random()` | |
| 93 | ||
| 94 | Get a random node from the table. | |
| 95 | ||
| 96 | #### `nodes = table.toArray()` | |
| 97 | ||
| 98 | Returns all nodes from table as an array. If you create a new routing table | |
| 99 | from these nodes it will be identical to the used here. | |
| 100 | ||
| 101 | #### `table.on('row', row)` | |
| 102 | ||
| 103 | Emitted when a new row is added to the routing table. At max, `bitLength(table.id)` | |
| 104 | will exist. | |
| 105 | ||
| 106 | #### `table.rows` | |
| 107 | ||
| 108 | A fixed size array of all rows in the table. Normally you would not need to worry | |
| 109 | about accessing rows directly outside the row event. | |
| 110 | ||
| 111 | ## Row API | |
| 112 | ||
| 113 | For the row passed in the the `onfullrow` function the following API exists. | |
| 114 | ||
| 115 | #### `row.index` | |
| 116 | ||
| 117 | The row index. Represents how many prefix bits are shared between nodes in the row | |
| 118 | and the table id. | |
| 119 | ||
| 120 | #### `row.nodes` | |
| 121 | ||
| 122 | A list of all the nodes in the row, sorted by their `id`. | |
| 123 | ||
| 124 | #### `row.data` | |
| 125 | ||
| 126 | Property set to null initially you can use if you want to store optional data on the row. | |
| 127 | ||
| 128 | #### `bool = row.add(node)` | |
| 129 | ||
| 130 | Same as `table.add` but for a specific row. Only use this to add the `newNode` | |
| 131 | passed in `onfullrow` function. | |
| 132 | ||
| 133 | #### `bool = row.remove(node)` | |
| 134 | ||
| 135 | Same as `table.remove` but for a specific row. Only use this to remove the | |
| 136 | "worst" node from the row when wanting to add the newNode. | |
| 137 | ||
| 138 | #### `row.on('add', node)` | |
| 139 | ||
| 140 | Emitted when a new node is added to this row. | |
| 141 | ||
| 142 | #### `row.on('remove', node)` | |
| 143 | ||
| 144 | Emitted when a node has been removed from this row. | |
| 145 | ||
| 146 | #### `row.on('full', node)` | |
| 147 | ||
| 148 | Emitted when a node wants to be added to this row, but the row is full (stores `k` nodes). | |
| 149 | ||
| 150 | When this happens you should check if any of the nodes already in the row (`row.nodes`) are | |
| 151 | "worse" than the passed node. If that is the case, remove the "worst" one and re-add the node passed in the arguments. | |
| 152 | ||
| 153 | Various algorithms can be implemented to handle full rows, which is why the routing table leaves most of this logic | |
| 154 | up to the user. These kind of algorithms include adding the rejected node to a cache and wait for another node in the | |
| 155 | row to be removed before trying to insert it again, or using an LRU cache to determine which node already in the row | |
| 156 | has been heard from yet. | |
| 157 | ||
| 158 | ## License | |
| 159 | ||
| 160 | MIT |
| … | … | @@ -0,0 +1,202 @@ |
| 1 | const { EventEmitter } = require('events') | |
| 2 | ||
| 3 | module.exports = class RoutingTable extends EventEmitter { | |
| 4 | constructor (id, opts) { | |
| 5 | if (!opts) opts = {} | |
| 6 | ||
| 7 | super() | |
| 8 | ||
| 9 | this.id = id | |
| 10 | this.k = opts.k || 20 | |
| 11 | this.size = 0 | |
| 12 | this.rows = new Array(id.length * 8) | |
| 13 | } | |
| 14 | ||
| 15 | add (node) { | |
| 16 | const i = this._diff(node.id) | |
| 17 | ||
| 18 | let row = this.rows[i] | |
| 19 | ||
| 20 | if (!row) { | |
| 21 | row = this.rows[i] = new Row(this, i) | |
| 22 | this.emit('row', row) | |
| 23 | } | |
| 24 | ||
| 25 | const len = row.nodes.length | |
| 26 | if (!row.add(node, this.k)) return false | |
| 27 | ||
| 28 | this.size += row.nodes.length - len | |
| 29 | return true | |
| 30 | } | |
| 31 | ||
| 32 | remove (id) { | |
| 33 | const i = this._diff(id) | |
| 34 | const row = this.rows[i] | |
| 35 | if (!row) return false | |
| 36 | if (!row.remove(id)) return false | |
| 37 | this.size-- | |
| 38 | return true | |
| 39 | } | |
| 40 | ||
| 41 | get (id) { | |
| 42 | const i = this._diff(id) | |
| 43 | const row = this.rows[i] | |
| 44 | if (!row) return null | |
| 45 | return row.get(id) | |
| 46 | } | |
| 47 | ||
| 48 | has (id) { | |
| 49 | return this.get(id) !== null | |
| 50 | } | |
| 51 | ||
| 52 | random () { | |
| 53 | let n = (Math.random() * this.size) | 0 | |
| 54 | ||
| 55 | for (let i = 0; i < this.rows.length; i++) { | |
| 56 | const r = this.rows[i] | |
| 57 | if (!r) continue | |
| 58 | if (n < r.nodes.length) return r.nodes[n] | |
| 59 | n -= r.nodes.length | |
| 60 | } | |
| 61 | ||
| 62 | return null | |
| 63 | } | |
| 64 | ||
| 65 | closest (id, k) { | |
| 66 | if (!k) k = this.k | |
| 67 | ||
| 68 | const result = [] | |
| 69 | const d = this._diff(id) | |
| 70 | ||
| 71 | // push close nodes | |
| 72 | for (let i = d; i >= 0 && result.length < k; i--) this._pushNodes(i, k, result) | |
| 73 | ||
| 74 | // if we don't have enough close nodes, populate from other rows, re the paper | |
| 75 | for (let i = d + 1; i < this.rows.length && result.length < k; i++) this._pushNodes(i, k, result) | |
| 76 | ||
| 77 | return result | |
| 78 | } | |
| 79 | ||
| 80 | _pushNodes (i, k, result) { | |
| 81 | const row = this.rows[i] | |
| 82 | if (!row) return | |
| 83 | ||
| 84 | const missing = Math.min(k - result.length, row.nodes.length) | |
| 85 | for (let j = 0; j < missing; j++) result.push(row.nodes[j]) | |
| 86 | } | |
| 87 | ||
| 88 | toArray () { | |
| 89 | return this.closest(this.id, Infinity) | |
| 90 | } | |
| 91 | ||
| 92 | _diff (id) { | |
| 93 | for (let i = 0; i < id.length; i++) { | |
| 94 | const a = id[i] | |
| 95 | const b = this.id[i] | |
| 96 | ||
| 97 | if (a !== b) return i * 8 + Math.clz32(a ^ b) - 24 | |
| 98 | } | |
| 99 | ||
| 100 | return this.rows.length - 1 | |
| 101 | } | |
| 102 | } | |
| 103 | ||
| 104 | class Row extends EventEmitter { | |
| 105 | constructor (table, index) { | |
| 106 | super() | |
| 107 | ||
| 108 | this.data = null // can be used be upstream for whatevs | |
| 109 | this.byteOffset = index >> 3 | |
| 110 | this.index = index | |
| 111 | this.table = table | |
| 112 | this.nodes = [] | |
| 113 | } | |
| 114 | ||
| 115 | add (node) { | |
| 116 | const id = node.id | |
| 117 | ||
| 118 | let l = 0 | |
| 119 | let r = this.nodes.length - 1 | |
| 120 | ||
| 121 | while (l <= r) { | |
| 122 | const m = (l + r) >> 1 | |
| 123 | const c = this.compare(id, this.nodes[m].id) | |
| 124 | ||
| 125 | if (c === 0) { | |
| 126 | this.nodes[m] = node | |
| 127 | return true | |
| 128 | } | |
| 129 | ||
| 130 | if (c < 0) r = m - 1 | |
| 131 | else l = m + 1 | |
| 132 | } | |
| 133 | ||
| 134 | if (this.nodes.length >= this.table.k) { | |
| 135 | this.emit('full', node) | |
| 136 | return false | |
| 137 | } | |
| 138 | ||
| 139 | this.insert(l, node) | |
| 140 | return true | |
| 141 | } | |
| 142 | ||
| 143 | remove (id) { | |
| 144 | let l = 0 | |
| 145 | let r = this.nodes.length - 1 | |
| 146 | ||
| 147 | while (l <= r) { | |
| 148 | const m = (l + r) >> 1 | |
| 149 | const c = this.compare(id, this.nodes[m].id) | |
| 150 | ||
| 151 | if (c === 0) { | |
| 152 | this.splice(m) | |
| 153 | return true | |
| 154 | } | |
| 155 | ||
| 156 | if (c < 0) r = m - 1 | |
| 157 | else l = m + 1 | |
| 158 | } | |
| 159 | ||
| 160 | return false | |
| 161 | } | |
| 162 | ||
| 163 | get (id) { | |
| 164 | let l = 0 | |
| 165 | let r = this.nodes.length - 1 | |
| 166 | ||
| 167 | while (l <= r) { | |
| 168 | const m = (l + r) >> 1 | |
| 169 | const node = this.nodes[m] | |
| 170 | const c = this.compare(id, node.id) | |
| 171 | ||
| 172 | if (c === 0) return node | |
| 173 | if (c < 0) r = m - 1 | |
| 174 | else l = m + 1 | |
| 175 | } | |
| 176 | ||
| 177 | return null | |
| 178 | } | |
| 179 | ||
| 180 | insert (i, node) { | |
| 181 | this.nodes.push(node) // push node or null or whatevs, just trying to not be polymorphic | |
| 182 | for (let j = this.nodes.length - 1; j > i; j--) this.nodes[j] = this.nodes[j - 1] | |
| 183 | this.nodes[i] = node | |
| 184 | this.emit('add', node) | |
| 185 | } | |
| 186 | ||
| 187 | splice (i) { | |
| 188 | for (; i < this.nodes.length - 1; i++) this.nodes[i] = this.nodes[i + 1] | |
| 189 | this.emit('remove', this.nodes.pop()) | |
| 190 | } | |
| 191 | ||
| 192 | // very likely they diverge after a couple of bytes so a simple impl, like this is prop fastest vs Buffer.compare | |
| 193 | compare (a, b) { | |
| 194 | for (let i = this.byteOffset; i < a.length; i++) { | |
| 195 | const ai = a[i] | |
| 196 | const bi = b[i] | |
| 197 | if (ai === bi) continue | |
| 198 | return ai < bi ? -1 : 1 | |
| 199 | } | |
| 200 | return 0 | |
| 201 | } | |
| 202 | } |
| … | … | @@ -0,0 +1,45 @@ |
| 1 | { | |
| 2 | "name": "kademlia-routing-table", | |
| 3 | "version": "1.0.6", | |
| 4 | "description": "XOR based routing table used for P2P networks such as a Kademlia DHT.", | |
| 5 | "main": "index.js", | |
| 6 | "files": [ | |
| 7 | "index.js" | |
| 8 | ], | |
| 9 | "imports": { | |
| 10 | "events": { | |
| 11 | "bare": "bare-events", | |
| 12 | "default": "events" | |
| 13 | } | |
| 14 | }, | |
| 15 | "dependencies": { | |
| 16 | "bare-events": "^2.2.0" | |
| 17 | }, | |
| 18 | "devDependencies": { | |
| 19 | "brittle": "^3.3.2", | |
| 20 | "standard": "^17.1.0" | |
| 21 | }, | |
| 22 | "scripts": { | |
| 23 | "test": "standard && brittle test.js" | |
| 24 | }, | |
| 25 | "keywords": [ | |
| 26 | "kademlia", | |
| 27 | "p2p", | |
| 28 | "k-bucket", | |
| 29 | "k-buckets", | |
| 30 | "xor", | |
| 31 | "routing", | |
| 32 | "distributed", | |
| 33 | "systems" | |
| 34 | ], | |
| 35 | "repository": { | |
| 36 | "type": "git", | |
| 37 | "url": "https://github.com/mafintosh/kademlia-routing-table.git" | |
| 38 | }, | |
| 39 | "author": "Mathias Buus (@mafintosh)", | |
| 40 | "license": "MIT", | |
| 41 | "bugs": { | |
| 42 | "url": "https://github.com/mafintosh/kademlia-routing-table/issues" | |
| 43 | }, | |
| 44 | "homepage": "https://github.com/mafintosh/kademlia-routing-table" | |
| 45 | } |
| … | … | @@ -0,0 +1,24 @@ |
| 1 | name: Build Status | |
| 2 | ||
| 3 | on: | |
| 4 | push: | |
| 5 | branches: | |
| 6 | - master | |
| 7 | pull_request: | |
| 8 | branches: | |
| 9 | - master | |
| 10 | jobs: | |
| 11 | build: | |
| 12 | strategy: | |
| 13 | matrix: | |
| 14 | node-version: [14.x] | |
| 15 | os: [ubuntu-16.04, macos-latest, windows-latest] | |
| 16 | runs-on: ${{ matrix.os }} | |
| 17 | steps: | |
| 18 | - uses: actions/checkout@v2 | |
| 19 | - name: Use Node.js ${{ matrix.node-version }} | |
| 20 | uses: actions/setup-node@v1 | |
| 21 | with: | |
| 22 | node-version: ${{ matrix.node-version }} | |
| 23 | - run: npm install | |
| 24 | - run: npm test |
| … | … | @@ -0,0 +1,21 @@ |
| 1 | The MIT License (MIT) | |
| 2 | ||
| 3 | Copyright (c) 2021 Mathias Buus | |
| 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 | |
| 13 | all 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 | |
| 21 | THE SOFTWARE. |
| … | … | @@ -0,0 +1,63 @@ |
| 1 | # nat-sampler | |
| 2 | ||
| 3 | Sample addresses to figure out if a host + port is consistent | |
| 4 | ||
| 5 | ``` | |
| 6 | npm install nat-sampler | |
| 7 | ``` | |
| 8 | ||
| 9 | ## Usage | |
| 10 | ||
| 11 | ``` js | |
| 12 | const NatSampler = require('nat-sampler') | |
| 13 | ||
| 14 | const s = new NatSampler() | |
| 15 | ||
| 16 | // add a sample | |
| 17 | const hits = s.add('127.0.0.1', 9090) | |
| 18 | ||
| 19 | console.log('In there this many times:', hits) | |
| 20 | console.log('Estimated host + port', s.host, s.port) | |
| 21 | ``` | |
| 22 | ||
| 23 | ## API | |
| 24 | ||
| 25 | #### `s = new NatSampler()` | |
| 26 | ||
| 27 | Make a new sampler. | |
| 28 | ||
| 29 | #### `hits = s.add(host, port)` | |
| 30 | ||
| 31 | Add a sample of your host and port. | |
| 32 | Returns how many hits this entry has. | |
| 33 | ||
| 34 | #### `s.host` | |
| 35 | ||
| 36 | What the sampler thinks your host is. | |
| 37 | ||
| 38 | If your host is unknown it will be `null`. | |
| 39 | ||
| 40 | #### `s.port` | |
| 41 | ||
| 42 | What the sampler thinks your port is. | |
| 43 | ||
| 44 | If your port is random or unknown it will be `0`. | |
| 45 | ||
| 46 | #### `s.size` | |
| 47 | ||
| 48 | How many samples the sampler is basing this on. | |
| 49 | ||
| 50 | ## Error correction | |
| 51 | ||
| 52 | The sampler applies some simple error correction to make sure bad samples do not mess it up. | |
| 53 | ||
| 54 | * If it has `<=` 3 samples, it will disregard none outliers. | |
| 55 | * If it has `<=` 7 samples, it will disregard one outliers. | |
| 56 | * If it has `<=` 11 samples, it will disregard two outliers. | |
| 57 | * If it has `>` 11 samples, it will disregard three outliers. | |
| 58 | ||
| 59 | At max it keeps 16 samples of `{ host, port }` pairs for the analysis. | |
| 60 | ||
| 61 | ## License | |
| 62 | ||
| 63 | MIT |
| … | … | @@ -0,0 +1,64 @@ |
| 1 | module.exports = class NatSampler { | |
| 2 | constructor () { | |
| 3 | this.host = null | |
| 4 | this.port = 0 | |
| 5 | this.size = 0 | |
| 6 | ||
| 7 | this._a = null | |
| 8 | this._b = null | |
| 9 | this._threshold = 0 | |
| 10 | this._top = 0 | |
| 11 | this._samples = [] | |
| 12 | } | |
| 13 | ||
| 14 | add (host, port) { | |
| 15 | const a = this._bump(host, port, 2) | |
| 16 | const b = this._bump(host, 0, 1) | |
| 17 | ||
| 18 | if (this._samples.length < 32) { | |
| 19 | this.size++ | |
| 20 | this._threshold = this.size - (this.size < 4 ? 0 : this.size < 8 ? 1 : this.size < 12 ? 2 : 3) | |
| 21 | this._samples.push(a, b) | |
| 22 | this._top += 2 | |
| 23 | } else { | |
| 24 | if (this._top === 32) this._top = 0 | |
| 25 | ||
| 26 | const oa = this._samples[this._top] | |
| 27 | this._samples[this._top++] = a | |
| 28 | oa.hits-- | |
| 29 | ||
| 30 | const ob = this._samples[this._top] | |
| 31 | this._samples[this._top++] = b | |
| 32 | ob.hits-- | |
| 33 | } | |
| 34 | ||
| 35 | if (this._a === null || this._a.hits < a.hits) this._a = a | |
| 36 | if (this._b === null || this._b.hits < b.hits) this._b = b | |
| 37 | ||
| 38 | if (this._a.hits >= this._threshold) { | |
| 39 | this.host = this._a.host | |
| 40 | this.port = this._a.port | |
| 41 | } else if (this._b.hits >= this._threshold) { | |
| 42 | this.host = this._b.host | |
| 43 | this.port = 0 | |
| 44 | } else { | |
| 45 | this.host = null | |
| 46 | this.port = 0 | |
| 47 | } | |
| 48 | ||
| 49 | return a.hits | |
| 50 | } | |
| 51 | ||
| 52 | _bump (host, port, inc) { | |
| 53 | for (let i = 0; i < 4; i++) { | |
| 54 | const j = (this._top - inc - (2 * i)) & 31 | |
| 55 | if (j >= this._samples.length) return { host, port, hits: 1 } | |
| 56 | const s = this._samples[j] | |
| 57 | if (s.port === port && s.host === host) { | |
| 58 | s.hits++ | |
| 59 | return s | |
| 60 | } | |
| 61 | } | |
| 62 | return { host, port, hits: 1 } | |
| 63 | } | |
| 64 | } |
| … | … | @@ -0,0 +1,24 @@ |
| 1 | { | |
| 2 | "name": "nat-sampler", | |
| 3 | "version": "1.0.1", | |
| 4 | "description": "Sample addresses to figure out if a host + port is consistent", | |
| 5 | "main": "index.js", | |
| 6 | "dependencies": {}, | |
| 7 | "devDependencies": { | |
| 8 | "standard": "^16.0.3", | |
| 9 | "tape": "^5.2.2" | |
| 10 | }, | |
| 11 | "repository": { | |
| 12 | "type": "git", | |
| 13 | "url": "https://github.com/mafintosh/nat-sampler.git" | |
| 14 | }, | |
| 15 | "scripts": { | |
| 16 | "test": "standard && tape test.js" | |
| 17 | }, | |
| 18 | "author": "Mathias Buus (@mafintosh)", | |
| 19 | "license": "MIT", | |
| 20 | "bugs": { | |
| 21 | "url": "https://github.com/mafintosh/nat-sampler/issues" | |
| 22 | }, | |
| 23 | "homepage": "https://github.com/mafintosh/nat-sampler" | |
| 24 | } |
| … | … | @@ -0,0 +1,106 @@ |
| 1 | const tape = require('tape') | |
| 2 | const NatSampler = require('./') | |
| 3 | ||
| 4 | tape('small consistent samples', function (t) { | |
| 5 | const nat = new NatSampler() | |
| 6 | ||
| 7 | t.same(nat.add('127.0.0.1', 9090), 1) | |
| 8 | t.same(nat.host, '127.0.0.1') | |
| 9 | t.same(nat.port, 9090) | |
| 10 | ||
| 11 | t.same(nat.add('127.0.0.1', 9090), 2) | |
| 12 | t.same(nat.host, '127.0.0.1') | |
| 13 | t.same(nat.port, 9090) | |
| 14 | ||
| 15 | t.same(nat.add('127.0.0.1', 9090), 3) | |
| 16 | t.same(nat.host, '127.0.0.1') | |
| 17 | t.same(nat.port, 9090) | |
| 18 | ||
| 19 | t.end() | |
| 20 | }) | |
| 21 | ||
| 22 | tape('small consistent samples with errors', function (t) { | |
| 23 | const nat = new NatSampler() | |
| 24 | ||
| 25 | t.same(nat.add('127.0.0.1', 9090), 1) | |
| 26 | t.same(nat.host, '127.0.0.1') | |
| 27 | t.same(nat.port, 9090) | |
| 28 | ||
| 29 | t.same(nat.add('127.0.0.1', 9091), 1) | |
| 30 | t.same(nat.host, '127.0.0.1') | |
| 31 | t.same(nat.port, 0) | |
| 32 | ||
| 33 | t.same(nat.add('127.0.0.1', 9090), 2) | |
| 34 | t.same(nat.host, '127.0.0.1') | |
| 35 | t.same(nat.port, 0) | |
| 36 | ||
| 37 | t.same(nat.add('127.0.0.1', 9090), 3) | |
| 38 | t.same(nat.host, '127.0.0.1') | |
| 39 | t.same(nat.port, 9090) | |
| 40 | ||
| 41 | t.same(nat.add('127.0.0.1', 9091), 2) | |
| 42 | t.same(nat.host, '127.0.0.1') | |
| 43 | t.same(nat.port, 0) | |
| 44 | ||
| 45 | t.end() | |
| 46 | }) | |
| 47 | ||
| 48 | tape('host consistency', function (t) { | |
| 49 | const nat = new NatSampler() | |
| 50 | ||
| 51 | t.same(nat.add('127.0.0.1', 9090), 1) | |
| 52 | t.same(nat.add('127.0.0.2', 9090), 1) | |
| 53 | t.same(nat.add('127.0.0.1', 9090), 2) | |
| 54 | t.same(nat.add('127.0.0.2', 9090), 2) | |
| 55 | ||
| 56 | t.same(nat.host, null) | |
| 57 | t.same(nat.port, 0) | |
| 58 | ||
| 59 | t.same(nat.add('127.0.0.1', 9090), 3) | |
| 60 | t.same(nat.host, null) | |
| 61 | t.same(nat.port, 0) | |
| 62 | ||
| 63 | t.same(nat.add('127.0.0.1', 9090), 4) | |
| 64 | t.same(nat.host, null) | |
| 65 | t.same(nat.port, 0) | |
| 66 | ||
| 67 | t.same(nat.add('127.0.0.1', 9090), 5) | |
| 68 | t.same(nat.host, null) | |
| 69 | t.same(nat.port, 0) | |
| 70 | ||
| 71 | t.same(nat.add('127.0.0.1', 9091), 1) | |
| 72 | t.same(nat.host, '127.0.0.1') | |
| 73 | t.same(nat.port, 0) | |
| 74 | ||
| 75 | t.end() | |
| 76 | }) | |
| 77 | ||
| 78 | tape('can recover up to 3 errors', function (t) { | |
| 79 | const nat = new NatSampler() | |
| 80 | ||
| 81 | for (let i = 0; i < 20; i++) nat.add('127.0.0.1', 9090) | |
| 82 | ||
| 83 | t.same(nat.add('127.0.0.1', 9091), 1) | |
| 84 | t.same(nat.add('127.0.0.2', 9091), 1) | |
| 85 | t.same(nat.add('127.0.0.3', 9091), 1) | |
| 86 | t.same(nat.host, '127.0.0.1') | |
| 87 | t.same(nat.port, 9090) | |
| 88 | ||
| 89 | for (let i = 0; i < 5; i++) nat.add('127.0.0.1', 9090) | |
| 90 | t.same(nat.host, '127.0.0.1') | |
| 91 | t.same(nat.port, 9090) | |
| 92 | ||
| 93 | nat.add('127.0.0.1', 9095) | |
| 94 | t.same(nat.host, '127.0.0.1') | |
| 95 | t.same(nat.port, 0) | |
| 96 | ||
| 97 | nat.add('127.0.0.2', 9095) | |
| 98 | t.same(nat.host, '127.0.0.1') | |
| 99 | t.same(nat.port, 0) | |
| 100 | ||
| 101 | nat.add('127.0.0.2', 9095) | |
| 102 | t.same(nat.host, null) | |
| 103 | t.same(nat.port, 0) | |
| 104 | ||
| 105 | t.end() | |
| 106 | }) |
| … | … | @@ -0,0 +1,21 @@ |
| 1 | The MIT License (MIT) | |
| 2 | ||
| 3 | Copyright (c) 2017-present, Jon Schlinkert. | |
| 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 | |
| 13 | all 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 | |
| 21 | THE SOFTWARE. |
| … | … | @@ -0,0 +1,749 @@ |
| 1 | <h1 align="center">Picomatch</h1> | |
| 2 | ||
| 3 | <p align="center"> | |
| 4 | <a href="https://npmjs.org/package/picomatch"> | |
| 5 | <img src="https://img.shields.io/npm/v/picomatch.svg" alt="version"> | |
| 6 | </a> | |
| 7 | <a href="https://github.com/micromatch/picomatch/actions?workflow=Tests"> | |
| 8 | <img src="https://github.com/micromatch/picomatch/workflows/Tests/badge.svg" alt="test status"> | |
| 9 | </a> | |
| 10 | <a href="https://coveralls.io/github/micromatch/picomatch"> | |
| 11 | <img src="https://img.shields.io/coveralls/github/micromatch/picomatch/master.svg" alt="coverage status"> | |
| 12 | </a> | |
| 13 | <a href="https://npmjs.org/package/picomatch"> | |
| 14 | <img src="https://img.shields.io/npm/dm/picomatch.svg" alt="downloads"> | |
| 15 | </a> | |
| 16 | </p> | |
| 17 | ||
| 18 | <br> | |
| 19 | <br> | |
| 20 | ||
| 21 | <p align="center"> | |
| 22 | <strong>Blazing fast and accurate glob matcher written in JavaScript.</strong></br> | |
| 23 | <em>No dependencies and full support for standard and extended Bash glob features, including braces, extglobs, POSIX brackets, and regular expressions.</em> | |
| 24 | </p> | |
| 25 | ||
| 26 | <br> | |
| 27 | <br> | |
| 28 | ||
| 29 | ## Why picomatch? | |
| 30 | ||
| 31 | * **Lightweight** - No dependencies | |
| 32 | * **Minimal** - Tiny API surface. Main export is a function that takes a glob pattern and returns a matcher function. | |
| 33 | * **Fast** - Loads in about 2ms (that's several times faster than a [single frame of a HD movie](http://www.endmemo.com/sconvert/framespersecondframespermillisecond.php) at 60fps) | |
| 34 | * **Performant** - Use the returned matcher function to speed up repeat matching (like when watching files) | |
| 35 | * **Accurate matching** - Using wildcards (`*` and `?`), globstars (`**`) for nested directories, [advanced globbing](#advanced-globbing) with extglobs, braces, and POSIX brackets, and support for escaping special characters with `\` or quotes. | |
| 36 | * **Well tested** - Thousands of unit tests | |
| 37 | ||
| 38 | See the [library comparison](#library-comparisons) to other libraries. | |
| 39 | ||
| 40 | <br> | |
| 41 | <br> | |
| 42 | ||
| 43 | ## Table of Contents | |
| 44 | ||
| 45 | <details><summary> Click to expand </summary> | |
| 46 | ||
| 47 | - [Install](#install) | |
| 48 | - [Usage](#usage) | |
| 49 | - [API](#api) | |
| 50 | * [picomatch](#picomatch) | |
| 51 | * [.test](#test) | |
| 52 | * [.matchBase](#matchbase) | |
| 53 | * [.isMatch](#ismatch) | |
| 54 | * [.parse](#parse) | |
| 55 | * [.scan](#scan) | |
| 56 | * [.compileRe](#compilere) | |
| 57 | * [.makeRe](#makere) | |
| 58 | * [.toRegex](#toregex) | |
| 59 | - [Options](#options) | |
| 60 | * [Picomatch options](#picomatch-options) | |
| 61 | * [Scan Options](#scan-options) | |
| 62 | * [Options Examples](#options-examples) | |
| 63 | - [Globbing features](#globbing-features) | |
| 64 | * [Basic globbing](#basic-globbing) | |
| 65 | * [Advanced globbing](#advanced-globbing) | |
| 66 | * [Braces](#braces) | |
| 67 | * [Matching special characters as literals](#matching-special-characters-as-literals) | |
| 68 | - [Library Comparisons](#library-comparisons) | |
| 69 | - [Benchmarks](#benchmarks) | |
| 70 | - [Philosophies](#philosophies) | |
| 71 | - [About](#about) | |
| 72 | * [Author](#author) | |
| 73 | * [License](#license) | |
| 74 | ||
| 75 | _(TOC generated by [verb](https://github.com/verbose/verb) using [markdown-toc](https://github.com/jonschlinkert/markdown-toc))_ | |
| 76 | ||
| 77 | </details> | |
| 78 | ||
| 79 | <br> | |
| 80 | <br> | |
| 81 | ||
| 82 | ## Install | |
| 83 | ||
| 84 | Install with [npm](https://www.npmjs.com/): | |
| 85 | ||
| 86 | ```sh | |
| 87 | npm install --save picomatch | |
| 88 | ``` | |
| 89 | ||
| 90 | <br> | |
| 91 | ||
| 92 | ## Usage | |
| 93 | ||
| 94 | The main export is a function that takes a glob pattern and an options object and returns a function for matching strings. | |
| 95 | ||
| 96 | ```js | |
| 97 | const pm = require('picomatch'); | |
| 98 | const isMatch = pm('*.js'); | |
| 99 | ||
| 100 | console.log(isMatch('abcd')); //=> false | |
| 101 | console.log(isMatch('a.js')); //=> true | |
| 102 | console.log(isMatch('a.md')); //=> false | |
| 103 | console.log(isMatch('a/b.js')); //=> false | |
| 104 | ``` | |
| 105 | ||
| 106 | <br> | |
| 107 | ||
| 108 | ## API | |
| 109 | ||
| 110 | ### [picomatch](lib/picomatch.js#L31) | |
| 111 | ||
| 112 | Creates a matcher function from one or more glob patterns. The returned function takes a string to match as its first argument, and returns true if the string is a match. The returned matcher function also takes a boolean as the second argument that, when true, returns an object with additional information. | |
| 113 | ||
| 114 | **Params** | |
| 115 | ||
| 116 | * `globs` **{String|Array}**: One or more glob patterns. | |
| 117 | * `options` **{Object=}** | |
| 118 | * `returns` **{Function=}**: Returns a matcher function. | |
| 119 | ||
| 120 | **Example** | |
| 121 | ||
| 122 | ```js | |
| 123 | const picomatch = require('picomatch'); | |
| 124 | // picomatch(glob[, options]); | |
| 125 | ||
| 126 | const isMatch = picomatch('*.!(*a)'); | |
| 127 | console.log(isMatch('a.a')); //=> false | |
| 128 | console.log(isMatch('a.b')); //=> true | |
| 129 | ``` | |
| 130 | ||
| 131 | **Example without node.js** | |
| 132 | ||
| 133 | For environments without `node.js`, `picomatch/posix` provides you a dependency-free matcher, without automatic OS detection. | |
| 134 | ||
| 135 | ```js | |
| 136 | const picomatch = require('picomatch/posix'); | |
| 137 | // the same API, defaulting to posix paths | |
| 138 | const isMatch = picomatch('a/*'); | |
| 139 | console.log(isMatch('a\\b')); //=> false | |
| 140 | console.log(isMatch('a/b')); //=> true | |
| 141 | ||
| 142 | // you can still configure the matcher function to accept windows paths | |
| 143 | const isMatch = picomatch('a/*', { options: windows }); | |
| 144 | console.log(isMatch('a\\b')); //=> true | |
| 145 | console.log(isMatch('a/b')); //=> true | |
| 146 | ``` | |
| 147 | ||
| 148 | ### [.test](lib/picomatch.js#L116) | |
| 149 | ||
| 150 | Test `input` with the given `regex`. This is used by the main `picomatch()` function to test the input string. | |
| 151 | ||
| 152 | **Params** | |
| 153 | ||
| 154 | * `input` **{String}**: String to test. | |
| 155 | * `regex` **{RegExp}** | |
| 156 | * `returns` **{Object}**: Returns an object with matching info. | |
| 157 | ||
| 158 | **Example** | |
| 159 | ||
| 160 | ```js | |
| 161 | const picomatch = require('picomatch'); | |
| 162 | // picomatch.test(input, regex[, options]); | |
| 163 | ||
| 164 | console.log(picomatch.test('foo/bar', /^(?:([^/]*?)\/([^/]*?))$/)); | |
| 165 | // { isMatch: true, match: [ 'foo/', 'foo', 'bar' ], output: 'foo/bar' } | |
| 166 | ``` | |
| 167 | ||
| 168 | ### [.matchBase](lib/picomatch.js#L160) | |
| 169 | ||
| 170 | Match the basename of a filepath. | |
| 171 | ||
| 172 | **Params** | |
| 173 | ||
| 174 | * `input` **{String}**: String to test. | |
| 175 | * `glob` **{RegExp|String}**: Glob pattern or regex created by [.makeRe](#makeRe). | |
| 176 | * `returns` **{Boolean}** | |
| 177 | ||
| 178 | **Example** | |
| 179 | ||
| 180 | ```js | |
| 181 | const picomatch = require('picomatch'); | |
| 182 | // picomatch.matchBase(input, glob[, options]); | |
| 183 | console.log(picomatch.matchBase('foo/bar.js', '*.js'); // true | |
| 184 | ``` | |
| 185 | ||
| 186 | ### [.isMatch](lib/picomatch.js#L182) | |
| 187 | ||
| 188 | Returns true if **any** of the given glob `patterns` match the specified `string`. | |
| 189 | ||
| 190 | **Params** | |
| 191 | ||
| 192 | * **{String|Array}**: str The string to test. | |
| 193 | * **{String|Array}**: patterns One or more glob patterns to use for matching. | |
| 194 | * **{Object}**: See available [options](#options). | |
| 195 | * `returns` **{Boolean}**: Returns true if any patterns match `str` | |
| 196 | ||
| 197 | **Example** | |
| 198 | ||
| 199 | ```js | |
| 200 | const picomatch = require('picomatch'); | |
| 201 | // picomatch.isMatch(string, patterns[, options]); | |
| 202 | ||
| 203 | console.log(picomatch.isMatch('a.a', ['b.*', '*.a'])); //=> true | |
| 204 | console.log(picomatch.isMatch('a.a', 'b.*')); //=> false | |
| 205 | ``` | |
| 206 | ||
| 207 | ### [.parse](lib/picomatch.js#L198) | |
| 208 | ||
| 209 | Parse a glob pattern to create the source string for a regular expression. | |
| 210 | ||
| 211 | **Params** | |
| 212 | ||
| 213 | * `pattern` **{String}** | |
| 214 | * `options` **{Object}** | |
| 215 | * `returns` **{Object}**: Returns an object with useful properties and output to be used as a regex source string. | |
| 216 | ||
| 217 | **Example** | |
| 218 | ||
| 219 | ```js | |
| 220 | const picomatch = require('picomatch'); | |
| 221 | const result = picomatch.parse(pattern[, options]); | |
| 222 | ``` | |
| 223 | ||
| 224 | ### [.scan](lib/picomatch.js#L230) | |
| 225 | ||
| 226 | Scan a glob pattern to separate the pattern into segments. | |
| 227 | ||
| 228 | **Params** | |
| 229 | ||
| 230 | * `input` **{String}**: Glob pattern to scan. | |
| 231 | * `options` **{Object}** | |
| 232 | * `returns` **{Object}**: Returns an object with | |
| 233 | ||
| 234 | **Example** | |
| 235 | ||
| 236 | ```js | |
| 237 | const picomatch = require('picomatch'); | |
| 238 | // picomatch.scan(input[, options]); | |
| 239 | ||
| 240 | const result = picomatch.scan('!./foo/*.js'); | |
| 241 | console.log(result); | |
| 242 | { prefix: '!./', | |
| 243 | input: '!./foo/*.js', | |
| 244 | start: 3, | |
| 245 | base: 'foo', | |
| 246 | glob: '*.js', | |
| 247 | isBrace: false, | |
| 248 | isBracket: false, | |
| 249 | isGlob: true, | |
| 250 | isExtglob: false, | |
| 251 | isGlobstar: false, | |
| 252 | negated: true } | |
| 253 | ``` | |
| 254 | ||
| 255 | ### [.compileRe](lib/picomatch.js#L244) | |
| 256 | ||
| 257 | Compile a regular expression from the `state` object returned by the | |
| 258 | [parse()](#parse) method. | |
| 259 | ||
| 260 | **Params** | |
| 261 | ||
| 262 | * `state` **{Object}** | |
| 263 | * `options` **{Object}** | |
| 264 | * `returnOutput` **{Boolean}**: Intended for implementors, this argument allows you to return the raw output from the parser. | |
| 265 | * `returnState` **{Boolean}**: Adds the state to a `state` property on the returned regex. Useful for implementors and debugging. | |
| 266 | * `returns` **{RegExp}** | |
| 267 | ||
| 268 | **Example** | |
| 269 | ||
| 270 | ```js | |
| 271 | const picomatch = require('picomatch'); | |
| 272 | const state = picomatch.parse('*.js'); | |
| 273 | // picomatch.compileRe(state[, options]); | |
| 274 | ||
| 275 | console.log(picomatch.compileRe(state)); | |
| 276 | //=> /^(?:(?!\.)(?=.)[^/]*?\.js)$/ | |
| 277 | ``` | |
| 278 | ||
| 279 | ### [.makeRe](lib/picomatch.js#L285) | |
| 280 | ||
| 281 | Create a regular expression from a parsed glob pattern. | |
| 282 | ||
| 283 | **Params** | |
| 284 | ||
| 285 | * `state` **{String}**: The object returned from the `.parse` method. | |
| 286 | * `options` **{Object}** | |
| 287 | * `returnOutput` **{Boolean}**: Implementors may use this argument to return the compiled output, instead of a regular expression. This is not exposed on the options to prevent end-users from mutating the result. | |
| 288 | * `returnState` **{Boolean}**: Implementors may use this argument to return the state from the parsed glob with the returned regular expression. | |
| 289 | * `returns` **{RegExp}**: Returns a regex created from the given pattern. | |
| 290 | ||
| 291 | **Example** | |
| 292 | ||
| 293 | ```js | |
| 294 | const picomatch = require('picomatch'); | |
| 295 | // picomatch.makeRe(state[, options]); | |
| 296 | ||
| 297 | const result = picomatch.makeRe('*.js'); | |
| 298 | console.log(result); | |
| 299 | //=> /^(?:(?!\.)(?=.)[^/]*?\.js)$/ | |
| 300 | ``` | |
| 301 | ||
| 302 | ### [.toRegex](lib/picomatch.js#L320) | |
| 303 | ||
| 304 | Create a regular expression from the given regex source string. | |
| 305 | ||
| 306 | **Params** | |
| 307 | ||
| 308 | * `source` **{String}**: Regular expression source string. | |
| 309 | * `options` **{Object}** | |
| 310 | * `returns` **{RegExp}** | |
| 311 | ||
| 312 | **Example** | |
| 313 | ||
| 314 | ```js | |
| 315 | const picomatch = require('picomatch'); | |
| 316 | // picomatch.toRegex(source[, options]); | |
| 317 | ||
| 318 | const { output } = picomatch.parse('*.js'); | |
| 319 | console.log(picomatch.toRegex(output)); | |
| 320 | //=> /^(?:(?!\.)(?=.)[^/]*?\.js)$/ | |
| 321 | ``` | |
| 322 | ||
| 323 | <br> | |
| 324 | ||
| 325 | ## Options | |
| 326 | ||
| 327 | ### Picomatch options | |
| 328 | ||
| 329 | The following options may be used with the main `picomatch()` function or any of the methods on the picomatch API. | |
| 330 | ||
| 331 | | **Option** | **Type** | **Default value** | **Description** | | |
| 332 | | --- | --- | --- | --- | | |
| 333 | | `basename` | `boolean` | `false` | If set, then patterns without slashes will be matched against the basename of the path if it contains slashes. For example, `a?b` would match the path `/xyz/123/acb`, but not `/xyz/acb/123`. | | |
| 334 | | `bash` | `boolean` | `false` | Follow bash matching rules more strictly - disallows backslashes as escape characters, and treats single stars as globstars (`**`). | | |
| 335 | | `capture` | `boolean` | `undefined` | Return regex matches in supporting methods. | | |
| 336 | | `contains` | `boolean` | `undefined` | Allows glob to match any part of the given string(s). | | |
| 337 | | `debug` | `boolean` | `undefined` | Debug regular expressions when an error is thrown. | | |
| 338 | | `dot` | `boolean` | `false` | Enable dotfile matching. By default, dotfiles are ignored unless a `.` is explicitly defined in the pattern, or `options.dot` is true | | |
| 339 | | `expandRange` | `function` | `undefined` | Custom function for expanding ranges in brace patterns, such as `{a..z}`. The function receives the range values as two arguments, and it must return a string to be used in the generated regex. It's recommended that returned strings be wrapped in parentheses. | | |
| 340 | | `fastpaths` | `boolean` | `true` | To speed up processing, full parsing is skipped for a handful common glob patterns. Disable this behavior by setting this option to `false`. | | |
| 341 | | `flags` | `string` | `undefined` | Regex flags to use in the generated regex. If defined, the `nocase` option will be overridden. | | |
| 342 | | [format](#optionsformat) | `function` | `undefined` | Custom function for formatting the returned string. This is useful for removing leading slashes, converting Windows paths to Posix paths, etc. | | |
| 343 | | `ignore` | `array\|string` | `undefined` | One or more glob patterns for excluding strings that should not be matched from the result. | | |
| 344 | | `keepQuotes` | `boolean` | `false` | Retain quotes in the generated regex, since quotes may also be used as an alternative to backslashes. | | |
| 345 | | `literalBrackets` | `boolean` | `undefined` | When `true`, brackets in the glob pattern will be escaped so that only literal brackets will be matched. | | |
| 346 | | `matchBase` | `boolean` | `false` | Alias for `basename` | | |
| 347 | | `maxLength` | `number` | `65536` | Limit the max length of the input string. An error is thrown if the input string is longer than this value. | | |
| 348 | | `maxExtglobRecursion` | `number\|boolean` | `0` | Limit nested quantified extglobs and other risky repeated extglob forms. When the limit is exceeded, the extglob is treated as a literal string instead of being compiled to regex. Set to `false` to disable this safeguard. | | |
| 349 | | `nobrace` | `boolean` | `false` | Disable brace matching, so that `{a,b}` and `{1..3}` would be treated as literal characters. | | |
| 350 | | `nobracket` | `boolean` | `undefined` | Disable matching with regex brackets. | | |
| 351 | | `nocase` | `boolean` | `false` | Make matching case-insensitive. Equivalent to the regex `i` flag. Note that this option is overridden by the `flags` option. | | |
| 352 | | `noext` | `boolean` | `false` | Alias for `noextglob` | | |
| 353 | | `noextglob` | `boolean` | `false` | Disable support for matching with extglobs (like `+(a\|b)`) | | |
| 354 | | `noglobstar` | `boolean` | `false` | Disable support for matching nested directories with globstars (`**`) | | |
| 355 | | `nonegate` | `boolean` | `false` | Disable support for negating with leading `!` | | |
| 356 | | [onIgnore](#optionsonIgnore) | `function` | `undefined` | Function to be called on ignored items. | | |
| 357 | | [onMatch](#optionsonMatch) | `function` | `undefined` | Function to be called on matched items. | | |
| 358 | | [onResult](#optionsonResult) | `function` | `undefined` | Function to be called on all items, regardless of whether or not they are matched or ignored. | | |
| 359 | | `posix` | `boolean` | `false` | Support POSIX character classes ("posix brackets"). | | |
| 360 | | `prepend` | `boolean` | `undefined` | String to prepend to the generated regex used for matching. | | |
| 361 | | `regex` | `boolean` | `false` | Use regular expression rules for `+` (instead of matching literal `+`), and for stars that follow closing parentheses or brackets (as in `)*` and `]*`). | | |
| 362 | | `strictBrackets` | `boolean` | `undefined` | Throw an error if brackets, braces, or parens are imbalanced. | | |
| 363 | | `strictSlashes` | `boolean` | `undefined` | When true, picomatch won't match trailing slashes with single stars. | | |
| 364 | | `unescape` | `boolean` | `undefined` | Remove backslashes preceding escaped characters in the glob pattern. By default, backslashes are retained. | | |
| 365 | | `windows` | `boolean` | `false` | Also accept backslashes as the path separator. | | |
| 366 | ||
| 367 | ### Scan Options | |
| 368 | ||
| 369 | In addition to the main [picomatch options](#picomatch-options), the following options may also be used with the [.scan](#scan) method. | |
| 370 | ||
| 371 | | **Option** | **Type** | **Default value** | **Description** | | |
| 372 | | --- | --- | --- | --- | | |
| 373 | | `tokens` | `boolean` | `false` | When `true`, the returned object will include an array of tokens (objects), representing each path "segment" in the scanned glob pattern | | |
| 374 | | `parts` | `boolean` | `false` | When `true`, the returned object will include an array of strings representing each path "segment" in the scanned glob pattern. This is automatically enabled when `options.tokens` is true | | |
| 375 | ||
| 376 | **Example** | |
| 377 | ||
| 378 | ```js | |
| 379 | const picomatch = require('picomatch'); | |
| 380 | const result = picomatch.scan('!./foo/*.js', { tokens: true }); | |
| 381 | console.log(result); | |
| 382 | // { | |
| 383 | // prefix: '!./', | |
| 384 | // input: '!./foo/*.js', | |
| 385 | // start: 3, | |
| 386 | // base: 'foo', | |
| 387 | // glob: '*.js', | |
| 388 | // isBrace: false, | |
| 389 | // isBracket: false, | |
| 390 | // isGlob: true, | |
| 391 | // isExtglob: false, | |
| 392 | // isGlobstar: false, | |
| 393 | // negated: true, | |
| 394 | // maxDepth: 2, | |
| 395 | // tokens: [ | |
| 396 | // { value: '!./', depth: 0, isGlob: false, negated: true, isPrefix: true }, | |
| 397 | // { value: 'foo', depth: 1, isGlob: false }, | |
| 398 | // { value: '*.js', depth: 1, isGlob: true } | |
| 399 | // ], | |
| 400 | // slashes: [ 2, 6 ], | |
| 401 | // parts: [ 'foo', '*.js' ] | |
| 402 | // } | |
| 403 | ``` | |
| 404 | ||
| 405 | <br> | |
| 406 | ||
| 407 | ### Options Examples | |
| 408 | ||
| 409 | #### options.expandRange | |
| 410 | ||
| 411 | **Type**: `function` | |
| 412 | ||
| 413 | **Default**: `undefined` | |
| 414 | ||
| 415 | Custom function for expanding ranges in brace patterns. The [fill-range](https://github.com/jonschlinkert/fill-range) library is ideal for this purpose, or you can use custom code to do whatever you need. | |
| 416 | ||
| 417 | **Example** | |
| 418 | ||
| 419 | The following example shows how to create a glob that matches a folder | |
| 420 | ||
| 421 | ```js | |
| 422 | const fill = require('fill-range'); | |
| 423 | const regex = pm.makeRe('foo/{01..25}/bar', { | |
| 424 | expandRange(a, b) { | |
| 425 | return `(${fill(a, b, { toRegex: true })})`; | |
| 426 | } | |
| 427 | }); | |
| 428 | ||
| 429 | console.log(regex); | |
| 430 | //=> /^(?:foo\/((?:0[1-9]|1[0-9]|2[0-5]))\/bar)$/ | |
| 431 | ||
| 432 | console.log(regex.test('foo/00/bar')) // false | |
| 433 | console.log(regex.test('foo/01/bar')) // true | |
| 434 | console.log(regex.test('foo/10/bar')) // true | |
| 435 | console.log(regex.test('foo/22/bar')) // true | |
| 436 | console.log(regex.test('foo/25/bar')) // true | |
| 437 | console.log(regex.test('foo/26/bar')) // false | |
| 438 | ``` | |
| 439 | ||
| 440 | #### options.format | |
| 441 | ||
| 442 | **Type**: `function` | |
| 443 | ||
| 444 | **Default**: `undefined` | |
| 445 | ||
| 446 | Custom function for formatting strings before they're matched. | |
| 447 | ||
| 448 | **Example** | |
| 449 | ||
| 450 | ```js | |
| 451 | // strip leading './' from strings | |
| 452 | const format = str => str.replace(/^\.\//, ''); | |
| 453 | const isMatch = picomatch('foo/*.js', { format }); | |
| 454 | console.log(isMatch('./foo/bar.js')); //=> true | |
| 455 | ``` | |
| 456 | ||
| 457 | #### options.onMatch | |
| 458 | ||
| 459 | ```js | |
| 460 | const onMatch = ({ glob, regex, input, output }) => { | |
| 461 | console.log({ glob, regex, input, output }); | |
| 462 | }; | |
| 463 | ||
| 464 | const isMatch = picomatch('*', { onMatch }); | |
| 465 | isMatch('foo'); | |
| 466 | isMatch('bar'); | |
| 467 | isMatch('baz'); | |
| 468 | ``` | |
| 469 | ||
| 470 | #### options.onIgnore | |
| 471 | ||
| 472 | ```js | |
| 473 | const onIgnore = ({ glob, regex, input, output }) => { | |
| 474 | console.log({ glob, regex, input, output }); | |
| 475 | }; | |
| 476 | ||
| 477 | const isMatch = picomatch('*', { onIgnore, ignore: 'f*' }); | |
| 478 | isMatch('foo'); | |
| 479 | isMatch('bar'); | |
| 480 | isMatch('baz'); | |
| 481 | ``` | |
| 482 | ||
| 483 | #### options.onResult | |
| 484 | ||
| 485 | ```js | |
| 486 | const onResult = ({ glob, regex, input, output }) => { | |
| 487 | console.log({ glob, regex, input, output }); | |
| 488 | }; | |
| 489 | ||
| 490 | const isMatch = picomatch('*', { onResult, ignore: 'f*' }); | |
| 491 | isMatch('foo'); | |
| 492 | isMatch('bar'); | |
| 493 | isMatch('baz'); | |
| 494 | ``` | |
| 495 | ||
| 496 | <br> | |
| 497 | <br> | |
| 498 | ||
| 499 | ## Globbing features | |
| 500 | ||
| 501 | * [Basic globbing](#basic-globbing) (Wildcard matching) | |
| 502 | * [Advanced globbing](#advanced-globbing) (extglobs, posix brackets, brace matching) | |
| 503 | ||
| 504 | ### Basic globbing | |
| 505 | ||
| 506 | | **Character** | **Description** | | |
| 507 | | --- | --- | | |
| 508 | | `*` | Matches any character zero or more times, excluding path separators. Does _not match_ path separators or hidden files or directories ("dotfiles"), unless explicitly enabled by setting the `dot` option to `true`. | | |
| 509 | | `**` | Matches any character zero or more times, including path separators. Note that `**` will only match path separators (`/`, and `\\` with the `windows` option) when they are the only characters in a path segment. Thus, `foo**/bar` is equivalent to `foo*/bar`, and `foo/a**b/bar` is equivalent to `foo/a*b/bar`, and _more than two_ consecutive stars in a glob path segment are regarded as _a single star_. Thus, `foo/***/bar` is equivalent to `foo/*/bar`. | | |
| 510 | | `?` | Matches any character excluding path separators one time. Does _not match_ path separators or leading dots. | | |
| 511 | | `[abc]` | Matches any characters inside the brackets. For example, `[abc]` would match the characters `a`, `b` or `c`, and nothing else. | | |
| 512 | ||
| 513 | #### Matching behavior vs. Bash | |
| 514 | ||
| 515 | Picomatch's matching features and expected results in unit tests are based on Bash's unit tests and the Bash 4.3 specification, with the following exceptions: | |
| 516 | ||
| 517 | * Bash will match `foo/bar/baz` with `*`. Picomatch only matches nested directories with `**`. | |
| 518 | * Bash greedily matches with negated extglobs. For example, Bash 4.3 says that `!(foo)*` should match `foo` and `foobar`, since the trailing `*` bracktracks to match the preceding pattern. This is very memory-inefficient, and IMHO, also incorrect. Picomatch would return `false` for both `foo` and `foobar`. | |
| 519 | ||
| 520 | <br> | |
| 521 | ||
| 522 | ### Advanced globbing | |
| 523 | ||
| 524 | * [extglobs](#extglobs) | |
| 525 | * [POSIX brackets](#posix-brackets) | |
| 526 | * [Braces](#brace-expansion) | |
| 527 | ||
| 528 | #### Extglobs | |
| 529 | ||
| 530 | | **Pattern** | **Description** | | |
| 531 | | --- | --- | | |
| 532 | | `@(pattern)` | Match _only one_ consecutive occurrence of `pattern` | | |
| 533 | | `*(pattern)` | Match _zero or more_ consecutive occurrences of `pattern` | | |
| 534 | | `+(pattern)` | Match _one or more_ consecutive occurrences of `pattern` | | |
| 535 | | `?(pattern)` | Match _zero or **one**_ consecutive occurrences of `pattern` | | |
| 536 | | `!(pattern)` | Match _anything but_ `pattern` | | |
| 537 | ||
| 538 | **Examples** | |
| 539 | ||
| 540 | ```js | |
| 541 | const pm = require('picomatch'); | |
| 542 | ||
| 543 | // *(pattern) matches ZERO or more of "pattern" | |
| 544 | console.log(pm.isMatch('a', 'a*(z)')); // true | |
| 545 | console.log(pm.isMatch('az', 'a*(z)')); // true | |
| 546 | console.log(pm.isMatch('azzz', 'a*(z)')); // true | |
| 547 | ||
| 548 | // +(pattern) matches ONE or more of "pattern" | |
| 549 | console.log(pm.isMatch('a', 'a+(z)')); // false | |
| 550 | console.log(pm.isMatch('az', 'a+(z)')); // true | |
| 551 | console.log(pm.isMatch('azzz', 'a+(z)')); // true | |
| 552 | ||
| 553 | // supports multiple extglobs | |
| 554 | console.log(pm.isMatch('foo.bar', '!(foo).!(bar)')); // false | |
| 555 | ||
| 556 | // supports nested extglobs | |
| 557 | console.log(pm.isMatch('foo.bar', '!(!(foo)).!(!(bar))')); // true | |
| 558 | ||
| 559 | // risky quantified extglobs are treated literally by default | |
| 560 | console.log(pm.makeRe('+(a|aa)')); | |
| 561 | //=> /^(?:\+\(a\|aa\))$/ | |
| 562 | ||
| 563 | // increase the limit to allow a small amount of nested quantified extglobs | |
| 564 | console.log(pm.isMatch('aaa', '+(+(a))', { maxExtglobRecursion: 1 })); // true | |
| 565 | ``` | |
| 566 | ||
| 567 | #### POSIX brackets | |
| 568 | ||
| 569 | POSIX classes are disabled by default. Enable this feature by setting the `posix` option to true. | |
| 570 | ||
| 571 | **Enable POSIX bracket support** | |
| 572 | ||
| 573 | ```js | |
| 574 | console.log(pm.makeRe('[[:word:]]+', { posix: true })); | |
| 575 | //=> /^(?:(?=.)[A-Za-z0-9_]+\/?)$/ | |
| 576 | ``` | |
| 577 | ||
| 578 | **Supported POSIX classes** | |
| 579 | ||
| 580 | The following named POSIX bracket expressions are supported: | |
| 581 | ||
| 582 | * `[:alnum:]` - Alphanumeric characters, equ `[a-zA-Z0-9]` | |
| 583 | * `[:alpha:]` - Alphabetical characters, equivalent to `[a-zA-Z]`. | |
| 584 | * `[:ascii:]` - ASCII characters, equivalent to `[\\x00-\\x7F]`. | |
| 585 | * `[:blank:]` - Space and tab characters, equivalent to `[ \\t]`. | |
| 586 | * `[:cntrl:]` - Control characters, equivalent to `[\\x00-\\x1F\\x7F]`. | |
| 587 | * `[:digit:]` - Numerical digits, equivalent to `[0-9]`. | |
| 588 | * `[:graph:]` - Graph characters, equivalent to `[\\x21-\\x7E]`. | |
| 589 | * `[:lower:]` - Lowercase letters, equivalent to `[a-z]`. | |
| 590 | * `[:print:]` - Print characters, equivalent to `[\\x20-\\x7E ]`. | |
| 591 | * `[:punct:]` - Punctuation and symbols, equivalent to `[\\-!"#$%&\'()\\*+,./:;<=>?@[\\]^_`{|}~]`. | |
| 592 | * `[:space:]` - Extended space characters, equivalent to `[ \\t\\r\\n\\v\\f]`. | |
| 593 | * `[:upper:]` - Uppercase letters, equivalent to `[A-Z]`. | |
| 594 | * `[:word:]` - Word characters (letters, numbers and underscores), equivalent to `[A-Za-z0-9_]`. | |
| 595 | * `[:xdigit:]` - Hexadecimal digits, equivalent to `[A-Fa-f0-9]`. | |
| 596 | ||
| 597 | See the [Bash Reference Manual](https://www.gnu.org/software/bash/manual/html_node/Pattern-Matching.html) for more information. | |
| 598 | ||
| 599 | ### Braces | |
| 600 | ||
| 601 | Picomatch only does [brace expansion](https://www.gnu.org/software/bash/manual/html_node/Brace-Expansion.html) of comma-delimited lists (e.g. `a/{b,c}/d`). For advanced matching with braces, use [micromatch](https://github.com/micromatch/micromatch), which supports advanced syntax such as ranges (e.g. `{01..03}`) and increments (e.g. `{2..10..2}`). | |
| 602 | ||
| 603 | ### Matching special characters as literals | |
| 604 | ||
| 605 | If you wish to match the following special characters in a filepath, and you want to use these characters in your glob pattern, they must be escaped with backslashes or quotes: | |
| 606 | ||
| 607 | **Special Characters** | |
| 608 | ||
| 609 | Some characters that are used for matching in regular expressions are also regarded as valid file path characters on some platforms. | |
| 610 | ||
| 611 | To match any of the following characters as literals: `$^*+?()[] | |
| 612 | ||
| 613 | Examples: | |
| 614 | ||
| 615 | ```js | |
| 616 | console.log(pm.makeRe('foo/bar \\(1\\)')); | |
| 617 | console.log(pm.makeRe('foo/bar \\(1\\)')); | |
| 618 | ``` | |
| 619 | ||
| 620 | <br> | |
| 621 | <br> | |
| 622 | ||
| 623 | ## Library Comparisons | |
| 624 | ||
| 625 | The following table shows which features are supported by [minimatch](https://github.com/isaacs/minimatch), [micromatch](https://github.com/micromatch/micromatch), [picomatch](https://github.com/micromatch/picomatch), [nanomatch](https://github.com/micromatch/nanomatch), [extglob](https://github.com/micromatch/extglob), [braces](https://github.com/micromatch/braces), and [expand-brackets](https://github.com/micromatch/expand-brackets). | |
| 626 | ||
| 627 | | **Feature** | `minimatch` | `micromatch` | `picomatch` | `nanomatch` | `extglob` | `braces` | `expand-brackets` | | |
| 628 | | --- | --- | --- | --- | --- | --- | --- | --- | | |
| 629 | | Wildcard matching (`*?+`) | ✔ | ✔ | ✔ | ✔ | - | - | - | | |
| 630 | | Advancing globbing | ✔ | ✔ | ✔ | - | - | - | - | | |
| 631 | | Brace _matching_ | ✔ | ✔ | ✔ | - | - | ✔ | - | | |
| 632 | | Brace _expansion_ | ✔ | ✔ | - | - | - | ✔ | - | | |
| 633 | | Extglobs | partial | ✔ | ✔ | - | ✔ | - | - | | |
| 634 | | Posix brackets | - | ✔ | ✔ | - | - | - | ✔ | | |
| 635 | | Regular expression syntax | - | ✔ | ✔ | ✔ | ✔ | - | ✔ | | |
| 636 | | File system operations | - | - | - | - | - | - | - | | |
| 637 | ||
| 638 | <br> | |
| 639 | <br> | |
| 640 | ||
| 641 | ## Benchmarks | |
| 642 | ||
| 643 | Performance comparison of picomatch and minimatch. | |
| 644 | ||
| 645 | ``` | |
| 646 | # .makeRe star (*) | |
| 647 | picomatch x 3,251,247 ops/sec ±0.25% (95 runs sampled) | |
| 648 | minimatch x 497,224 ops/sec ±0.11% (100 runs sampled) | |
| 649 | ||
| 650 | # .makeRe star; dot=true (*) | |
| 651 | picomatch x 2,624,035 ops/sec ±0.16% (98 runs sampled) | |
| 652 | minimatch x 446,244 ops/sec ±0.63% (99 runs sampled) | |
| 653 | ||
| 654 | # .makeRe globstar (**) | |
| 655 | picomatch x 2,524,465 ops/sec ±0.13% (99 runs sampled) | |
| 656 | minimatch x 1,396,257 ops/sec ±0.58% (96 runs sampled) | |
| 657 | ||
| 658 | # .makeRe globstars (**/**/**) | |
| 659 | picomatch x 2,545,674 ops/sec ±0.10% (99 runs sampled) | |
| 660 | minimatch x 1,196,835 ops/sec ±0.63% (98 runs sampled) | |
| 661 | ||
| 662 | # .makeRe with leading star (*.txt) | |
| 663 | picomatch x 2,537,708 ops/sec ±0.11% (100 runs sampled) | |
| 664 | minimatch x 345,284 ops/sec ±0.64% (96 runs sampled) | |
| 665 | ||
| 666 | # .makeRe - basic braces ({a,b,c}*.txt) | |
| 667 | picomatch x 505,430 ops/sec ±1.04% (94 runs sampled) | |
| 668 | minimatch x 107,991 ops/sec ±0.54% (99 runs sampled) | |
| 669 | ||
| 670 | # .makeRe - short ranges ({a..z}*.txt) | |
| 671 | picomatch x 371,179 ops/sec ±2.91% (77 runs sampled) | |
| 672 | minimatch x 14,104 ops/sec ±0.61% (99 runs sampled) | |
| 673 | ||
| 674 | # .makeRe - medium ranges ({1..100000}*.txt) | |
| 675 | picomatch x 384,958 ops/sec ±1.70% (82 runs sampled) | |
| 676 | minimatch x 2.55 ops/sec ±3.22% (11 runs sampled) | |
| 677 | ||
| 678 | # .makeRe - long ranges ({1..10000000}*.txt) | |
| 679 | picomatch x 382,552 ops/sec ±1.52% (71 runs sampled) | |
| 680 | minimatch x 0.83 ops/sec ±5.67% (7 runs sampled)) | |
| 681 | ``` | |
| 682 | ||
| 683 | <br> | |
| 684 | <br> | |
| 685 | ||
| 686 | ## Philosophies | |
| 687 | ||
| 688 | The goal of this library is to be blazing fast, without compromising on accuracy. | |
| 689 | ||
| 690 | **Accuracy** | |
| 691 | ||
| 692 | The number one of goal of this library is accuracy. However, it's not unusual for different glob implementations to have different rules for matching behavior, even with simple wildcard matching. It gets increasingly more complicated when combinations of different features are combined, like when extglobs are combined with globstars, braces, slashes, and so on: `!(**/{a,b,*/c})`. | |
| 693 | ||
| 694 | Thus, given that there is no canonical glob specification to use as a single source of truth when differences of opinion arise regarding behavior, sometimes we have to implement our best judgement and rely on feedback from users to make improvements. | |
| 695 | ||
| 696 | **Performance** | |
| 697 | ||
| 698 | Although this library performs well in benchmarks, and in most cases it's faster than other popular libraries we benchmarked against, we will always choose accuracy over performance. It's not helpful to anyone if our library is faster at returning the wrong answer. | |
| 699 | ||
| 700 | <br> | |
| 701 | <br> | |
| 702 | ||
| 703 | ## About | |
| 704 | ||
| 705 | <details> | |
| 706 | <summary><strong>Contributing</strong></summary> | |
| 707 | ||
| 708 | Pull requests and stars are always welcome. For bugs and feature requests, [please create an issue](../../issues/new). | |
| 709 | ||
| 710 | Please read the [contributing guide](.github/contributing.md) for advice on opening issues, pull requests, and coding standards. | |
| 711 | ||
| 712 | </details> | |
| 713 | ||
| 714 | <details> | |
| 715 | <summary><strong>Running Tests</strong></summary> | |
| 716 | ||
| 717 | Running and reviewing unit tests is a great way to get familiarized with a library and its API. You can install dependencies and run tests with the following command: | |
| 718 | ||
| 719 | ```sh | |
| 720 | npm install && npm test | |
| 721 | ``` | |
| 722 | ||
| 723 | </details> | |
| 724 | ||
| 725 | <details> | |
| 726 | <summary><strong>Building docs</strong></summary> | |
| 727 | ||
| 728 | _(This project's readme.md is generated by [verb](https://github.com/verbose/verb-generate-readme), please don't edit the readme directly. Any changes to the readme must be made in the [.verb.md](.verb.md) readme template.)_ | |
| 729 | ||
| 730 | To generate the readme, run the following command: | |
| 731 | ||
| 732 | ```sh | |
| 733 | npm install -g verbose/verb#dev verb-generate-readme && verb | |
| 734 | ``` | |
| 735 | ||
| 736 | </details> | |
| 737 | ||
| 738 | ### Author | |
| 739 | ||
| 740 | **Jon Schlinkert** | |
| 741 | ||
| 742 | * [GitHub Profile](https://github.com/jonschlinkert) | |
| 743 | * [Twitter Profile](https://twitter.com/jonschlinkert) | |
| 744 | * [LinkedIn Profile](https://linkedin.com/in/jonschlinkert) | |
| 745 | ||
| 746 | ### License | |
| 747 | ||
| 748 | Copyright © 2017-present, [Jon Schlinkert](https://github.com/jonschlinkert). | |
| 749 | Released under the [MIT License](LICENSE). |
| … | … | @@ -0,0 +1,17 @@ |
| 1 | 'use strict'; | |
| 2 | ||
| 3 | const pico = require('./lib/picomatch'); | |
| 4 | const utils = require('./lib/utils'); | |
| 5 | ||
| 6 | function picomatch(glob, options, returnState = false) { | |
| 7 | // default to os.platform() | |
| 8 | if (options && (options.windows === null || options.windows === undefined)) { | |
| 9 | // don't mutate the original options object | |
| 10 | options = { ...options, windows: utils.isWindows() }; | |
| 11 | } | |
| 12 | ||
| 13 | return pico(glob, options, returnState); | |
| 14 | } | |
| 15 | ||
| 16 | Object.assign(picomatch, pico); | |
| 17 | module.exports = picomatch; |
| … | … | @@ -0,0 +1,184 @@ |
| 1 | 'use strict'; | |
| 2 | ||
| 3 | const WIN_SLASH = '\\\\/'; | |
| 4 | const WIN_NO_SLASH = `[^${WIN_SLASH}]`; | |
| 5 | ||
| 6 | const DEFAULT_MAX_EXTGLOB_RECURSION = 0; | |
| 7 | ||
| 8 | /** | |
| 9 | * Posix glob regex | |
| 10 | */ | |
| 11 | ||
| 12 | const DOT_LITERAL = '\\.'; | |
| 13 | const PLUS_LITERAL = '\\+'; | |
| 14 | const QMARK_LITERAL = '\\?'; | |
| 15 | const SLASH_LITERAL = '\\/'; | |
| 16 | const ONE_CHAR = '(?=.)'; | |
| 17 | const QMARK = '[^/]'; | |
| 18 | const END_ANCHOR = `(?:${SLASH_LITERAL}|$)`; | |
| 19 | const START_ANCHOR = `(?:^|${SLASH_LITERAL})`; | |
| 20 | const DOTS_SLASH = `${DOT_LITERAL}{1,2}${END_ANCHOR}`; | |
| 21 | const NO_DOT = `(?!${DOT_LITERAL})`; | |
| 22 | const NO_DOTS = `(?!${START_ANCHOR}${DOTS_SLASH})`; | |
| 23 | const NO_DOT_SLASH = `(?!${DOT_LITERAL}{0,1}${END_ANCHOR})`; | |
| 24 | const NO_DOTS_SLASH = `(?!${DOTS_SLASH})`; | |
| 25 | const QMARK_NO_DOT = `[^.${SLASH_LITERAL}]`; | |
| 26 | const STAR = `${QMARK}*?`; | |
| 27 | const SEP = '/'; | |
| 28 | ||
| 29 | const POSIX_CHARS = { | |
| 30 | DOT_LITERAL, | |
| 31 | PLUS_LITERAL, | |
| 32 | QMARK_LITERAL, | |
| 33 | SLASH_LITERAL, | |
| 34 | ONE_CHAR, | |
| 35 | QMARK, | |
| 36 | END_ANCHOR, | |
| 37 | DOTS_SLASH, | |
| 38 | NO_DOT, | |
| 39 | NO_DOTS, | |
| 40 | NO_DOT_SLASH, | |
| 41 | NO_DOTS_SLASH, | |
| 42 | QMARK_NO_DOT, | |
| 43 | STAR, | |
| 44 | START_ANCHOR, | |
| 45 | SEP | |
| 46 | }; | |
| 47 | ||
| 48 | /** | |
| 49 | * Windows glob regex | |
| 50 | */ | |
| 51 | ||
| 52 | const WINDOWS_CHARS = { | |
| 53 | ...POSIX_CHARS, | |
| 54 | ||
| 55 | SLASH_LITERAL: `[${WIN_SLASH}]`, | |
| 56 | QMARK: WIN_NO_SLASH, | |
| 57 | STAR: `${WIN_NO_SLASH}*?`, | |
| 58 | DOTS_SLASH: `${DOT_LITERAL}{1,2}(?:[${WIN_SLASH}]|$)`, | |
| 59 | NO_DOT: `(?!${DOT_LITERAL})`, | |
| 60 | NO_DOTS: `(?!(?:^|[${WIN_SLASH}])${DOT_LITERAL}{1,2}(?:[${WIN_SLASH}]|$))`, | |
| 61 | NO_DOT_SLASH: `(?!${DOT_LITERAL}{0,1}(?:[${WIN_SLASH}]|$))`, | |
| 62 | NO_DOTS_SLASH: `(?!${DOT_LITERAL}{1,2}(?:[${WIN_SLASH}]|$))`, | |
| 63 | QMARK_NO_DOT: `[^.${WIN_SLASH}]`, | |
| 64 | START_ANCHOR: `(?:^|[${WIN_SLASH}])`, | |
| 65 | END_ANCHOR: `(?:[${WIN_SLASH}]|$)`, | |
| 66 | SEP: '\\' | |
| 67 | }; | |
| 68 | ||
| 69 | /** | |
| 70 | * POSIX Bracket Regex | |
| 71 | */ | |
| 72 | ||
| 73 | const POSIX_REGEX_SOURCE = { | |
| 74 | __proto__: null, | |
| 75 | alnum: 'a-zA-Z0-9', | |
| 76 | alpha: 'a-zA-Z', | |
| 77 | ascii: '\\x00-\\x7F', | |
| 78 | blank: ' \\t', | |
| 79 | cntrl: '\\x00-\\x1F\\x7F', | |
| 80 | digit: '0-9', | |
| 81 | graph: '\\x21-\\x7E', | |
| 82 | lower: 'a-z', | |
| 83 | print: '\\x20-\\x7E ', | |
| 84 | punct: '\\-!"#$%&\'()\\*+,./:;<=>?@[\\]^_`{|}~', | |
| 85 | space: ' \\t\\r\\n\\v\\f', | |
| 86 | upper: 'A-Z', | |
| 87 | word: 'A-Za-z0-9_', | |
| 88 | xdigit: 'A-Fa-f0-9' | |
| 89 | }; | |
| 90 | ||
| 91 | module.exports = { | |
| 92 | DEFAULT_MAX_EXTGLOB_RECURSION, | |
| 93 | MAX_LENGTH: 1024 * 64, | |
| 94 | POSIX_REGEX_SOURCE, | |
| 95 | ||
| 96 | // regular expressions | |
| 97 | REGEX_BACKSLASH: /\\(?![*+?^${}(|)[\]])/g, | |
| 98 | REGEX_NON_SPECIAL_CHARS: /^[^@![\].,$*+?^{}()|\\/]+/, | |
| 99 | REGEX_SPECIAL_CHARS: /[-*+?.^${}(|)[\]]/, | |
| 100 | REGEX_SPECIAL_CHARS_BACKREF: /(\\?)((\W)(\3*))/g, | |
| 101 | REGEX_SPECIAL_CHARS_GLOBAL: /([-*+?.^${}(|)[\]])/g, | |
| 102 | REGEX_REMOVE_BACKSLASH: /(?:\[.*?[^\\]\]|\\(?=.))/g, | |
| 103 | ||
| 104 | // Replace globs with equivalent patterns to reduce parsing time. | |
| 105 | REPLACEMENTS: { | |
| 106 | __proto__: null, | |
| 107 | '***': '*', | |
| 108 | '**/**': '**', | |
| 109 | '**/**/**': '**' | |
| 110 | }, | |
| 111 | ||
| 112 | // Digits | |
| 113 | CHAR_0: 48, /* 0 */ | |
| 114 | CHAR_9: 57, /* 9 */ | |
| 115 | ||
| 116 | // Alphabet chars. | |
| 117 | CHAR_UPPERCASE_A: 65, /* A */ | |
| 118 | CHAR_LOWERCASE_A: 97, /* a */ | |
| 119 | CHAR_UPPERCASE_Z: 90, /* Z */ | |
| 120 | CHAR_LOWERCASE_Z: 122, /* z */ | |
| 121 | ||
| 122 | CHAR_LEFT_PARENTHESES: 40, /* ( */ | |
| 123 | CHAR_RIGHT_PARENTHESES: 41, /* ) */ | |
| 124 | ||
| 125 | CHAR_ASTERISK: 42, /* * */ | |
| 126 | ||
| 127 | // Non-alphabetic chars. | |
| 128 | CHAR_AMPERSAND: 38, /* & */ | |
| 129 | CHAR_AT: 64, /* @ */ | |
| 130 | CHAR_BACKWARD_SLASH: 92, /* \ */ | |
| 131 | CHAR_CARRIAGE_RETURN: 13, /* \r */ | |
| 132 | CHAR_CIRCUMFLEX_ACCENT: 94, /* ^ */ | |
| 133 | CHAR_COLON: 58, /* : */ | |
| 134 | CHAR_COMMA: 44, /* , */ | |
| 135 | CHAR_DOT: 46, /* . */ | |
| 136 | CHAR_DOUBLE_QUOTE: 34, /* " */ | |
| 137 | CHAR_EQUAL: 61, /* = */ | |
| 138 | CHAR_EXCLAMATION_MARK: 33, /* ! */ | |
| 139 | CHAR_FORM_FEED: 12, /* \f */ | |
| 140 | CHAR_FORWARD_SLASH: 47, /* / */ | |
| 141 | CHAR_GRAVE_ACCENT: 96, /* ` */ | |
| 142 | CHAR_HASH: 35, /* # */ | |
| 143 | CHAR_HYPHEN_MINUS: 45, /* - */ | |
| 144 | CHAR_LEFT_ANGLE_BRACKET: 60, /* < */ | |
| 145 | CHAR_LEFT_CURLY_BRACE: 123, /* { */ | |
| 146 | CHAR_LEFT_SQUARE_BRACKET: 91, /* [ */ | |
| 147 | CHAR_LINE_FEED: 10, /* \n */ | |
| 148 | CHAR_NO_BREAK_SPACE: 160, /* \u00A0 */ | |
| 149 | CHAR_PERCENT: 37, /* % */ | |
| 150 | CHAR_PLUS: 43, /* + */ | |
| 151 | CHAR_QUESTION_MARK: 63, /* ? */ | |
| 152 | CHAR_RIGHT_ANGLE_BRACKET: 62, /* > */ | |
| 153 | CHAR_RIGHT_CURLY_BRACE: 125, /* } */ | |
| 154 | CHAR_RIGHT_SQUARE_BRACKET: 93, /* ] */ | |
| 155 | CHAR_SEMICOLON: 59, /* ; */ | |
| 156 | CHAR_SINGLE_QUOTE: 39, /* ' */ | |
| 157 | CHAR_SPACE: 32, /* */ | |
| 158 | CHAR_TAB: 9, /* \t */ | |
| 159 | CHAR_UNDERSCORE: 95, /* _ */ | |
| 160 | CHAR_VERTICAL_LINE: 124, /* | */ | |
| 161 | CHAR_ZERO_WIDTH_NOBREAK_SPACE: 65279, /* \uFEFF */ | |
| 162 | ||
| 163 | /** | |
| 164 | * Create EXTGLOB_CHARS | |
| 165 | */ | |
| 166 | ||
| 167 | extglobChars(chars) { | |
| 168 | return { | |
| 169 | '!': { type: 'negate', open: '(?:(?!(?:', close: `))${chars.STAR})` }, | |
| 170 | '?': { type: 'qmark', open: '(?:', close: ')?' }, | |
| 171 | '+': { type: 'plus', open: '(?:', close: ')+' }, | |
| 172 | '*': { type: 'star', open: '(?:', close: ')*' }, | |
| 173 | '@': { type: 'at', open: '(?:', close: ')' } | |
| 174 | }; | |
| 175 | }, | |
| 176 | ||
| 177 | /** | |
| 178 | * Create GLOB_CHARS | |
| 179 | */ | |
| 180 | ||
| 181 | globChars(win32) { | |
| 182 | return win32 === true ? WINDOWS_CHARS : POSIX_CHARS; | |
| 183 | } | |
| 184 | }; |
| … | … | @@ -0,0 +1,1386 @@ |
| 1 | 'use strict'; | |
| 2 | ||
| 3 | const constants = require('./constants'); | |
| 4 | const utils = require('./utils'); | |
| 5 | ||
| 6 | /** | |
| 7 | * Constants | |
| 8 | */ | |
| 9 | ||
| 10 | const { | |
| 11 | MAX_LENGTH, | |
| 12 | POSIX_REGEX_SOURCE, | |
| 13 | REGEX_NON_SPECIAL_CHARS, | |
| 14 | REGEX_SPECIAL_CHARS_BACKREF, | |
| 15 | REPLACEMENTS | |
| 16 | } = constants; | |
| 17 | ||
| 18 | /** | |
| 19 | * Helpers | |
| 20 | */ | |
| 21 | ||
| 22 | const expandRange = (args, options) => { | |
| 23 | if (typeof options.expandRange === 'function') { | |
| 24 | return options.expandRange(...args, options); | |
| 25 | } | |
| 26 | ||
| 27 | args.sort(); | |
| 28 | const value = `[${args.join('-')}]`; | |
| 29 | ||
| 30 | try { | |
| 31 | /* eslint-disable-next-line no-new */ | |
| 32 | new RegExp(value); | |
| 33 | } catch (ex) { | |
| 34 | return args.map(v => utils.escapeRegex(v)).join('..'); | |
| 35 | } | |
| 36 | ||
| 37 | return value; | |
| 38 | }; | |
| 39 | ||
| 40 | /** | |
| 41 | * Create the message for a syntax error | |
| 42 | */ | |
| 43 | ||
| 44 | const syntaxError = (type, char) => { | |
| 45 | return `Missing ${type}: "${char}" - use "\\\\${char}" to match literal characters`; | |
| 46 | }; | |
| 47 | ||
| 48 | const splitTopLevel = input => { | |
| 49 | const parts = []; | |
| 50 | let bracket = 0; | |
| 51 | let paren = 0; | |
| 52 | let quote = 0; | |
| 53 | let value = ''; | |
| 54 | let escaped = false; | |
| 55 | ||
| 56 | for (const ch of input) { | |
| 57 | if (escaped === true) { | |
| 58 | value += ch; | |
| 59 | escaped = false; | |
| 60 | continue; | |
| 61 | } | |
| 62 | ||
| 63 | if (ch === '\\') { | |
| 64 | value += ch; | |
| 65 | escaped = true; | |
| 66 | continue; | |
| 67 | } | |
| 68 | ||
| 69 | if (ch === '"') { | |
| 70 | quote = quote === 1 ? 0 : 1; | |
| 71 | value += ch; | |
| 72 | continue; | |
| 73 | } | |
| 74 | ||
| 75 | if (quote === 0) { | |
| 76 | if (ch === '[') { | |
| 77 | bracket++; | |
| 78 | } else if (ch === ']' && bracket > 0) { | |
| 79 | bracket--; | |
| 80 | } else if (bracket === 0) { | |
| 81 | if (ch === '(') { | |
| 82 | paren++; | |
| 83 | } else if (ch === ')' && paren > 0) { | |
| 84 | paren--; | |
| 85 | } else if (ch === '|' && paren === 0) { | |
| 86 | parts.push(value); | |
| 87 | value = ''; | |
| 88 | continue; | |
| 89 | } | |
| 90 | } | |
| 91 | } | |
| 92 | ||
| 93 | value += ch; | |
| 94 | } | |
| 95 | ||
| 96 | parts.push(value); | |
| 97 | return parts; | |
| 98 | }; | |
| 99 | ||
| 100 | const isPlainBranch = branch => { | |
| 101 | let escaped = false; | |
| 102 | ||
| 103 | for (const ch of branch) { | |
| 104 | if (escaped === true) { | |
| 105 | escaped = false; | |
| 106 | continue; | |
| 107 | } | |
| 108 | ||
| 109 | if (ch === '\\') { | |
| 110 | escaped = true; | |
| 111 | continue; | |
| 112 | } | |
| 113 | ||
| 114 | if (/[?*+@!()[\]{}]/.test(ch)) { | |
| 115 | return false; | |
| 116 | } | |
| 117 | } | |
| 118 | ||
| 119 | return true; | |
| 120 | }; | |
| 121 | ||
| 122 | const normalizeSimpleBranch = branch => { | |
| 123 | let value = branch.trim(); | |
| 124 | let changed = true; | |
| 125 | ||
| 126 | while (changed === true) { | |
| 127 | changed = false; | |
| 128 | ||
| 129 | if (/^@\([^\\()[\]{}|]+\)$/.test(value)) { | |
| 130 | value = value.slice(2, -1); | |
| 131 | changed = true; | |
| 132 | } | |
| 133 | } | |
| 134 | ||
| 135 | if (!isPlainBranch(value)) { | |
| 136 | return; | |
| 137 | } | |
| 138 | ||
| 139 | return value.replace(/\\(.)/g, '$1'); | |
| 140 | }; | |
| 141 | ||
| 142 | const hasRepeatedCharPrefixOverlap = branches => { | |
| 143 | const values = branches.map(normalizeSimpleBranch).filter(Boolean); | |
| 144 | ||
| 145 | for (let i = 0; i < values.length; i++) { | |
| 146 | for (let j = i + 1; j < values.length; j++) { | |
| 147 | const a = values[i]; | |
| 148 | const b = values[j]; | |
| 149 | const char = a[0]; | |
| 150 | ||
| 151 | if (!char || a !== char.repeat(a.length) || b !== char.repeat(b.length)) { | |
| 152 | continue; | |
| 153 | } | |
| 154 | ||
| 155 | if (a === b || a.startsWith(b) || b.startsWith(a)) { | |
| 156 | return true; | |
| 157 | } | |
| 158 | } | |
| 159 | } | |
| 160 | ||
| 161 | return false; | |
| 162 | }; | |
| 163 | ||
| 164 | const parseRepeatedExtglob = (pattern, requireEnd = true) => { | |
| 165 | if ((pattern[0] !== '+' && pattern[0] !== '*') || pattern[1] !== '(') { | |
| 166 | return; | |
| 167 | } | |
| 168 | ||
| 169 | let bracket = 0; | |
| 170 | let paren = 0; | |
| 171 | let quote = 0; | |
| 172 | let escaped = false; | |
| 173 | ||
| 174 | for (let i = 1; i < pattern.length; i++) { | |
| 175 | const ch = pattern[i]; | |
| 176 | ||
| 177 | if (escaped === true) { | |
| 178 | escaped = false; | |
| 179 | continue; | |
| 180 | } | |
| 181 | ||
| 182 | if (ch === '\\') { | |
| 183 | escaped = true; | |
| 184 | continue; | |
| 185 | } | |
| 186 | ||
| 187 | if (ch === '"') { | |
| 188 | quote = quote === 1 ? 0 : 1; | |
| 189 | continue; | |
| 190 | } | |
| 191 | ||
| 192 | if (quote === 1) { | |
| 193 | continue; | |
| 194 | } | |
| 195 | ||
| 196 | if (ch === '[') { | |
| 197 | bracket++; | |
| 198 | continue; | |
| 199 | } | |
| 200 | ||
| 201 | if (ch === ']' && bracket > 0) { | |
| 202 | bracket--; | |
| 203 | continue; | |
| 204 | } | |
| 205 | ||
| 206 | if (bracket > 0) { | |
| 207 | continue; | |
| 208 | } | |
| 209 | ||
| 210 | if (ch === '(') { | |
| 211 | paren++; | |
| 212 | continue; | |
| 213 | } | |
| 214 | ||
| 215 | if (ch === ')') { | |
| 216 | paren--; | |
| 217 | ||
| 218 | if (paren === 0) { | |
| 219 | if (requireEnd === true && i !== pattern.length - 1) { | |
| 220 | return; | |
| 221 | } | |
| 222 | ||
| 223 | return { | |
| 224 | type: pattern[0], | |
| 225 | body: pattern.slice(2, i), | |
| 226 | end: i | |
| 227 | }; | |
| 228 | } | |
| 229 | } | |
| 230 | } | |
| 231 | }; | |
| 232 | ||
| 233 | const getStarExtglobSequenceOutput = pattern => { | |
| 234 | let index = 0; | |
| 235 | const chars = []; | |
| 236 | ||
| 237 | while (index < pattern.length) { | |
| 238 | const match = parseRepeatedExtglob(pattern.slice(index), false); | |
| 239 | ||
| 240 | if (!match || match.type !== '*') { | |
| 241 | return; | |
| 242 | } | |
| 243 | ||
| 244 | const branches = splitTopLevel(match.body).map(branch => branch.trim()); | |
| 245 | if (branches.length !== 1) { | |
| 246 | return; | |
| 247 | } | |
| 248 | ||
| 249 | const branch = normalizeSimpleBranch(branches[0]); | |
| 250 | if (!branch || branch.length !== 1) { | |
| 251 | return; | |
| 252 | } | |
| 253 | ||
| 254 | chars.push(branch); | |
| 255 | index += match.end + 1; | |
| 256 | } | |
| 257 | ||
| 258 | if (chars.length < 1) { | |
| 259 | return; | |
| 260 | } | |
| 261 | ||
| 262 | const source = chars.length === 1 | |
| 263 | ? utils.escapeRegex(chars[0]) | |
| 264 | : `[${chars.map(ch => utils.escapeRegex(ch)).join('')}]`; | |
| 265 | ||
| 266 | return `${source}*`; | |
| 267 | }; | |
| 268 | ||
| 269 | const repeatedExtglobRecursion = pattern => { | |
| 270 | let depth = 0; | |
| 271 | let value = pattern.trim(); | |
| 272 | let match = parseRepeatedExtglob(value); | |
| 273 | ||
| 274 | while (match) { | |
| 275 | depth++; | |
| 276 | value = match.body.trim(); | |
| 277 | match = parseRepeatedExtglob(value); | |
| 278 | } | |
| 279 | ||
| 280 | return depth; | |
| 281 | }; | |
| 282 | ||
| 283 | const analyzeRepeatedExtglob = (body, options) => { | |
| 284 | if (options.maxExtglobRecursion === false) { | |
| 285 | return { risky: false }; | |
| 286 | } | |
| 287 | ||
| 288 | const max = | |
| 289 | typeof options.maxExtglobRecursion === 'number' | |
| 290 | ? options.maxExtglobRecursion | |
| 291 | : constants.DEFAULT_MAX_EXTGLOB_RECURSION; | |
| 292 | ||
| 293 | const branches = splitTopLevel(body).map(branch => branch.trim()); | |
| 294 | ||
| 295 | if (branches.length > 1) { | |
| 296 | if ( | |
| 297 | branches.some(branch => branch === '') || | |
| 298 | branches.some(branch => /^[*?]+$/.test(branch)) || | |
| 299 | hasRepeatedCharPrefixOverlap(branches) | |
| 300 | ) { | |
| 301 | return { risky: true }; | |
| 302 | } | |
| 303 | } | |
| 304 | ||
| 305 | for (const branch of branches) { | |
| 306 | const safeOutput = getStarExtglobSequenceOutput(branch); | |
| 307 | if (safeOutput) { | |
| 308 | return { risky: true, safeOutput }; | |
| 309 | } | |
| 310 | ||
| 311 | if (repeatedExtglobRecursion(branch) > max) { | |
| 312 | return { risky: true }; | |
| 313 | } | |
| 314 | } | |
| 315 | ||
| 316 | return { risky: false }; | |
| 317 | }; | |
| 318 | ||
| 319 | /** | |
| 320 | * Parse the given input string. | |
| 321 | * @param {String} input | |
| 322 | * @param {Object} options | |
| 323 | * @return {Object} | |
| 324 | */ | |
| 325 | ||
| 326 | const parse = (input, options) => { | |
| 327 | if (typeof input !== 'string') { | |
| 328 | throw new TypeError('Expected a string'); | |
| 329 | } | |
| 330 | ||
| 331 | input = REPLACEMENTS[input] || input; | |
| 332 | ||
| 333 | const opts = { ...options }; | |
| 334 | const max = typeof opts.maxLength === 'number' ? Math.min(MAX_LENGTH, opts.maxLength) : MAX_LENGTH; | |
| 335 | ||
| 336 | let len = input.length; | |
| 337 | if (len > max) { | |
| 338 | throw new SyntaxError(`Input length: ${len}, exceeds maximum allowed length: ${max}`); | |
| 339 | } | |
| 340 | ||
| 341 | const bos = { type: 'bos', value: '', output: opts.prepend || '' }; | |
| 342 | const tokens = [bos]; | |
| 343 | ||
| 344 | const capture = opts.capture ? '' : '?:'; | |
| 345 | ||
| 346 | // create constants based on platform, for windows or posix | |
| 347 | const PLATFORM_CHARS = constants.globChars(opts.windows); | |
| 348 | const EXTGLOB_CHARS = constants.extglobChars(PLATFORM_CHARS); | |
| 349 | ||
| 350 | const { | |
| 351 | DOT_LITERAL, | |
| 352 | PLUS_LITERAL, | |
| 353 | SLASH_LITERAL, | |
| 354 | ONE_CHAR, | |
| 355 | DOTS_SLASH, | |
| 356 | NO_DOT, | |
| 357 | NO_DOT_SLASH, | |
| 358 | NO_DOTS_SLASH, | |
| 359 | QMARK, | |
| 360 | QMARK_NO_DOT, | |
| 361 | STAR, | |
| 362 | START_ANCHOR | |
| 363 | } = PLATFORM_CHARS; | |
| 364 | ||
| 365 | const globstar = opts => { | |
| 366 | return `(${capture}(?:(?!${START_ANCHOR}${opts.dot ? DOTS_SLASH : DOT_LITERAL}).)*?)`; | |
| 367 | }; | |
| 368 | ||
| 369 | const nodot = opts.dot ? '' : NO_DOT; | |
| 370 | const qmarkNoDot = opts.dot ? QMARK : QMARK_NO_DOT; | |
| 371 | let star = opts.bash === true ? globstar(opts) : STAR; | |
| 372 | ||
| 373 | if (opts.capture) { | |
| 374 | star = `(${star})`; | |
| 375 | } | |
| 376 | ||
| 377 | // minimatch options support | |
| 378 | if (typeof opts.noext === 'boolean') { | |
| 379 | opts.noextglob = opts.noext; | |
| 380 | } | |
| 381 | ||
| 382 | const state = { | |
| 383 | input, | |
| 384 | index: -1, | |
| 385 | start: 0, | |
| 386 | dot: opts.dot === true, | |
| 387 | consumed: '', | |
| 388 | output: '', | |
| 389 | prefix: '', | |
| 390 | backtrack: false, | |
| 391 | negated: false, | |
| 392 | brackets: 0, | |
| 393 | braces: 0, | |
| 394 | parens: 0, | |
| 395 | quotes: 0, | |
| 396 | globstar: false, | |
| 397 | tokens | |
| 398 | }; | |
| 399 | ||
| 400 | input = utils.removePrefix(input, state); | |
| 401 | len = input.length; | |
| 402 | ||
| 403 | const extglobs = []; | |
| 404 | const braces = []; | |
| 405 | const stack = []; | |
| 406 | let prev = bos; | |
| 407 | let value; | |
| 408 | ||
| 409 | /** | |
| 410 | * Tokenizing helpers | |
| 411 | */ | |
| 412 | ||
| 413 | const eos = () => state.index === len - 1; | |
| 414 | const peek = state.peek = (n = 1) => input[state.index + n]; | |
| 415 | const advance = state.advance = () => input[++state.index] || ''; | |
| 416 | const remaining = () => input.slice(state.index + 1); | |
| 417 | const consume = (value = '', num = 0) => { | |
| 418 | state.consumed += value; | |
| 419 | state.index += num; | |
| 420 | }; | |
| 421 | ||
| 422 | const append = token => { | |
| 423 | state.output += token.output != null ? token.output : token.value; | |
| 424 | consume(token.value); | |
| 425 | }; | |
| 426 | ||
| 427 | const negate = () => { | |
| 428 | let count = 1; | |
| 429 | ||
| 430 | while (peek() === '!' && (peek(2) !== '(' || peek(3) === '?')) { | |
| 431 | advance(); | |
| 432 | state.start++; | |
| 433 | count++; | |
| 434 | } | |
| 435 | ||
| 436 | if (count % 2 === 0) { | |
| 437 | return false; | |
| 438 | } | |
| 439 | ||
| 440 | state.negated = true; | |
| 441 | state.start++; | |
| 442 | return true; | |
| 443 | }; | |
| 444 | ||
| 445 | const increment = type => { | |
| 446 | state[type]++; | |
| 447 | stack.push(type); | |
| 448 | }; | |
| 449 | ||
| 450 | const decrement = type => { | |
| 451 | state[type]--; | |
| 452 | stack.pop(); | |
| 453 | }; | |
| 454 | ||
| 455 | /** | |
| 456 | * Push tokens onto the tokens array. This helper speeds up | |
| 457 | * tokenizing by 1) helping us avoid backtracking as much as possible, | |
| 458 | * and 2) helping us avoid creating extra tokens when consecutive | |
| 459 | * characters are plain text. This improves performance and simplifies | |
| 460 | * lookbehinds. | |
| 461 | */ | |
| 462 | ||
| 463 | const push = tok => { | |
| 464 | if (prev.type === 'globstar') { | |
| 465 | const isBrace = state.braces > 0 && (tok.type === 'comma' || tok.type === 'brace'); | |
| 466 | const isExtglob = tok.extglob === true || (extglobs.length && (tok.type === 'pipe' || tok.type === 'paren')); | |
| 467 | ||
| 468 | if (tok.type !== 'slash' && tok.type !== 'paren' && !isBrace && !isExtglob) { | |
| 469 | state.output = state.output.slice(0, -prev.output.length); | |
| 470 | prev.type = 'star'; | |
| 471 | prev.value = '*'; | |
| 472 | prev.output = star; | |
| 473 | state.output += prev.output; | |
| 474 | } | |
| 475 | } | |
| 476 | ||
| 477 | if (extglobs.length && tok.type !== 'paren') { | |
| 478 | extglobs[extglobs.length - 1].inner += tok.value; | |
| 479 | } | |
| 480 | ||
| 481 | if (tok.value || tok.output) append(tok); | |
| 482 | if (prev && prev.type === 'text' && tok.type === 'text') { | |
| 483 | prev.output = (prev.output || prev.value) + tok.value; | |
| 484 | prev.value += tok.value; | |
| 485 | return; | |
| 486 | } | |
| 487 | ||
| 488 | tok.prev = prev; | |
| 489 | tokens.push(tok); | |
| 490 | prev = tok; | |
| 491 | }; | |
| 492 | ||
| 493 | const extglobOpen = (type, value) => { | |
| 494 | const token = { ...EXTGLOB_CHARS[value], conditions: 1, inner: '' }; | |
| 495 | ||
| 496 | token.prev = prev; | |
| 497 | token.parens = state.parens; | |
| 498 | token.output = state.output; | |
| 499 | token.startIndex = state.index; | |
| 500 | token.tokensIndex = tokens.length; | |
| 501 | const output = (opts.capture ? '(' : '') + token.open; | |
| 502 | ||
| 503 | increment('parens'); | |
| 504 | push({ type, value, output: state.output ? '' : ONE_CHAR }); | |
| 505 | push({ type: 'paren', extglob: true, value: advance(), output }); | |
| 506 | extglobs.push(token); | |
| 507 | }; | |
| 508 | ||
| 509 | const extglobClose = token => { | |
| 510 | const literal = input.slice(token.startIndex, state.index + 1); | |
| 511 | const body = input.slice(token.startIndex + 2, state.index); | |
| 512 | const analysis = analyzeRepeatedExtglob(body, opts); | |
| 513 | ||
| 514 | if ((token.type === 'plus' || token.type === 'star') && analysis.risky) { | |
| 515 | const safeOutput = analysis.safeOutput | |
| 516 | ? (token.output ? '' : ONE_CHAR) + (opts.capture ? `(${analysis.safeOutput})` : analysis.safeOutput) | |
| 517 | : undefined; | |
| 518 | const open = tokens[token.tokensIndex]; | |
| 519 | ||
| 520 | open.type = 'text'; | |
| 521 | open.value = literal; | |
| 522 | open.output = safeOutput || utils.escapeRegex(literal); | |
| 523 | ||
| 524 | for (let i = token.tokensIndex + 1; i < tokens.length; i++) { | |
| 525 | tokens[i].value = ''; | |
| 526 | tokens[i].output = ''; | |
| 527 | delete tokens[i].suffix; | |
| 528 | } | |
| 529 | ||
| 530 | state.output = token.output + open.output; | |
| 531 | state.backtrack = true; | |
| 532 | ||
| 533 | push({ type: 'paren', extglob: true, value, output: '' }); | |
| 534 | decrement('parens'); | |
| 535 | return; | |
| 536 | } | |
| 537 | ||
| 538 | let output = token.close + (opts.capture ? ')' : ''); | |
| 539 | let rest; | |
| 540 | ||
| 541 | if (token.type === 'negate') { | |
| 542 | let extglobStar = star; | |
| 543 | ||
| 544 | if (token.inner && token.inner.length > 1 && token.inner.includes('/')) { | |
| 545 | extglobStar = globstar(opts); | |
| 546 | } | |
| 547 | ||
| 548 | if (extglobStar !== star || eos() || /^\)+$/.test(remaining())) { | |
| 549 | output = token.close = `)$))${extglobStar}`; | |
| 550 | } | |
| 551 | ||
| 552 | if (token.inner.includes('*') && (rest = remaining()) && /^\.[^\\/.]+$/.test(rest)) { | |
| 553 | // Any non-magical string (`.ts`) or even nested expression (`.{ts,tsx}`) can follow after the closing parenthesis. | |
| 554 | // In this case, we need to parse the string and use it in the output of the original pattern. | |
| 555 | // Suitable patterns: `/!(*.d).ts`, `/!(*.d).{ts,tsx}`, `**/!(*-dbg).@(js)`. | |
| 556 | // | |
| 557 | // Disabling the `fastpaths` option due to a problem with parsing strings as `.ts` in the pattern like `**/!(*.d).ts`. | |
| 558 | const expression = parse(rest, { ...options, fastpaths: false }).output; | |
| 559 | ||
| 560 | output = token.close = `)${expression})${extglobStar})`; | |
| 561 | } | |
| 562 | ||
| 563 | if (token.prev.type === 'bos') { | |
| 564 | state.negatedExtglob = true; | |
| 565 | } | |
| 566 | } | |
| 567 | ||
| 568 | push({ type: 'paren', extglob: true, value, output }); | |
| 569 | decrement('parens'); | |
| 570 | }; | |
| 571 | ||
| 572 | /** | |
| 573 | * Fast paths | |
| 574 | */ | |
| 575 | ||
| 576 | if (opts.fastpaths !== false && !/(^[*!]|[/()[\]{}"])/.test(input)) { | |
| 577 | let backslashes = false; | |
| 578 | ||
| 579 | let output = input.replace(REGEX_SPECIAL_CHARS_BACKREF, (m, esc, chars, first, rest, index) => { | |
| 580 | if (first === '\\') { | |
| 581 | backslashes = true; | |
| 582 | return m; | |
| 583 | } | |
| 584 | ||
| 585 | if (first === '?') { | |
| 586 | if (esc) { | |
| 587 | return esc + first + (rest ? QMARK.repeat(rest.length) : ''); | |
| 588 | } | |
| 589 | if (index === 0) { | |
| 590 | return qmarkNoDot + (rest ? QMARK.repeat(rest.length) : ''); | |
| 591 | } | |
| 592 | return QMARK.repeat(chars.length); | |
| 593 | } | |
| 594 | ||
| 595 | if (first === '.') { | |
| 596 | return DOT_LITERAL.repeat(chars.length); | |
| 597 | } | |
| 598 | ||
| 599 | if (first === '*') { | |
| 600 | if (esc) { | |
| 601 | return esc + first + (rest ? star : ''); | |
| 602 | } | |
| 603 | return star; | |
| 604 | } | |
| 605 | return esc ? m : `\\${m}`; | |
| 606 | }); | |
| 607 | ||
| 608 | if (backslashes === true) { | |
| 609 | if (opts.unescape === true) { | |
| 610 | output = output.replace(/\\/g, ''); | |
| 611 | } else { | |
| 612 | output = output.replace(/\\+/g, m => { | |
| 613 | return m.length % 2 === 0 ? '\\\\' : (m ? '\\' : ''); | |
| 614 | }); | |
| 615 | } | |
| 616 | } | |
| 617 | ||
| 618 | if (output === input && opts.contains === true) { | |
| 619 | state.output = input; | |
| 620 | return state; | |
| 621 | } | |
| 622 | ||
| 623 | state.output = utils.wrapOutput(output, state, options); | |
| 624 | return state; | |
| 625 | } | |
| 626 | ||
| 627 | /** | |
| 628 | * Tokenize input until we reach end-of-string | |
| 629 | */ | |
| 630 | ||
| 631 | while (!eos()) { | |
| 632 | value = advance(); | |
| 633 | ||
| 634 | if (value === '\u0000') { | |
| 635 | continue; | |
| 636 | } | |
| 637 | ||
| 638 | /** | |
| 639 | * Escaped characters | |
| 640 | */ | |
| 641 | ||
| 642 | if (value === '\\') { | |
| 643 | const next = peek(); | |
| 644 | ||
| 645 | if (next === '/' && opts.bash !== true) { | |
| 646 | continue; | |
| 647 | } | |
| 648 | ||
| 649 | if (next === '.' || next === ';') { | |
| 650 | continue; | |
| 651 | } | |
| 652 | ||
| 653 | if (!next) { | |
| 654 | value += '\\'; | |
| 655 | push({ type: 'text', value }); | |
| 656 | continue; | |
| 657 | } | |
| 658 | ||
| 659 | // collapse slashes to reduce potential for exploits | |
| 660 | const match = /^\\+/.exec(remaining()); | |
| 661 | let slashes = 0; | |
| 662 | ||
| 663 | if (match && match[0].length > 2) { | |
| 664 | slashes = match[0].length; | |
| 665 | state.index += slashes; | |
| 666 | if (slashes % 2 !== 0) { | |
| 667 | value += '\\'; | |
| 668 | } | |
| 669 | } | |
| 670 | ||
| 671 | if (opts.unescape === true) { | |
| 672 | value = advance(); | |
| 673 | } else { | |
| 674 | value += advance(); | |
| 675 | } | |
| 676 | ||
| 677 | if (state.brackets === 0) { | |
| 678 | push({ type: 'text', value }); | |
| 679 | continue; | |
| 680 | } | |
| 681 | } | |
| 682 | ||
| 683 | /** | |
| 684 | * If we're inside a regex character class, continue | |
| 685 | * until we reach the closing bracket. | |
| 686 | */ | |
| 687 | ||
| 688 | if (state.brackets > 0 && (value !== ']' || prev.value === '[' || prev.value === '[^')) { | |
| 689 | if (opts.posix !== false && value === ':') { | |
| 690 | const inner = prev.value.slice(1); | |
| 691 | if (inner.includes('[')) { | |
| 692 | prev.posix = true; | |
| 693 | ||
| 694 | if (inner.includes(':')) { | |
| 695 | const idx = prev.value.lastIndexOf('['); | |
| 696 | const pre = prev.value.slice(0, idx); | |
| 697 | const rest = prev.value.slice(idx + 2); | |
| 698 | const posix = POSIX_REGEX_SOURCE[rest]; | |
| 699 | if (posix) { | |
| 700 | prev.value = pre + posix; | |
| 701 | state.backtrack = true; | |
| 702 | advance(); | |
| 703 | ||
| 704 | if (!bos.output && tokens.indexOf(prev) === 1) { | |
| 705 | bos.output = ONE_CHAR; | |
| 706 | } | |
| 707 | continue; | |
| 708 | } | |
| 709 | } | |
| 710 | } | |
| 711 | } | |
| 712 | ||
| 713 | if ((value === '[' && peek() !== ':') || (value === '-' && peek() === ']')) { | |
| 714 | value = `\\${value}`; | |
| 715 | } | |
| 716 | ||
| 717 | if (value === ']' && (prev.value === '[' || prev.value === '[^')) { | |
| 718 | value = `\\${value}`; | |
| 719 | } | |
| 720 | ||
| 721 | if (opts.posix === true && value === '!' && prev.value === '[') { | |
| 722 | value = '^'; | |
| 723 | } | |
| 724 | ||
| 725 | prev.value += value; | |
| 726 | append({ value }); | |
| 727 | continue; | |
| 728 | } | |
| 729 | ||
| 730 | /** | |
| 731 | * If we're inside a quoted string, continue | |
| 732 | * until we reach the closing double quote. | |
| 733 | */ | |
| 734 | ||
| 735 | if (state.quotes === 1 && value !== '"') { | |
| 736 | value = utils.escapeRegex(value); | |
| 737 | prev.value += value; | |
| 738 | append({ value }); | |
| 739 | continue; | |
| 740 | } | |
| 741 | ||
| 742 | /** | |
| 743 | * Double quotes | |
| 744 | */ | |
| 745 | ||
| 746 | if (value === '"') { | |
| 747 | state.quotes = state.quotes === 1 ? 0 : 1; | |
| 748 | if (opts.keepQuotes === true) { | |
| 749 | push({ type: 'text', value }); | |
| 750 | } | |
| 751 | continue; | |
| 752 | } | |
| 753 | ||
| 754 | /** | |
| 755 | * Parentheses | |
| 756 | */ | |
| 757 | ||
| 758 | if (value === '(') { | |
| 759 | increment('parens'); | |
| 760 | push({ type: 'paren', value }); | |
| 761 | continue; | |
| 762 | } | |
| 763 | ||
| 764 | if (value === ')') { | |
| 765 | if (state.parens === 0 && opts.strictBrackets === true) { | |
| 766 | throw new SyntaxError(syntaxError('opening', '(')); | |
| 767 | } | |
| 768 | ||
| 769 | const extglob = extglobs[extglobs.length - 1]; | |
| 770 | if (extglob && state.parens === extglob.parens + 1) { | |
| 771 | extglobClose(extglobs.pop()); | |
| 772 | continue; | |
| 773 | } | |
| 774 | ||
| 775 | push({ type: 'paren', value, output: state.parens ? ')' : '\\)' }); | |
| 776 | decrement('parens'); | |
| 777 | continue; | |
| 778 | } | |
| 779 | ||
| 780 | /** | |
| 781 | * Square brackets | |
| 782 | */ | |
| 783 | ||
| 784 | if (value === '[') { | |
| 785 | if (opts.nobracket === true || !remaining().includes(']')) { | |
| 786 | if (opts.nobracket !== true && opts.strictBrackets === true) { | |
| 787 | throw new SyntaxError(syntaxError('closing', ']')); | |
| 788 | } | |
| 789 | ||
| 790 | value = `\\${value}`; | |
| 791 | } else { | |
| 792 | increment('brackets'); | |
| 793 | } | |
| 794 | ||
| 795 | push({ type: 'bracket', value }); | |
| 796 | continue; | |
| 797 | } | |
| 798 | ||
| 799 | if (value === ']') { | |
| 800 | if (opts.nobracket === true || (prev && prev.type === 'bracket' && prev.value.length === 1)) { | |
| 801 | push({ type: 'text', value, output: `\\${value}` }); | |
| 802 | continue; | |
| 803 | } | |
| 804 | ||
| 805 | if (state.brackets === 0) { | |
| 806 | if (opts.strictBrackets === true) { | |
| 807 | throw new SyntaxError(syntaxError('opening', '[')); | |
| 808 | } | |
| 809 | ||
| 810 | push({ type: 'text', value, output: `\\${value}` }); | |
| 811 | continue; | |
| 812 | } | |
| 813 | ||
| 814 | decrement('brackets'); | |
| 815 | ||
| 816 | const prevValue = prev.value.slice(1); | |
| 817 | if (prev.posix !== true && prevValue[0] === '^' && !prevValue.includes('/')) { | |
| 818 | value = `/${value}`; | |
| 819 | } | |
| 820 | ||
| 821 | prev.value += value; | |
| 822 | append({ value }); | |
| 823 | ||
| 824 | // when literal brackets are explicitly disabled | |
| 825 | // assume we should match with a regex character class | |
| 826 | if (opts.literalBrackets === false || utils.hasRegexChars(prevValue)) { | |
| 827 | continue; | |
| 828 | } | |
| 829 | ||
| 830 | const escaped = utils.escapeRegex(prev.value); | |
| 831 | state.output = state.output.slice(0, -prev.value.length); | |
| 832 | ||
| 833 | // when literal brackets are explicitly enabled | |
| 834 | // assume we should escape the brackets to match literal characters | |
| 835 | if (opts.literalBrackets === true) { | |
| 836 | state.output += escaped; | |
| 837 | prev.value = escaped; | |
| 838 | continue; | |
| 839 | } | |
| 840 | ||
| 841 | // when the user specifies nothing, try to match both | |
| 842 | prev.value = `(${capture}${escaped}|${prev.value})`; | |
| 843 | state.output += prev.value; | |
| 844 | continue; | |
| 845 | } | |
| 846 | ||
| 847 | /** | |
| 848 | * Braces | |
| 849 | */ | |
| 850 | ||
| 851 | if (value === '{' && opts.nobrace !== true) { | |
| 852 | increment('braces'); | |
| 853 | ||
| 854 | const open = { | |
| 855 | type: 'brace', | |
| 856 | value, | |
| 857 | output: '(', | |
| 858 | outputIndex: state.output.length, | |
| 859 | tokensIndex: state.tokens.length | |
| 860 | }; | |
| 861 | ||
| 862 | braces.push(open); | |
| 863 | push(open); | |
| 864 | continue; | |
| 865 | } | |
| 866 | ||
| 867 | if (value === '}') { | |
| 868 | const brace = braces[braces.length - 1]; | |
| 869 | ||
| 870 | if (opts.nobrace === true || !brace) { | |
| 871 | push({ type: 'text', value, output: value }); | |
| 872 | continue; | |
| 873 | } | |
| 874 | ||
| 875 | let output = ')'; | |
| 876 | ||
| 877 | if (brace.dots === true) { | |
| 878 | const arr = tokens.slice(); | |
| 879 | const range = []; | |
| 880 | ||
| 881 | for (let i = arr.length - 1; i >= 0; i--) { | |
| 882 | tokens.pop(); | |
| 883 | if (arr[i].type === 'brace') { | |
| 884 | break; | |
| 885 | } | |
| 886 | if (arr[i].type !== 'dots') { | |
| 887 | range.unshift(arr[i].value); | |
| 888 | } | |
| 889 | } | |
| 890 | ||
| 891 | output = expandRange(range, opts); | |
| 892 | state.backtrack = true; | |
| 893 | } | |
| 894 | ||
| 895 | if (brace.comma !== true && brace.dots !== true) { | |
| 896 | const out = state.output.slice(0, brace.outputIndex); | |
| 897 | const toks = state.tokens.slice(brace.tokensIndex); | |
| 898 | brace.value = brace.output = '\\{'; | |
| 899 | value = output = '\\}'; | |
| 900 | state.output = out; | |
| 901 | for (const t of toks) { | |
| 902 | state.output += (t.output || t.value); | |
| 903 | } | |
| 904 | } | |
| 905 | ||
| 906 | push({ type: 'brace', value, output }); | |
| 907 | decrement('braces'); | |
| 908 | braces.pop(); | |
| 909 | continue; | |
| 910 | } | |
| 911 | ||
| 912 | /** | |
| 913 | * Pipes | |
| 914 | */ | |
| 915 | ||
| 916 | if (value === '|') { | |
| 917 | if (extglobs.length > 0) { | |
| 918 | extglobs[extglobs.length - 1].conditions++; | |
| 919 | } | |
| 920 | push({ type: 'text', value }); | |
| 921 | continue; | |
| 922 | } | |
| 923 | ||
| 924 | /** | |
| 925 | * Commas | |
| 926 | */ | |
| 927 | ||
| 928 | if (value === ',') { | |
| 929 | let output = value; | |
| 930 | ||
| 931 | const brace = braces[braces.length - 1]; | |
| 932 | if (brace && stack[stack.length - 1] === 'braces') { | |
| 933 | brace.comma = true; | |
| 934 | output = '|'; | |
| 935 | } | |
| 936 | ||
| 937 | push({ type: 'comma', value, output }); | |
| 938 | continue; | |
| 939 | } | |
| 940 | ||
| 941 | /** | |
| 942 | * Slashes | |
| 943 | */ | |
| 944 | ||
| 945 | if (value === '/') { | |
| 946 | // if the beginning of the glob is "./", advance the start | |
| 947 | // to the current index, and don't add the "./" characters | |
| 948 | // to the state. This greatly simplifies lookbehinds when | |
| 949 | // checking for BOS characters like "!" and "." (not "./") | |
| 950 | if (prev.type === 'dot' && state.index === state.start + 1) { | |
| 951 | state.start = state.index + 1; | |
| 952 | state.consumed = ''; | |
| 953 | state.output = ''; | |
| 954 | tokens.pop(); | |
| 955 | prev = bos; // reset "prev" to the first token | |
| 956 | continue; | |
| 957 | } | |
| 958 | ||
| 959 | push({ type: 'slash', value, output: SLASH_LITERAL }); | |
| 960 | continue; | |
| 961 | } | |
| 962 | ||
| 963 | /** | |
| 964 | * Dots | |
| 965 | */ | |
| 966 | ||
| 967 | if (value === '.') { | |
| 968 | if (state.braces > 0 && prev.type === 'dot') { | |
| 969 | if (prev.value === '.') prev.output = DOT_LITERAL; | |
| 970 | const brace = braces[braces.length - 1]; | |
| 971 | prev.type = 'dots'; | |
| 972 | prev.output += value; | |
| 973 | prev.value += value; | |
| 974 | brace.dots = true; | |
| 975 | continue; | |
| 976 | } | |
| 977 | ||
| 978 | if ((state.braces + state.parens) === 0 && prev.type !== 'bos' && prev.type !== 'slash') { | |
| 979 | push({ type: 'text', value, output: DOT_LITERAL }); | |
| 980 | continue; | |
| 981 | } | |
| 982 | ||
| 983 | push({ type: 'dot', value, output: DOT_LITERAL }); | |
| 984 | continue; | |
| 985 | } | |
| 986 | ||
| 987 | /** | |
| 988 | * Question marks | |
| 989 | */ | |
| 990 | ||
| 991 | if (value === '?') { | |
| 992 | const isGroup = prev && prev.value === '('; | |
| 993 | if (!isGroup && opts.noextglob !== true && peek() === '(' && peek(2) !== '?') { | |
| 994 | extglobOpen('qmark', value); | |
| 995 | continue; | |
| 996 | } | |
| 997 | ||
| 998 | if (prev && prev.type === 'paren') { | |
| 999 | const next = peek(); | |
| 1000 | let output = value; | |
| 1001 | ||
| 1002 | if ((prev.value === '(' && !/[!=<:]/.test(next)) || (next === '<' && !/<([!=]|\w+>)/.test(remaining()))) { | |
| 1003 | output = `\\${value}`; | |
| 1004 | } | |
| 1005 | ||
| 1006 | push({ type: 'text', value, output }); | |
| 1007 | continue; | |
| 1008 | } | |
| 1009 | ||
| 1010 | if (opts.dot !== true && (prev.type === 'slash' || prev.type === 'bos')) { | |
| 1011 | push({ type: 'qmark', value, output: QMARK_NO_DOT }); | |
| 1012 | continue; | |
| 1013 | } | |
| 1014 | ||
| 1015 | push({ type: 'qmark', value, output: QMARK }); | |
| 1016 | continue; | |
| 1017 | } | |
| 1018 | ||
| 1019 | /** | |
| 1020 | * Exclamation | |
| 1021 | */ | |
| 1022 | ||
| 1023 | if (value === '!') { | |
| 1024 | if (opts.noextglob !== true && peek() === '(') { | |
| 1025 | if (peek(2) !== '?' || !/[!=<:]/.test(peek(3))) { | |
| 1026 | extglobOpen('negate', value); | |
| 1027 | continue; | |
| 1028 | } | |
| 1029 | } | |
| 1030 | ||
| 1031 | if (opts.nonegate !== true && state.index === 0) { | |
| 1032 | negate(); | |
| 1033 | continue; | |
| 1034 | } | |
| 1035 | } | |
| 1036 | ||
| 1037 | /** | |
| 1038 | * Plus | |
| 1039 | */ | |
| 1040 | ||
| 1041 | if (value === '+') { | |
| 1042 | if (opts.noextglob !== true && peek() === '(' && peek(2) !== '?') { | |
| 1043 | extglobOpen('plus', value); | |
| 1044 | continue; | |
| 1045 | } | |
| 1046 | ||
| 1047 | if ((prev && prev.value === '(') || opts.regex === false) { | |
| 1048 | push({ type: 'plus', value, output: PLUS_LITERAL }); | |
| 1049 | continue; | |
| 1050 | } | |
| 1051 | ||
| 1052 | if ((prev && (prev.type === 'bracket' || prev.type === 'paren' || prev.type === 'brace')) || state.parens > 0) { | |
| 1053 | push({ type: 'plus', value }); | |
| 1054 | continue; | |
| 1055 | } | |
| 1056 | ||
| 1057 | push({ type: 'plus', value: PLUS_LITERAL }); | |
| 1058 | continue; | |
| 1059 | } | |
| 1060 | ||
| 1061 | /** | |
| 1062 | * Plain text | |
| 1063 | */ | |
| 1064 | ||
| 1065 | if (value === '@') { | |
| 1066 | if (opts.noextglob !== true && peek() === '(' && peek(2) !== '?') { | |
| 1067 | push({ type: 'at', extglob: true, value, output: '' }); | |
| 1068 | continue; | |
| 1069 | } | |
| 1070 | ||
| 1071 | push({ type: 'text', value }); | |
| 1072 | continue; | |
| 1073 | } | |
| 1074 | ||
| 1075 | /** | |
| 1076 | * Plain text | |
| 1077 | */ | |
| 1078 | ||
| 1079 | if (value !== '*') { | |
| 1080 | if (value === '$' || value === '^') { | |
| 1081 | value = `\\${value}`; | |
| 1082 | } | |
| 1083 | ||
| 1084 | const match = REGEX_NON_SPECIAL_CHARS.exec(remaining()); | |
| 1085 | if (match) { | |
| 1086 | value += match[0]; | |
| 1087 | state.index += match[0].length; | |
| 1088 | } | |
| 1089 | ||
| 1090 | push({ type: 'text', value }); | |
| 1091 | continue; | |
| 1092 | } | |
| 1093 | ||
| 1094 | /** | |
| 1095 | * Stars | |
| 1096 | */ | |
| 1097 | ||
| 1098 | if (prev && (prev.type === 'globstar' || prev.star === true)) { | |
| 1099 | prev.type = 'star'; | |
| 1100 | prev.star = true; | |
| 1101 | prev.value += value; | |
| 1102 | prev.output = star; | |
| 1103 | state.backtrack = true; | |
| 1104 | state.globstar = true; | |
| 1105 | consume(value); | |
| 1106 | continue; | |
| 1107 | } | |
| 1108 | ||
| 1109 | let rest = remaining(); | |
| 1110 | if (opts.noextglob !== true && /^\([^?]/.test(rest)) { | |
| 1111 | extglobOpen('star', value); | |
| 1112 | continue; | |
| 1113 | } | |
| 1114 | ||
| 1115 | if (prev.type === 'star') { | |
| 1116 | if (opts.noglobstar === true) { | |
| 1117 | consume(value); | |
| 1118 | continue; | |
| 1119 | } | |
| 1120 | ||
| 1121 | const prior = prev.prev; | |
| 1122 | const before = prior.prev; | |
| 1123 | const isStart = prior.type === 'slash' || prior.type === 'bos'; | |
| 1124 | const afterStar = before && (before.type === 'star' || before.type === 'globstar'); | |
| 1125 | ||
| 1126 | if (opts.bash === true && (!isStart || (rest[0] && rest[0] !== '/'))) { | |
| 1127 | push({ type: 'star', value, output: '' }); | |
| 1128 | continue; | |
| 1129 | } | |
| 1130 | ||
| 1131 | const isBrace = state.braces > 0 && (prior.type === 'comma' || prior.type === 'brace'); | |
| 1132 | const isExtglob = extglobs.length && (prior.type === 'pipe' || prior.type === 'paren'); | |
| 1133 | if (!isStart && prior.type !== 'paren' && !isBrace && !isExtglob) { | |
| 1134 | push({ type: 'star', value, output: '' }); | |
| 1135 | continue; | |
| 1136 | } | |
| 1137 | ||
| 1138 | // strip consecutive `/**/` | |
| 1139 | while (rest.slice(0, 3) === '/**') { | |
| 1140 | const after = input[state.index + 4]; | |
| 1141 | if (after && after !== '/') { | |
| 1142 | break; | |
| 1143 | } | |
| 1144 | rest = rest.slice(3); | |
| 1145 | consume('/**', 3); | |
| 1146 | } | |
| 1147 | ||
| 1148 | if (prior.type === 'bos' && eos()) { | |
| 1149 | prev.type = 'globstar'; | |
| 1150 | prev.value += value; | |
| 1151 | prev.output = globstar(opts); | |
| 1152 | state.output = prev.output; | |
| 1153 | state.globstar = true; | |
| 1154 | consume(value); | |
| 1155 | continue; | |
| 1156 | } | |
| 1157 | ||
| 1158 | if (prior.type === 'slash' && prior.prev.type !== 'bos' && !afterStar && eos()) { | |
| 1159 | state.output = state.output.slice(0, -(prior.output + prev.output).length); | |
| 1160 | prior.output = `(?:${prior.output}`; | |
| 1161 | ||
| 1162 | prev.type = 'globstar'; | |
| 1163 | prev.output = globstar(opts) + (opts.strictSlashes ? ')' : '|$)'); | |
| 1164 | prev.value += value; | |
| 1165 | state.globstar = true; | |
| 1166 | state.output += prior.output + prev.output; | |
| 1167 | consume(value); | |
| 1168 | continue; | |
| 1169 | } | |
| 1170 | ||
| 1171 | if (prior.type === 'slash' && prior.prev.type !== 'bos' && rest[0] === '/') { | |
| 1172 | const end = rest[1] !== void 0 ? '|$' : ''; | |
| 1173 | ||
| 1174 | state.output = state.output.slice(0, -(prior.output + prev.output).length); | |
| 1175 | prior.output = `(?:${prior.output}`; | |
| 1176 | ||
| 1177 | prev.type = 'globstar'; | |
| 1178 | prev.output = `${globstar(opts)}${SLASH_LITERAL}|${SLASH_LITERAL}${end})`; | |
| 1179 | prev.value += value; | |
| 1180 | ||
| 1181 | state.output += prior.output + prev.output; | |
| 1182 | state.globstar = true; | |
| 1183 | ||
| 1184 | consume(value + advance()); | |
| 1185 | ||
| 1186 | push({ type: 'slash', value: '/', output: '' }); | |
| 1187 | continue; | |
| 1188 | } | |
| 1189 | ||
| 1190 | if (prior.type === 'bos' && rest[0] === '/') { | |
| 1191 | prev.type = 'globstar'; | |
| 1192 | prev.value += value; | |
| 1193 | prev.output = `(?:^|${SLASH_LITERAL}|${globstar(opts)}${SLASH_LITERAL})`; | |
| 1194 | state.output = prev.output; | |
| 1195 | state.globstar = true; | |
| 1196 | consume(value + advance()); | |
| 1197 | push({ type: 'slash', value: '/', output: '' }); | |
| 1198 | continue; | |
| 1199 | } | |
| 1200 | ||
| 1201 | // remove single star from output | |
| 1202 | state.output = state.output.slice(0, -prev.output.length); | |
| 1203 | ||
| 1204 | // reset previous token to globstar | |
| 1205 | prev.type = 'globstar'; | |
| 1206 | prev.output = globstar(opts); | |
| 1207 | prev.value += value; | |
| 1208 | ||
| 1209 | // reset output with globstar | |
| 1210 | state.output += prev.output; | |
| 1211 | state.globstar = true; | |
| 1212 | consume(value); | |
| 1213 | continue; | |
| 1214 | } | |
| 1215 | ||
| 1216 | const token = { type: 'star', value, output: star }; | |
| 1217 | ||
| 1218 | if (opts.bash === true) { | |
| 1219 | token.output = '.*?'; | |
| 1220 | if (prev.type === 'bos' || prev.type === 'slash') { | |
| 1221 | token.output = nodot + token.output; | |
| 1222 | } | |
| 1223 | push(token); | |
| 1224 | continue; | |
| 1225 | } | |
| 1226 | ||
| 1227 | if (prev && (prev.type === 'bracket' || prev.type === 'paren') && opts.regex === true) { | |
| 1228 | token.output = value; | |
| 1229 | push(token); | |
| 1230 | continue; | |
| 1231 | } | |
| 1232 | ||
| 1233 | if (state.index === state.start || prev.type === 'slash' || prev.type === 'dot') { | |
| 1234 | if (prev.type === 'dot') { | |
| 1235 | state.output += NO_DOT_SLASH; | |
| 1236 | prev.output += NO_DOT_SLASH; | |
| 1237 | ||
| 1238 | } else if (opts.dot === true) { | |
| 1239 | state.output += NO_DOTS_SLASH; | |
| 1240 | prev.output += NO_DOTS_SLASH; | |
| 1241 | ||
| 1242 | } else { | |
| 1243 | state.output += nodot; | |
| 1244 | prev.output += nodot; | |
| 1245 | } | |
| 1246 | ||
| 1247 | if (peek() !== '*') { | |
| 1248 | state.output += ONE_CHAR; | |
| 1249 | prev.output += ONE_CHAR; | |
| 1250 | } | |
| 1251 | } | |
| 1252 | ||
| 1253 | push(token); | |
| 1254 | } | |
| 1255 | ||
| 1256 | while (state.brackets > 0) { | |
| 1257 | if (opts.strictBrackets === true) throw new SyntaxError(syntaxError('closing', ']')); | |
| 1258 | state.output = utils.escapeLast(state.output, '['); | |
| 1259 | decrement('brackets'); | |
| 1260 | } | |
| 1261 | ||
| 1262 | while (state.parens > 0) { | |
| 1263 | if (opts.strictBrackets === true) throw new SyntaxError(syntaxError('closing', ')')); | |
| 1264 | state.output = utils.escapeLast(state.output, '('); | |
| 1265 | decrement('parens'); | |
| 1266 | } | |
| 1267 | ||
| 1268 | while (state.braces > 0) { | |
| 1269 | if (opts.strictBrackets === true) throw new SyntaxError(syntaxError('closing', '}')); | |
| 1270 | state.output = utils.escapeLast(state.output, '{'); | |
| 1271 | decrement('braces'); | |
| 1272 | } | |
| 1273 | ||
| 1274 | if (opts.strictSlashes !== true && (prev.type === 'star' || prev.type === 'bracket')) { | |
| 1275 | push({ type: 'maybe_slash', value: '', output: `${SLASH_LITERAL}?` }); | |
| 1276 | } | |
| 1277 | ||
| 1278 | // rebuild the output if we had to backtrack at any point | |
| 1279 | if (state.backtrack === true) { | |
| 1280 | state.output = ''; | |
| 1281 | ||
| 1282 | for (const token of state.tokens) { | |
| 1283 | state.output += token.output != null ? token.output : token.value; | |
| 1284 | ||
| 1285 | if (token.suffix) { | |
| 1286 | state.output += token.suffix; | |
| 1287 | } | |
| 1288 | } | |
| 1289 | } | |
| 1290 | ||
| 1291 | return state; | |
| 1292 | }; | |
| 1293 | ||
| 1294 | /** | |
| 1295 | * Fast paths for creating regular expressions for common glob patterns. | |
| 1296 | * This can significantly speed up processing and has very little downside | |
| 1297 | * impact when none of the fast paths match. | |
| 1298 | */ | |
| 1299 | ||
| 1300 | parse.fastpaths = (input, options) => { | |
| 1301 | const opts = { ...options }; | |
| 1302 | const max = typeof opts.maxLength === 'number' ? Math.min(MAX_LENGTH, opts.maxLength) : MAX_LENGTH; | |
| 1303 | const len = input.length; | |
| 1304 | if (len > max) { | |
| 1305 | throw new SyntaxError(`Input length: ${len}, exceeds maximum allowed length: ${max}`); | |
| 1306 | } | |
| 1307 | ||
| 1308 | input = REPLACEMENTS[input] || input; | |
| 1309 | ||
| 1310 | // create constants based on platform, for windows or posix | |
| 1311 | const { | |
| 1312 | DOT_LITERAL, | |
| 1313 | SLASH_LITERAL, | |
| 1314 | ONE_CHAR, | |
| 1315 | DOTS_SLASH, | |
| 1316 | NO_DOT, | |
| 1317 | NO_DOTS, | |
| 1318 | NO_DOTS_SLASH, | |
| 1319 | STAR, | |
| 1320 | START_ANCHOR | |
| 1321 | } = constants.globChars(opts.windows); | |
| 1322 | ||
| 1323 | const nodot = opts.dot ? NO_DOTS : NO_DOT; | |
| 1324 | const slashDot = opts.dot ? NO_DOTS_SLASH : NO_DOT; | |
| 1325 | const capture = opts.capture ? '' : '?:'; | |
| 1326 | const state = { negated: false, prefix: '' }; | |
| 1327 | let star = opts.bash === true ? '.*?' : STAR; | |
| 1328 | ||
| 1329 | if (opts.capture) { | |
| 1330 | star = `(${star})`; | |
| 1331 | } | |
| 1332 | ||
| 1333 | const globstar = opts => { | |
| 1334 | if (opts.noglobstar === true) return star; | |
| 1335 | return `(${capture}(?:(?!${START_ANCHOR}${opts.dot ? DOTS_SLASH : DOT_LITERAL}).)*?)`; | |
| 1336 | }; | |
| 1337 | ||
| 1338 | const create = str => { | |
| 1339 | switch (str) { | |
| 1340 | case '*': | |
| 1341 | return `${nodot}${ONE_CHAR}${star}`; | |
| 1342 | ||
| 1343 | case '.*': | |
| 1344 | return `${DOT_LITERAL}${ONE_CHAR}${star}`; | |
| 1345 | ||
| 1346 | case '*.*': | |
| 1347 | return `${nodot}${star}${DOT_LITERAL}${ONE_CHAR}${star}`; | |
| 1348 | ||
| 1349 | case '*/*': | |
| 1350 | return `${nodot}${star}${SLASH_LITERAL}${ONE_CHAR}${slashDot}${star}`; | |
| 1351 | ||
| 1352 | case '**': | |
| 1353 | return nodot + globstar(opts); | |
| 1354 | ||
| 1355 | case '**/*': | |
| 1356 | return `(?:${nodot}${globstar(opts)}${SLASH_LITERAL})?${slashDot}${ONE_CHAR}${star}`; | |
| 1357 | ||
| 1358 | case '**/*.*': | |
| 1359 | return `(?:${nodot}${globstar(opts)}${SLASH_LITERAL})?${slashDot}${star}${DOT_LITERAL}${ONE_CHAR}${star}`; | |
| 1360 | ||
| 1361 | case '**/.*': | |
| 1362 | return `(?:${nodot}${globstar(opts)}${SLASH_LITERAL})?${DOT_LITERAL}${ONE_CHAR}${star}`; | |
| 1363 | ||
| 1364 | default: { | |
| 1365 | const match = /^(.*?)\.(\w+)$/.exec(str); | |
| 1366 | if (!match) return; | |
| 1367 | ||
| 1368 | const source = create(match[1]); | |
| 1369 | if (!source) return; | |
| 1370 | ||
| 1371 | return source + DOT_LITERAL + match[2]; | |
| 1372 | } | |
| 1373 | } | |
| 1374 | }; | |
| 1375 | ||
| 1376 | const output = utils.removePrefix(input, state); | |
| 1377 | let source = create(output); | |
| 1378 | ||
| 1379 | if (source && opts.strictSlashes !== true) { | |
| 1380 | source += `${SLASH_LITERAL}?`; | |
| 1381 | } | |
| 1382 | ||
| 1383 | return source; | |
| 1384 | }; | |
| 1385 | ||
| 1386 | module.exports = parse; |
| … | … | @@ -0,0 +1,349 @@ |
| 1 | 'use strict'; | |
| 2 | ||
| 3 | const scan = require('./scan'); | |
| 4 | const parse = require('./parse'); | |
| 5 | const utils = require('./utils'); | |
| 6 | const constants = require('./constants'); | |
| 7 | const isObject = val => val && typeof val === 'object' && !Array.isArray(val); | |
| 8 | ||
| 9 | /** | |
| 10 | * Creates a matcher function from one or more glob patterns. The | |
| 11 | * returned function takes a string to match as its first argument, | |
| 12 | * and returns true if the string is a match. The returned matcher | |
| 13 | * function also takes a boolean as the second argument that, when true, | |
| 14 | * returns an object with additional information. | |
| 15 | * | |
| 16 | * ```js | |
| 17 | * const picomatch = require('picomatch'); | |
| 18 | * // picomatch(glob[, options]); | |
| 19 | * | |
| 20 | * const isMatch = picomatch('*.!(*a)'); | |
| 21 | * console.log(isMatch('a.a')); //=> false | |
| 22 | * console.log(isMatch('a.b')); //=> true | |
| 23 | * ``` | |
| 24 | * @name picomatch | |
| 25 | * @param {String|Array} `globs` One or more glob patterns. | |
| 26 | * @param {Object=} `options` | |
| 27 | * @return {Function=} Returns a matcher function. | |
| 28 | * @api public | |
| 29 | */ | |
| 30 | ||
| 31 | const picomatch = (glob, options, returnState = false) => { | |
| 32 | if (Array.isArray(glob)) { | |
| 33 | const fns = glob.map(input => picomatch(input, options, returnState)); | |
| 34 | const arrayMatcher = str => { | |
| 35 | for (const isMatch of fns) { | |
| 36 | const state = isMatch(str); | |
| 37 | if (state) return state; | |
| 38 | } | |
| 39 | return false; | |
| 40 | }; | |
| 41 | return arrayMatcher; | |
| 42 | } | |
| 43 | ||
| 44 | const isState = isObject(glob) && glob.tokens && glob.input; | |
| 45 | ||
| 46 | if (glob === '' || (typeof glob !== 'string' && !isState)) { | |
| 47 | throw new TypeError('Expected pattern to be a non-empty string'); | |
| 48 | } | |
| 49 | ||
| 50 | const opts = options || {}; | |
| 51 | const posix = opts.windows; | |
| 52 | const regex = isState | |
| 53 | ? picomatch.compileRe(glob, options) | |
| 54 | : picomatch.makeRe(glob, options, false, true); | |
| 55 | ||
| 56 | const state = regex.state; | |
| 57 | delete regex.state; | |
| 58 | ||
| 59 | let isIgnored = () => false; | |
| 60 | if (opts.ignore) { | |
| 61 | const ignoreOpts = { ...options, ignore: null, onMatch: null, onResult: null }; | |
| 62 | isIgnored = picomatch(opts.ignore, ignoreOpts, returnState); | |
| 63 | } | |
| 64 | ||
| 65 | const matcher = (input, returnObject = false) => { | |
| 66 | const { isMatch, match, output } = picomatch.test(input, regex, options, { glob, posix }); | |
| 67 | const result = { glob, state, regex, posix, input, output, match, isMatch }; | |
| 68 | ||
| 69 | if (typeof opts.onResult === 'function') { | |
| 70 | opts.onResult(result); | |
| 71 | } | |
| 72 | ||
| 73 | if (isMatch === false) { | |
| 74 | result.isMatch = false; | |
| 75 | return returnObject ? result : false; | |
| 76 | } | |
| 77 | ||
| 78 | if (isIgnored(input)) { | |
| 79 | if (typeof opts.onIgnore === 'function') { | |
| 80 | opts.onIgnore(result); | |
| 81 | } | |
| 82 | result.isMatch = false; | |
| 83 | return returnObject ? result : false; | |
| 84 | } | |
| 85 | ||
| 86 | if (typeof opts.onMatch === 'function') { | |
| 87 | opts.onMatch(result); | |
| 88 | } | |
| 89 | return returnObject ? result : true; | |
| 90 | }; | |
| 91 | ||
| 92 | if (returnState) { | |
| 93 | matcher.state = state; | |
| 94 | } | |
| 95 | ||
| 96 | return matcher; | |
| 97 | }; | |
| 98 | ||
| 99 | /** | |
| 100 | * Test `input` with the given `regex`. This is used by the main | |
| 101 | * `picomatch()` function to test the input string. | |
| 102 | * | |
| 103 | * ```js | |
| 104 | * const picomatch = require('picomatch'); | |
| 105 | * // picomatch.test(input, regex[, options]); | |
| 106 | * | |
| 107 | * console.log(picomatch.test('foo/bar', /^(?:([^/]*?)\/([^/]*?))$/)); | |
| 108 | * // { isMatch: true, match: [ 'foo/', 'foo', 'bar' ], output: 'foo/bar' } | |
| 109 | * ``` | |
| 110 | * @param {String} `input` String to test. | |
| 111 | * @param {RegExp} `regex` | |
| 112 | * @return {Object} Returns an object with matching info. | |
| 113 | * @api public | |
| 114 | */ | |
| 115 | ||
| 116 | picomatch.test = (input, regex, options, { glob, posix } = {}) => { | |
| 117 | if (typeof input !== 'string') { | |
| 118 | throw new TypeError('Expected input to be a string'); | |
| 119 | } | |
| 120 | ||
| 121 | if (input === '') { | |
| 122 | return { isMatch: false, output: '' }; | |
| 123 | } | |
| 124 | ||
| 125 | const opts = options || {}; | |
| 126 | const format = opts.format || (posix ? utils.toPosixSlashes : null); | |
| 127 | let match = input === glob; | |
| 128 | let output = (match && format) ? format(input) : input; | |
| 129 | ||
| 130 | if (match === false) { | |
| 131 | output = format ? format(input) : input; | |
| 132 | match = output === glob; | |
| 133 | } | |
| 134 | ||
| 135 | if (match === false || opts.capture === true) { | |
| 136 | if (opts.matchBase === true || opts.basename === true) { | |
| 137 | match = picomatch.matchBase(input, regex, options, posix); | |
| 138 | } else { | |
| 139 | match = regex.exec(output); | |
| 140 | } | |
| 141 | } | |
| 142 | ||
| 143 | return { isMatch: Boolean(match), match, output }; | |
| 144 | }; | |
| 145 | ||
| 146 | /** | |
| 147 | * Match the basename of a filepath. | |
| 148 | * | |
| 149 | * ```js | |
| 150 | * const picomatch = require('picomatch'); | |
| 151 | * // picomatch.matchBase(input, glob[, options]); | |
| 152 | * console.log(picomatch.matchBase('foo/bar.js', '*.js'); // true | |
| 153 | * ``` | |
| 154 | * @param {String} `input` String to test. | |
| 155 | * @param {RegExp|String} `glob` Glob pattern or regex created by [.makeRe](#makeRe). | |
| 156 | * @return {Boolean} | |
| 157 | * @api public | |
| 158 | */ | |
| 159 | ||
| 160 | picomatch.matchBase = (input, glob, options) => { | |
| 161 | const regex = glob instanceof RegExp ? glob : picomatch.makeRe(glob, options); | |
| 162 | return regex.test(utils.basename(input)); | |
| 163 | }; | |
| 164 | ||
| 165 | /** | |
| 166 | * Returns true if **any** of the given glob `patterns` match the specified `string`. | |
| 167 | * | |
| 168 | * ```js | |
| 169 | * const picomatch = require('picomatch'); | |
| 170 | * // picomatch.isMatch(string, patterns[, options]); | |
| 171 | * | |
| 172 | * console.log(picomatch.isMatch('a.a', ['b.*', '*.a'])); //=> true | |
| 173 | * console.log(picomatch.isMatch('a.a', 'b.*')); //=> false | |
| 174 | * ``` | |
| 175 | * @param {String|Array} str The string to test. | |
| 176 | * @param {String|Array} patterns One or more glob patterns to use for matching. | |
| 177 | * @param {Object} [options] See available [options](#options). | |
| 178 | * @return {Boolean} Returns true if any patterns match `str` | |
| 179 | * @api public | |
| 180 | */ | |
| 181 | ||
| 182 | picomatch.isMatch = (str, patterns, options) => picomatch(patterns, options)(str); | |
| 183 | ||
| 184 | /** | |
| 185 | * Parse a glob pattern to create the source string for a regular | |
| 186 | * expression. | |
| 187 | * | |
| 188 | * ```js | |
| 189 | * const picomatch = require('picomatch'); | |
| 190 | * const result = picomatch.parse(pattern[, options]); | |
| 191 | * ``` | |
| 192 | * @param {String} `pattern` | |
| 193 | * @param {Object} `options` | |
| 194 | * @return {Object} Returns an object with useful properties and output to be used as a regex source string. | |
| 195 | * @api public | |
| 196 | */ | |
| 197 | ||
| 198 | picomatch.parse = (pattern, options) => { | |
| 199 | if (Array.isArray(pattern)) return pattern.map(p => picomatch.parse(p, options)); | |
| 200 | return parse(pattern, { ...options, fastpaths: false }); | |
| 201 | }; | |
| 202 | ||
| 203 | /** | |
| 204 | * Scan a glob pattern to separate the pattern into segments. | |
| 205 | * | |
| 206 | * ```js | |
| 207 | * const picomatch = require('picomatch'); | |
| 208 | * // picomatch.scan(input[, options]); | |
| 209 | * | |
| 210 | * const result = picomatch.scan('!./foo/*.js'); | |
| 211 | * console.log(result); | |
| 212 | * { prefix: '!./', | |
| 213 | * input: '!./foo/*.js', | |
| 214 | * start: 3, | |
| 215 | * base: 'foo', | |
| 216 | * glob: '*.js', | |
| 217 | * isBrace: false, | |
| 218 | * isBracket: false, | |
| 219 | * isGlob: true, | |
| 220 | * isExtglob: false, | |
| 221 | * isGlobstar: false, | |
| 222 | * negated: true } | |
| 223 | * ``` | |
| 224 | * @param {String} `input` Glob pattern to scan. | |
| 225 | * @param {Object} `options` | |
| 226 | * @return {Object} Returns an object with | |
| 227 | * @api public | |
| 228 | */ | |
| 229 | ||
| 230 | picomatch.scan = (input, options) => scan(input, options); | |
| 231 | ||
| 232 | /** | |
| 233 | * Compile a regular expression from the `state` object returned by the | |
| 234 | * [parse()](#parse) method. | |
| 235 | * | |
| 236 | * ```js | |
| 237 | * const picomatch = require('picomatch'); | |
| 238 | * const state = picomatch.parse('*.js'); | |
| 239 | * // picomatch.compileRe(state[, options]); | |
| 240 | * | |
| 241 | * console.log(picomatch.compileRe(state)); | |
| 242 | * //=> /^(?:(?!\.)(?=.)[^/]*?\.js)$/ | |
| 243 | * ``` | |
| 244 | * @param {Object} `state` | |
| 245 | * @param {Object} `options` | |
| 246 | * @param {Boolean} `returnOutput` Intended for implementors, this argument allows you to return the raw output from the parser. | |
| 247 | * @param {Boolean} `returnState` Adds the state to a `state` property on the returned regex. Useful for implementors and debugging. | |
| 248 | * @return {RegExp} | |
| 249 | * @api public | |
| 250 | */ | |
| 251 | ||
| 252 | picomatch.compileRe = (state, options, returnOutput = false, returnState = false) => { | |
| 253 | if (returnOutput === true) { | |
| 254 | return state.output; | |
| 255 | } | |
| 256 | ||
| 257 | const opts = options || {}; | |
| 258 | const prepend = opts.contains ? '' : '^'; | |
| 259 | const append = opts.contains ? '' : '$'; | |
| 260 | ||
| 261 | let source = `${prepend}(?:${state.output})${append}`; | |
| 262 | if (state && state.negated === true) { | |
| 263 | source = `^(?!${source}).*$`; | |
| 264 | } | |
| 265 | ||
| 266 | const regex = picomatch.toRegex(source, options); | |
| 267 | if (returnState === true) { | |
| 268 | regex.state = state; | |
| 269 | } | |
| 270 | ||
| 271 | return regex; | |
| 272 | }; | |
| 273 | ||
| 274 | /** | |
| 275 | * Create a regular expression from a parsed glob pattern. | |
| 276 | * | |
| 277 | * ```js | |
| 278 | * const picomatch = require('picomatch'); | |
| 279 | * // picomatch.makeRe(state[, options]); | |
| 280 | * | |
| 281 | * const result = picomatch.makeRe('*.js'); | |
| 282 | * console.log(result); | |
| 283 | * //=> /^(?:(?!\.)(?=.)[^/]*?\.js)$/ | |
| 284 | * ``` | |
| 285 | * @param {String} `state` The object returned from the `.parse` method. | |
| 286 | * @param {Object} `options` | |
| 287 | * @param {Boolean} `returnOutput` Implementors may use this argument to return the compiled output, instead of a regular expression. This is not exposed on the options to prevent end-users from mutating the result. | |
| 288 | * @param {Boolean} `returnState` Implementors may use this argument to return the state from the parsed glob with the returned regular expression. | |
| 289 | * @return {RegExp} Returns a regex created from the given pattern. | |
| 290 | * @api public | |
| 291 | */ | |
| 292 | ||
| 293 | picomatch.makeRe = (input, options = {}, returnOutput = false, returnState = false) => { | |
| 294 | if (!input || typeof input !== 'string') { | |
| 295 | throw new TypeError('Expected a non-empty string'); | |
| 296 | } | |
| 297 | ||
| 298 | let parsed = { negated: false, fastpaths: true }; | |
| 299 | ||
| 300 | if (options.fastpaths !== false && (input[0] === '.' || input[0] === '*')) { | |
| 301 | parsed.output = parse.fastpaths(input, options); | |
| 302 | } | |
| 303 | ||
| 304 | if (!parsed.output) { | |
| 305 | parsed = parse(input, options); | |
| 306 | } | |
| 307 | ||
| 308 | return picomatch.compileRe(parsed, options, returnOutput, returnState); | |
| 309 | }; | |
| 310 | ||
| 311 | /** | |
| 312 | * Create a regular expression from the given regex source string. | |
| 313 | * | |
| 314 | * ```js | |
| 315 | * const picomatch = require('picomatch'); | |
| 316 | * // picomatch.toRegex(source[, options]); | |
| 317 | * | |
| 318 | * const { output } = picomatch.parse('*.js'); | |
| 319 | * console.log(picomatch.toRegex(output)); | |
| 320 | * //=> /^(?:(?!\.)(?=.)[^/]*?\.js)$/ | |
| 321 | * ``` | |
| 322 | * @param {String} `source` Regular expression source string. | |
| 323 | * @param {Object} `options` | |
| 324 | * @return {RegExp} | |
| 325 | * @api public | |
| 326 | */ | |
| 327 | ||
| 328 | picomatch.toRegex = (source, options) => { | |
| 329 | try { | |
| 330 | const opts = options || {}; | |
| 331 | return new RegExp(source, opts.flags || (opts.nocase ? 'i' : '')); | |
| 332 | } catch (err) { | |
| 333 | if (options && options.debug === true) throw err; | |
| 334 | return /$^/; | |
| 335 | } | |
| 336 | }; | |
| 337 | ||
| 338 | /** | |
| 339 | * Picomatch constants. | |
| 340 | * @return {Object} | |
| 341 | */ | |
| 342 | ||
| 343 | picomatch.constants = constants; | |
| 344 | ||
| 345 | /** | |
| 346 | * Expose "picomatch" | |
| 347 | */ | |
| 348 | ||
| 349 | module.exports = picomatch; |
| … | … | @@ -0,0 +1,391 @@ |
| 1 | 'use strict'; | |
| 2 | ||
| 3 | const utils = require('./utils'); | |
| 4 | const { | |
| 5 | CHAR_ASTERISK, /* * */ | |
| 6 | CHAR_AT, /* @ */ | |
| 7 | CHAR_BACKWARD_SLASH, /* \ */ | |
| 8 | CHAR_COMMA, /* , */ | |
| 9 | CHAR_DOT, /* . */ | |
| 10 | CHAR_EXCLAMATION_MARK, /* ! */ | |
| 11 | CHAR_FORWARD_SLASH, /* / */ | |
| 12 | CHAR_LEFT_CURLY_BRACE, /* { */ | |
| 13 | CHAR_LEFT_PARENTHESES, /* ( */ | |
| 14 | CHAR_LEFT_SQUARE_BRACKET, /* [ */ | |
| 15 | CHAR_PLUS, /* + */ | |
| 16 | CHAR_QUESTION_MARK, /* ? */ | |
| 17 | CHAR_RIGHT_CURLY_BRACE, /* } */ | |
| 18 | CHAR_RIGHT_PARENTHESES, /* ) */ | |
| 19 | CHAR_RIGHT_SQUARE_BRACKET /* ] */ | |
| 20 | } = require('./constants'); | |
| 21 | ||
| 22 | const isPathSeparator = code => { | |
| 23 | return code === CHAR_FORWARD_SLASH || code === CHAR_BACKWARD_SLASH; | |
| 24 | }; | |
| 25 | ||
| 26 | const depth = token => { | |
| 27 | if (token.isPrefix !== true) { | |
| 28 | token.depth = token.isGlobstar ? Infinity : 1; | |
| 29 | } | |
| 30 | }; | |
| 31 | ||
| 32 | /** | |
| 33 | * Quickly scans a glob pattern and returns an object with a handful of | |
| 34 | * useful properties, like `isGlob`, `path` (the leading non-glob, if it exists), | |
| 35 | * `glob` (the actual pattern), `negated` (true if the path starts with `!` but not | |
| 36 | * with `!(`) and `negatedExtglob` (true if the path starts with `!(`). | |
| 37 | * | |
| 38 | * ```js | |
| 39 | * const pm = require('picomatch'); | |
| 40 | * console.log(pm.scan('foo/bar/*.js')); | |
| 41 | * { isGlob: true, input: 'foo/bar/*.js', base: 'foo/bar', glob: '*.js' } | |
| 42 | * ``` | |
| 43 | * @param {String} `str` | |
| 44 | * @param {Object} `options` | |
| 45 | * @return {Object} Returns an object with tokens and regex source string. | |
| 46 | * @api public | |
| 47 | */ | |
| 48 | ||
| 49 | const scan = (input, options) => { | |
| 50 | const opts = options || {}; | |
| 51 | ||
| 52 | const length = input.length - 1; | |
| 53 | const scanToEnd = opts.parts === true || opts.scanToEnd === true; | |
| 54 | const slashes = []; | |
| 55 | const tokens = []; | |
| 56 | const parts = []; | |
| 57 | ||
| 58 | let str = input; | |
| 59 | let index = -1; | |
| 60 | let start = 0; | |
| 61 | let lastIndex = 0; | |
| 62 | let isBrace = false; | |
| 63 | let isBracket = false; | |
| 64 | let isGlob = false; | |
| 65 | let isExtglob = false; | |
| 66 | let isGlobstar = false; | |
| 67 | let braceEscaped = false; | |
| 68 | let backslashes = false; | |
| 69 | let negated = false; | |
| 70 | let negatedExtglob = false; | |
| 71 | let finished = false; | |
| 72 | let braces = 0; | |
| 73 | let prev; | |
| 74 | let code; | |
| 75 | let token = { value: '', depth: 0, isGlob: false }; | |
| 76 | ||
| 77 | const eos = () => index >= length; | |
| 78 | const peek = () => str.charCodeAt(index + 1); | |
| 79 | const advance = () => { | |
| 80 | prev = code; | |
| 81 | return str.charCodeAt(++index); | |
| 82 | }; | |
| 83 | ||
| 84 | while (index < length) { | |
| 85 | code = advance(); | |
| 86 | let next; | |
| 87 | ||
| 88 | if (code === CHAR_BACKWARD_SLASH) { | |
| 89 | backslashes = token.backslashes = true; | |
| 90 | code = advance(); | |
| 91 | ||
| 92 | if (code === CHAR_LEFT_CURLY_BRACE) { | |
| 93 | braceEscaped = true; | |
| 94 | } | |
| 95 | continue; | |
| 96 | } | |
| 97 | ||
| 98 | if (braceEscaped === true || code === CHAR_LEFT_CURLY_BRACE) { | |
| 99 | braces++; | |
| 100 | ||
| 101 | while (eos() !== true && (code = advance())) { | |
| 102 | if (code === CHAR_BACKWARD_SLASH) { | |
| 103 | backslashes = token.backslashes = true; | |
| 104 | advance(); | |
| 105 | continue; | |
| 106 | } | |
| 107 | ||
| 108 | if (code === CHAR_LEFT_CURLY_BRACE) { | |
| 109 | braces++; | |
| 110 | continue; | |
| 111 | } | |
| 112 | ||
| 113 | if (braceEscaped !== true && code === CHAR_DOT && (code = advance()) === CHAR_DOT) { | |
| 114 | isBrace = token.isBrace = true; | |
| 115 | isGlob = token.isGlob = true; | |
| 116 | finished = true; | |
| 117 | ||
| 118 | if (scanToEnd === true) { | |
| 119 | continue; | |
| 120 | } | |
| 121 | ||
| 122 | break; | |
| 123 | } | |
| 124 | ||
| 125 | if (braceEscaped !== true && code === CHAR_COMMA) { | |
| 126 | isBrace = token.isBrace = true; | |
| 127 | isGlob = token.isGlob = true; | |
| 128 | finished = true; | |
| 129 | ||
| 130 | if (scanToEnd === true) { | |
| 131 | continue; | |
| 132 | } | |
| 133 | ||
| 134 | break; | |
| 135 | } | |
| 136 | ||
| 137 | if (code === CHAR_RIGHT_CURLY_BRACE) { | |
| 138 | braces--; | |
| 139 | ||
| 140 | if (braces === 0) { | |
| 141 | braceEscaped = false; | |
| 142 | isBrace = token.isBrace = true; | |
| 143 | finished = true; | |
| 144 | break; | |
| 145 | } | |
| 146 | } | |
| 147 | } | |
| 148 | ||
| 149 | if (scanToEnd === true) { | |
| 150 | continue; | |
| 151 | } | |
| 152 | ||
| 153 | break; | |
| 154 | } | |
| 155 | ||
| 156 | if (code === CHAR_FORWARD_SLASH) { | |
| 157 | slashes.push(index); | |
| 158 | tokens.push(token); | |
| 159 | token = { value: '', depth: 0, isGlob: false }; | |
| 160 | ||
| 161 | if (finished === true) continue; | |
| 162 | if (prev === CHAR_DOT && index === (start + 1)) { | |
| 163 | start += 2; | |
| 164 | continue; | |
| 165 | } | |
| 166 | ||
| 167 | lastIndex = index + 1; | |
| 168 | continue; | |
| 169 | } | |
| 170 | ||
| 171 | if (opts.noext !== true) { | |
| 172 | const isExtglobChar = code === CHAR_PLUS | |
| 173 | || code === CHAR_AT | |
| 174 | || code === CHAR_ASTERISK | |
| 175 | || code === CHAR_QUESTION_MARK | |
| 176 | || code === CHAR_EXCLAMATION_MARK; | |
| 177 | ||
| 178 | if (isExtglobChar === true && peek() === CHAR_LEFT_PARENTHESES) { | |
| 179 | isGlob = token.isGlob = true; | |
| 180 | isExtglob = token.isExtglob = true; | |
| 181 | finished = true; | |
| 182 | if (code === CHAR_EXCLAMATION_MARK && index === start) { | |
| 183 | negatedExtglob = true; | |
| 184 | } | |
| 185 | ||
| 186 | if (scanToEnd === true) { | |
| 187 | while (eos() !== true && (code = advance())) { | |
| 188 | if (code === CHAR_BACKWARD_SLASH) { | |
| 189 | backslashes = token.backslashes = true; | |
| 190 | code = advance(); | |
| 191 | continue; | |
| 192 | } | |
| 193 | ||
| 194 | if (code === CHAR_RIGHT_PARENTHESES) { | |
| 195 | isGlob = token.isGlob = true; | |
| 196 | finished = true; | |
| 197 | break; | |
| 198 | } | |
| 199 | } | |
| 200 | continue; | |
| 201 | } | |
| 202 | break; | |
| 203 | } | |
| 204 | } | |
| 205 | ||
| 206 | if (code === CHAR_ASTERISK) { | |
| 207 | if (prev === CHAR_ASTERISK) isGlobstar = token.isGlobstar = true; | |
| 208 | isGlob = token.isGlob = true; | |
| 209 | finished = true; | |
| 210 | ||
| 211 | if (scanToEnd === true) { | |
| 212 | continue; | |
| 213 | } | |
| 214 | break; | |
| 215 | } | |
| 216 | ||
| 217 | if (code === CHAR_QUESTION_MARK) { | |
| 218 | isGlob = token.isGlob = true; | |
| 219 | finished = true; | |
| 220 | ||
| 221 | if (scanToEnd === true) { | |
| 222 | continue; | |
| 223 | } | |
| 224 | break; | |
| 225 | } | |
| 226 | ||
| 227 | if (code === CHAR_LEFT_SQUARE_BRACKET) { | |
| 228 | while (eos() !== true && (next = advance())) { | |
| 229 | if (next === CHAR_BACKWARD_SLASH) { | |
| 230 | backslashes = token.backslashes = true; | |
| 231 | advance(); | |
| 232 | continue; | |
| 233 | } | |
| 234 | ||
| 235 | if (next === CHAR_RIGHT_SQUARE_BRACKET) { | |
| 236 | isBracket = token.isBracket = true; | |
| 237 | isGlob = token.isGlob = true; | |
| 238 | finished = true; | |
| 239 | break; | |
| 240 | } | |
| 241 | } | |
| 242 | ||
| 243 | if (scanToEnd === true) { | |
| 244 | continue; | |
| 245 | } | |
| 246 | ||
| 247 | break; | |
| 248 | } | |
| 249 | ||
| 250 | if (opts.nonegate !== true && code === CHAR_EXCLAMATION_MARK && index === start) { | |
| 251 | negated = token.negated = true; | |
| 252 | start++; | |
| 253 | continue; | |
| 254 | } | |
| 255 | ||
| 256 | if (opts.noparen !== true && code === CHAR_LEFT_PARENTHESES) { | |
| 257 | isGlob = token.isGlob = true; | |
| 258 | ||
| 259 | if (scanToEnd === true) { | |
| 260 | while (eos() !== true && (code = advance())) { | |
| 261 | if (code === CHAR_LEFT_PARENTHESES) { | |
| 262 | backslashes = token.backslashes = true; | |
| 263 | code = advance(); | |
| 264 | continue; | |
| 265 | } | |
| 266 | ||
| 267 | if (code === CHAR_RIGHT_PARENTHESES) { | |
| 268 | finished = true; | |
| 269 | break; | |
| 270 | } | |
| 271 | } | |
| 272 | continue; | |
| 273 | } | |
| 274 | break; | |
| 275 | } | |
| 276 | ||
| 277 | if (isGlob === true) { | |
| 278 | finished = true; | |
| 279 | ||
| 280 | if (scanToEnd === true) { | |
| 281 | continue; | |
| 282 | } | |
| 283 | ||
| 284 | break; | |
| 285 | } | |
| 286 | } | |
| 287 | ||
| 288 | if (opts.noext === true) { | |
| 289 | isExtglob = false; | |
| 290 | isGlob = false; | |
| 291 | } | |
| 292 | ||
| 293 | let base = str; | |
| 294 | let prefix = ''; | |
| 295 | let glob = ''; | |
| 296 | ||
| 297 | if (start > 0) { | |
| 298 | prefix = str.slice(0, start); | |
| 299 | str = str.slice(start); | |
| 300 | lastIndex -= start; | |
| 301 | } | |
| 302 | ||
| 303 | if (base && isGlob === true && lastIndex > 0) { | |
| 304 | base = str.slice(0, lastIndex); | |
| 305 | glob = str.slice(lastIndex); | |
| 306 | } else if (isGlob === true) { | |
| 307 | base = ''; | |
| 308 | glob = str; | |
| 309 | } else { | |
| 310 | base = str; | |
| 311 | } | |
| 312 | ||
| 313 | if (base && base !== '' && base !== '/' && base !== str) { | |
| 314 | if (isPathSeparator(base.charCodeAt(base.length - 1))) { | |
| 315 | base = base.slice(0, -1); | |
| 316 | } | |
| 317 | } | |
| 318 | ||
| 319 | if (opts.unescape === true) { | |
| 320 | if (glob) glob = utils.removeBackslashes(glob); | |
| 321 | ||
| 322 | if (base && backslashes === true) { | |
| 323 | base = utils.removeBackslashes(base); | |
| 324 | } | |
| 325 | } | |
| 326 | ||
| 327 | const state = { | |
| 328 | prefix, | |
| 329 | input, | |
| 330 | start, | |
| 331 | base, | |
| 332 | glob, | |
| 333 | isBrace, | |
| 334 | isBracket, | |
| 335 | isGlob, | |
| 336 | isExtglob, | |
| 337 | isGlobstar, | |
| 338 | negated, | |
| 339 | negatedExtglob | |
| 340 | }; | |
| 341 | ||
| 342 | if (opts.tokens === true) { | |
| 343 | state.maxDepth = 0; | |
| 344 | if (!isPathSeparator(code)) { | |
| 345 | tokens.push(token); | |
| 346 | } | |
| 347 | state.tokens = tokens; | |
| 348 | } | |
| 349 | ||
| 350 | if (opts.parts === true || opts.tokens === true) { | |
| 351 | let prevIndex; | |
| 352 | ||
| 353 | for (let idx = 0; idx < slashes.length; idx++) { | |
| 354 | const n = prevIndex ? prevIndex + 1 : start; | |
| 355 | const i = slashes[idx]; | |
| 356 | const value = input.slice(n, i); | |
| 357 | if (opts.tokens) { | |
| 358 | if (idx === 0 && start !== 0) { | |
| 359 | tokens[idx].isPrefix = true; | |
| 360 | tokens[idx].value = prefix; | |
| 361 | } else { | |
| 362 | tokens[idx].value = value; | |
| 363 | } | |
| 364 | depth(tokens[idx]); | |
| 365 | state.maxDepth += tokens[idx].depth; | |
| 366 | } | |
| 367 | if (idx !== 0 || value !== '') { | |
| 368 | parts.push(value); | |
| 369 | } | |
| 370 | prevIndex = i; | |
| 371 | } | |
| 372 | ||
| 373 | if (prevIndex && prevIndex + 1 < input.length) { | |
| 374 | const value = input.slice(prevIndex + 1); | |
| 375 | parts.push(value); | |
| 376 | ||
| 377 | if (opts.tokens) { | |
| 378 | tokens[tokens.length - 1].value = value; | |
| 379 | depth(tokens[tokens.length - 1]); | |
| 380 | state.maxDepth += tokens[tokens.length - 1].depth; | |
| 381 | } | |
| 382 | } | |
| 383 | ||
| 384 | state.slashes = slashes; | |
| 385 | state.parts = parts; | |
| 386 | } | |
| 387 | ||
| 388 | return state; | |
| 389 | }; | |
| 390 | ||
| 391 | module.exports = scan; |
| … | … | @@ -0,0 +1,72 @@ |
| 1 | /*global navigator*/ | |
| 2 | 'use strict'; | |
| 3 | ||
| 4 | const { | |
| 5 | REGEX_BACKSLASH, | |
| 6 | REGEX_REMOVE_BACKSLASH, | |
| 7 | REGEX_SPECIAL_CHARS, | |
| 8 | REGEX_SPECIAL_CHARS_GLOBAL | |
| 9 | } = require('./constants'); | |
| 10 | ||
| 11 | exports.isObject = val => val !== null && typeof val === 'object' && !Array.isArray(val); | |
| 12 | exports.hasRegexChars = str => REGEX_SPECIAL_CHARS.test(str); | |
| 13 | exports.isRegexChar = str => str.length === 1 && exports.hasRegexChars(str); | |
| 14 | exports.escapeRegex = str => str.replace(REGEX_SPECIAL_CHARS_GLOBAL, '\\$1'); | |
| 15 | exports.toPosixSlashes = str => str.replace(REGEX_BACKSLASH, '/'); | |
| 16 | ||
| 17 | exports.isWindows = () => { | |
| 18 | if (typeof navigator !== 'undefined' && navigator.platform) { | |
| 19 | const platform = navigator.platform.toLowerCase(); | |
| 20 | return platform === 'win32' || platform === 'windows'; | |
| 21 | } | |
| 22 | ||
| 23 | if (typeof process !== 'undefined' && process.platform) { | |
| 24 | return process.platform === 'win32'; | |
| 25 | } | |
| 26 | ||
| 27 | return false; | |
| 28 | }; | |
| 29 | ||
| 30 | exports.removeBackslashes = str => { | |
| 31 | return str.replace(REGEX_REMOVE_BACKSLASH, match => { | |
| 32 | return match === '\\' ? '' : match; | |
| 33 | }); | |
| 34 | }; | |
| 35 | ||
| 36 | exports.escapeLast = (input, char, lastIdx) => { | |
| 37 | const idx = input.lastIndexOf(char, lastIdx); | |
| 38 | if (idx === -1) return input; | |
| 39 | if (input[idx - 1] === '\\') return exports.escapeLast(input, char, idx - 1); | |
| 40 | return `${input.slice(0, idx)}\\${input.slice(idx)}`; | |
| 41 | }; | |
| 42 | ||
| 43 | exports.removePrefix = (input, state = {}) => { | |
| 44 | let output = input; | |
| 45 | if (output.startsWith('./')) { | |
| 46 | output = output.slice(2); | |
| 47 | state.prefix = './'; | |
| 48 | } | |
| 49 | return output; | |
| 50 | }; | |
| 51 | ||
| 52 | exports.wrapOutput = (input, state = {}, options = {}) => { | |
| 53 | const prepend = options.contains ? '' : '^'; | |
| 54 | const append = options.contains ? '' : '$'; | |
| 55 | ||
| 56 | let output = `${prepend}(?:${input})${append}`; | |
| 57 | if (state.negated === true) { | |
| 58 | output = `(?:^(?!${output}).*$)`; | |
| 59 | } | |
| 60 | return output; | |
| 61 | }; | |
| 62 | ||
| 63 | exports.basename = (path, { windows } = {}) => { | |
| 64 | const segs = path.split(windows ? /[\\/]/ : '/'); | |
| 65 | const last = segs[segs.length - 1]; | |
| 66 | ||
| 67 | if (last === '') { | |
| 68 | return segs[segs.length - 2]; | |
| 69 | } | |
| 70 | ||
| 71 | return last; | |
| 72 | }; |
| … | … | @@ -0,0 +1,82 @@ |
| 1 | { | |
| 2 | "name": "picomatch", | |
| 3 | "description": "Blazing fast and accurate glob matcher written in JavaScript, with no dependencies and full support for standard and extended Bash glob features, including braces, extglobs, POSIX brackets, and regular expressions.", | |
| 4 | "version": "4.0.4", | |
| 5 | "homepage": "https://github.com/micromatch/picomatch", | |
| 6 | "author": "Jon Schlinkert (https://github.com/jonschlinkert)", | |
| 7 | "funding": "https://github.com/sponsors/jonschlinkert", | |
| 8 | "repository": "micromatch/picomatch", | |
| 9 | "bugs": { | |
| 10 | "url": "https://github.com/micromatch/picomatch/issues" | |
| 11 | }, | |
| 12 | "license": "MIT", | |
| 13 | "files": [ | |
| 14 | "index.js", | |
| 15 | "posix.js", | |
| 16 | "lib" | |
| 17 | ], | |
| 18 | "sideEffects": false, | |
| 19 | "main": "index.js", | |
| 20 | "engines": { | |
| 21 | "node": ">=12" | |
| 22 | }, | |
| 23 | "scripts": { | |
| 24 | "lint": "eslint --cache --cache-location node_modules/.cache/.eslintcache --report-unused-disable-directives --ignore-path .gitignore .", | |
| 25 | "mocha": "mocha --reporter dot", | |
| 26 | "test": "npm run lint && npm run mocha", | |
| 27 | "test:ci": "npm run test:cover", | |
| 28 | "test:cover": "nyc npm run mocha" | |
| 29 | }, | |
| 30 | "devDependencies": { | |
| 31 | "eslint": "^8.57.0", | |
| 32 | "fill-range": "^7.0.1", | |
| 33 | "gulp-format-md": "^2.0.0", | |
| 34 | "mocha": "^10.4.0", | |
| 35 | "nyc": "^15.1.0" | |
| 36 | }, | |
| 37 | "keywords": [ | |
| 38 | "glob", | |
| 39 | "match", | |
| 40 | "picomatch" | |
| 41 | ], | |
| 42 | "nyc": { | |
| 43 | "reporter": [ | |
| 44 | "html", | |
| 45 | "lcov", | |
| 46 | "text-summary" | |
| 47 | ] | |
| 48 | }, | |
| 49 | "verb": { | |
| 50 | "toc": { | |
| 51 | "render": true, | |
| 52 | "method": "preWrite", | |
| 53 | "maxdepth": 3 | |
| 54 | }, | |
| 55 | "layout": "empty", | |
| 56 | "tasks": [ | |
| 57 | "readme" | |
| 58 | ], | |
| 59 | "plugins": [ | |
| 60 | "gulp-format-md" | |
| 61 | ], | |
| 62 | "lint": { | |
| 63 | "reflinks": true | |
| 64 | }, | |
| 65 | "related": { | |
| 66 | "list": [ | |
| 67 | "braces", | |
| 68 | "micromatch" | |
| 69 | ] | |
| 70 | }, | |
| 71 | "reflinks": [ | |
| 72 | "braces", | |
| 73 | "expand-brackets", | |
| 74 | "extglob", | |
| 75 | "fill-range", | |
| 76 | "micromatch", | |
| 77 | "minimatch", | |
| 78 | "nanomatch", | |
| 79 | "picomatch" | |
| 80 | ] | |
| 81 | } | |
| 82 | } |
| … | … | @@ -0,0 +1,3 @@ |
| 1 | 'use strict'; | |
| 2 | ||
| 3 | module.exports = require('./lib/picomatch'); |
| … | … | @@ -0,0 +1,6 @@ |
| 1 | language: node_js | |
| 2 | sudo: false | |
| 3 | node_js: | |
| 4 | - 6 | |
| 5 | - 8 | |
| 6 | - 9 |
| … | … | @@ -0,0 +1,21 @@ |
| 1 | The MIT License (MIT) | |
| 2 | ||
| 3 | Copyright (c) 2018 Mathias Buus | |
| 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 | |
| 13 | all 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 | |
| 21 | THE SOFTWARE. |
| … | … | @@ -0,0 +1,83 @@ |
| 1 | # record-cache | |
| 2 | ||
| 3 | Cache optimised for record like things like `host:port` or `domain.names`. | |
| 4 | ||
| 5 | ``` | |
| 6 | npm install record-cache | |
| 7 | ``` | |
| 8 | ||
| 9 | [](https://travis-ci.org/mafintosh/record-cache) | |
| 10 | ||
| 11 | ## Usage | |
| 12 | ||
| 13 | ``` js | |
| 14 | var recordCache = require('record-cache') | |
| 15 | ||
| 16 | var cache = recordCache({ | |
| 17 | maxSize: 1000 // store ~1000 values at max | |
| 18 | maxAge: 1000 // gc values older than ~1000ms | |
| 19 | }) | |
| 20 | ||
| 21 | cache.add('hello', '127.0.0.1') | |
| 22 | cache.add('hello', '127.0.1.1') | |
| 23 | cache.add('hello', '127.0.0.2') | |
| 24 | ||
| 25 | console.log(cache.get('hello', 2)) // prints two of the above | |
| 26 | ||
| 27 | // wait 2s | |
| 28 | setTimeout(function () { | |
| 29 | console.log(cache.get('hello', 2)) // prints [] | |
| 30 | }, 2000) | |
| 31 | ``` | |
| 32 | ||
| 33 | ## API | |
| 34 | ||
| 35 | #### `var cache = recordCache([options])` | |
| 36 | ||
| 37 | Create a new record cache. | |
| 38 | ||
| 39 | Options include: | |
| 40 | ||
| 41 | ``` js | |
| 42 | { | |
| 43 | maxSize: 1000, // approximate max size | |
| 44 | maxAge: 1000, // approximate max age in ms | |
| 45 | onStale: false // function called when evicting stale records | |
| 46 | } | |
| 47 | ``` | |
| 48 | ||
| 49 | In the worst case the cache will be `2 * maxSize` large, and | |
| 50 | if `maxAge` is used old values are gc'ed every `0.66 * maxAge - 1.33 * maxAge` with an optional callback to the `onStale` function upon record eviction. | |
| 51 | ||
| 52 | This is to greatly simplify the data structures and also gives us a pretty decent | |
| 53 | perf boost compared to other cache modules out there. | |
| 54 | ||
| 55 | #### `cache.add(recordName, value)` | |
| 56 | ||
| 57 | Push a new value to the record set. `value` should be serialisable. | |
| 58 | ||
| 59 | #### `cache.remove(recordName, value)` | |
| 60 | ||
| 61 | Remove a value from the record set. `value` should be a previously added value. | |
| 62 | ||
| 63 | #### `var list = cache.get(recordName, [maxCount])` | |
| 64 | ||
| 65 | Get a list of values from the record set. The list will be randomised. | |
| 66 | Specify `maxCount` to only get this many values at max. | |
| 67 | ||
| 68 | #### `cache.size` | |
| 69 | ||
| 70 | Get the actual size of the cache. | |
| 71 | ||
| 72 | #### `cache.clear()` | |
| 73 | ||
| 74 | Clear all values from the cache. | |
| 75 | ||
| 76 | #### `cache.destroy()` | |
| 77 | ||
| 78 | Completely destroy the cache. Needed if you are using the `maxAge` option to | |
| 79 | cancel the gc timer. | |
| 80 | ||
| 81 | ## License | |
| 82 | ||
| 83 | MIT |
| … | … | @@ -0,0 +1,13 @@ |
| 1 | var recordCache = require('./') | |
| 2 | ||
| 3 | var rc = recordCache({ | |
| 4 | maxAge: 10, | |
| 5 | maxSize: 100 | |
| 6 | }) | |
| 7 | ||
| 8 | rc.add('hello', 'world') | |
| 9 | rc.add('hello', 'welt') | |
| 10 | rc.add('hello', 'verden') | |
| 11 | ||
| 12 | console.log(rc.get('hello', 2)) | |
| 13 | setTimeout(() => console.log(rc.get('hello', 2)), 200) |
| … | … | @@ -0,0 +1,170 @@ |
| 1 | const b4a = require('b4a') | |
| 2 | ||
| 3 | var EMPTY = [] | |
| 4 | ||
| 5 | module.exports = RecordCache | |
| 6 | ||
| 7 | function RecordSet () { | |
| 8 | this.list = [] | |
| 9 | this.map = new Map() | |
| 10 | } | |
| 11 | ||
| 12 | RecordSet.prototype.add = function (record, value) { | |
| 13 | var k = toString(record) | |
| 14 | var r = this.map.get(k) | |
| 15 | if (r) return false | |
| 16 | ||
| 17 | r = {index: this.list.length, record: value || record} | |
| 18 | this.list.push(r) | |
| 19 | this.map.set(k, r) | |
| 20 | return true | |
| 21 | } | |
| 22 | ||
| 23 | RecordSet.prototype.remove = function (record) { | |
| 24 | var k = toString(record) | |
| 25 | var r = this.map.get(k) | |
| 26 | if (!r) return false | |
| 27 | ||
| 28 | swap(this.list, r.index, this.list.length - 1) | |
| 29 | this.list.pop() | |
| 30 | this.map.delete(k) | |
| 31 | return true | |
| 32 | } | |
| 33 | ||
| 34 | function RecordStore () { | |
| 35 | this.records = new Map() | |
| 36 | this.size = 0 | |
| 37 | } | |
| 38 | ||
| 39 | RecordStore.prototype.add = function (name, record, value) { | |
| 40 | var r = this.records.get(name) | |
| 41 | ||
| 42 | if (!r) { | |
| 43 | r = new RecordSet() | |
| 44 | this.records.set(name, r) | |
| 45 | } | |
| 46 | ||
| 47 | if (r.add(record, value)) { | |
| 48 | this.size++ | |
| 49 | return true | |
| 50 | } | |
| 51 | ||
| 52 | return false | |
| 53 | } | |
| 54 | ||
| 55 | RecordStore.prototype.remove = function (name, record, value) { | |
| 56 | var r = this.records.get(name) | |
| 57 | if (!r) return false | |
| 58 | ||
| 59 | if (r.remove(record, value)) { | |
| 60 | this.size-- | |
| 61 | if (!r.map.size) this.records.delete(name) | |
| 62 | return true | |
| 63 | } | |
| 64 | ||
| 65 | return false | |
| 66 | } | |
| 67 | ||
| 68 | RecordStore.prototype.get = function (name) { | |
| 69 | var r = this.records.get(name) | |
| 70 | return r ? r.list : EMPTY | |
| 71 | } | |
| 72 | ||
| 73 | function RecordCache (opts) { | |
| 74 | if (!(this instanceof RecordCache)) return new RecordCache(opts) | |
| 75 | if (!opts) opts = {} | |
| 76 | ||
| 77 | this.maxSize = opts.maxSize || Infinity | |
| 78 | this.maxAge = opts.maxAge || 0 | |
| 79 | ||
| 80 | this._onstale = opts.onStale || opts.onstale || null | |
| 81 | this._fresh = new RecordStore() | |
| 82 | this._stale = new RecordStore() | |
| 83 | this._interval = null | |
| 84 | this._gced = false | |
| 85 | ||
| 86 | if (this.maxAge && this.maxAge < Infinity) { | |
| 87 | // 2/3 gives us a span of 0.66-1.33 maxAge or avg maxAge | |
| 88 | var tick = Math.ceil(2 / 3 * this.maxAge) | |
| 89 | this._interval = setInterval(this._gcAuto.bind(this), tick) | |
| 90 | if (this._interval.unref) this._interval.unref() | |
| 91 | } | |
| 92 | } | |
| 93 | ||
| 94 | Object.defineProperty(RecordCache.prototype, 'size', { | |
| 95 | get: function () { | |
| 96 | return this._fresh.size + this._stale.size | |
| 97 | } | |
| 98 | }) | |
| 99 | ||
| 100 | RecordCache.prototype.add = function (name, record, value) { | |
| 101 | this._stale.remove(name, record, value) | |
| 102 | if (this._fresh.add(name, record, value) && this._fresh.size > this.maxSize) { | |
| 103 | this._gc() | |
| 104 | } | |
| 105 | } | |
| 106 | ||
| 107 | RecordCache.prototype.remove = function (name, record, value) { | |
| 108 | this._fresh.remove(name, record, value) | |
| 109 | this._stale.remove(name, record, value) | |
| 110 | } | |
| 111 | ||
| 112 | RecordCache.prototype.get = function (name, n) { | |
| 113 | var a = this._fresh.get(name) | |
| 114 | var b = this._stale.get(name) | |
| 115 | var aLen = a.length | |
| 116 | var bLen = b.length | |
| 117 | var len = aLen + bLen | |
| 118 | ||
| 119 | if (n > len || !n) n = len | |
| 120 | var result = new Array(n) | |
| 121 | ||
| 122 | for (var i = 0; i < n; i++) { | |
| 123 | var j = Math.floor(Math.random() * (aLen + bLen)) | |
| 124 | if (j < aLen) { | |
| 125 | result[i] = a[j].record | |
| 126 | swap(a, j, --aLen) | |
| 127 | } else { | |
| 128 | j -= aLen | |
| 129 | result[i] = b[j].record | |
| 130 | swap(b, j, --bLen) | |
| 131 | } | |
| 132 | } | |
| 133 | ||
| 134 | return result | |
| 135 | } | |
| 136 | ||
| 137 | RecordCache.prototype._gcAuto = function () { | |
| 138 | if (!this._gced) this._gc() | |
| 139 | this._gced = false | |
| 140 | } | |
| 141 | ||
| 142 | RecordCache.prototype._gc = function () { | |
| 143 | if (this._onstale && this._stale.size > 0) this._onstale(this._stale) | |
| 144 | this._stale = this._fresh | |
| 145 | this._fresh = new RecordStore() | |
| 146 | this._gced = true | |
| 147 | } | |
| 148 | ||
| 149 | RecordCache.prototype.clear = function () { | |
| 150 | this._gc() | |
| 151 | this._gc() | |
| 152 | } | |
| 153 | ||
| 154 | RecordCache.prototype.destroy = function () { | |
| 155 | this.clear() | |
| 156 | clearInterval(this._interval) | |
| 157 | this._interval = null | |
| 158 | } | |
| 159 | ||
| 160 | function toString (record) { | |
| 161 | return b4a.isBuffer(record) ? b4a.toString(record, 'hex') : record | |
| 162 | } | |
| 163 | ||
| 164 | function swap (list, a, b) { | |
| 165 | var tmp = list[a] | |
| 166 | tmp.index = b | |
| 167 | list[b].index = a | |
| 168 | list[a] = list[b] | |
| 169 | list[b] = tmp | |
| 170 | } |
| … | … | @@ -0,0 +1,26 @@ |
| 1 | { | |
| 2 | "name": "record-cache", | |
| 3 | "version": "1.2.0", | |
| 4 | "description": "Cache optimised for record like things", | |
| 5 | "main": "index.js", | |
| 6 | "dependencies": { | |
| 7 | "b4a": "^1.3.1" | |
| 8 | }, | |
| 9 | "devDependencies": { | |
| 10 | "standard": "^10.0.3", | |
| 11 | "tape": "^4.8.0" | |
| 12 | }, | |
| 13 | "scripts": { | |
| 14 | "test": "standard && tape test.js" | |
| 15 | }, | |
| 16 | "repository": { | |
| 17 | "type": "git", | |
| 18 | "url": "https://github.com/mafintosh/record-cache.git" | |
| 19 | }, | |
| 20 | "author": "Mathias Buus (@mafintosh)", | |
| 21 | "license": "MIT", | |
| 22 | "bugs": { | |
| 23 | "url": "https://github.com/mafintosh/record-cache/issues" | |
| 24 | }, | |
| 25 | "homepage": "https://github.com/mafintosh/record-cache" | |
| 26 | } |
| … | … | @@ -0,0 +1,185 @@ |
| 1 | var tape = require('tape') | |
| 2 | var recordCache = require('./') | |
| 3 | ||
| 4 | tape('add and get', function (t) { | |
| 5 | var rc = recordCache() | |
| 6 | rc.add('hello', 'world') | |
| 7 | t.same(rc.get('hello'), ['world']) | |
| 8 | t.end() | |
| 9 | }) | |
| 10 | ||
| 11 | tape('add and get buffer', function (t) { | |
| 12 | var rc = recordCache() | |
| 13 | rc.add('hello', Buffer.from('world')) | |
| 14 | t.same(rc.get('hello'), [Buffer.from('world')]) | |
| 15 | t.end() | |
| 16 | }) | |
| 17 | ||
| 18 | tape('add and get (more than one)', function (t) { | |
| 19 | var rc = recordCache() | |
| 20 | rc.add('hello', 'world') | |
| 21 | rc.add('hello', 'verden') | |
| 22 | rc.add('hello', 'welt') | |
| 23 | t.same(rc.get('hello').sort(), ['verden', 'welt', 'world']) | |
| 24 | ||
| 25 | var list = rc.get('hello', 2) | |
| 26 | t.ok(list[0] !== list[1]) | |
| 27 | t.ok(['verden', 'welt', 'world'].includes(list[0])) | |
| 28 | t.ok(['verden', 'welt', 'world'].includes(list[1])) | |
| 29 | t.end() | |
| 30 | }) | |
| 31 | ||
| 32 | tape('get is randomised', function (t) { | |
| 33 | var rc = recordCache() | |
| 34 | ||
| 35 | rc.add('hello', 'a') | |
| 36 | rc.add('hello', 'b') | |
| 37 | rc.add('hello', 'c') | |
| 38 | ||
| 39 | var map = {} | |
| 40 | ||
| 41 | for (var i = 0; i < 1000; i++) { | |
| 42 | map[rc.get('hello', 2).join('')] = true | |
| 43 | } | |
| 44 | ||
| 45 | t.same(map, {ab: true, ba: true, cb: true, bc: true, ac: true, ca: true}) | |
| 46 | t.end() | |
| 47 | }) | |
| 48 | ||
| 49 | tape('get capped', function (t) { | |
| 50 | var rc = recordCache({maxSize: 10}) | |
| 51 | ||
| 52 | for (var i = 0; i < 50; i++) { | |
| 53 | rc.add('hello', '' + i) | |
| 54 | } | |
| 55 | ||
| 56 | t.ok(rc.get('hello').length <= 20) | |
| 57 | t.ok(rc.size <= 20) | |
| 58 | t.notOk(rc.get('hello').includes('0')) | |
| 59 | t.notOk(rc.get('hello').includes('29')) | |
| 60 | t.end() | |
| 61 | }) | |
| 62 | ||
| 63 | tape('get capped with many record sets', function (t) { | |
| 64 | var rc = recordCache({maxSize: 10}) | |
| 65 | ||
| 66 | for (var i = 0; i < 50; i++) { | |
| 67 | rc.add('' + i, 'hello') | |
| 68 | } | |
| 69 | ||
| 70 | t.ok(rc.size <= 20) | |
| 71 | t.same(rc.get('0'), []) | |
| 72 | t.same(rc.get('29'), []) | |
| 73 | t.same(rc.get('49'), ['hello']) | |
| 74 | t.end() | |
| 75 | }) | |
| 76 | ||
| 77 | tape('many updates is fine when capped', function (t) { | |
| 78 | var rc = recordCache({maxSize: 10}) | |
| 79 | ||
| 80 | for (var i = 0; i < 10; i++) { | |
| 81 | rc.add('hello', '' + i) | |
| 82 | } | |
| 83 | for (var j = 0; j < 100; j++) { | |
| 84 | rc.add('hello', '9') | |
| 85 | } | |
| 86 | ||
| 87 | t.same(rc.get('hello').sort().join(''), '0123456789') | |
| 88 | t.end() | |
| 89 | }) | |
| 90 | ||
| 91 | tape('remove', function (t) { | |
| 92 | var rc = recordCache() | |
| 93 | ||
| 94 | t.same(rc.get('hello'), []) | |
| 95 | rc.remove('hello', 'world') | |
| 96 | t.same(rc.get('hello'), []) | |
| 97 | rc.add('hello', 'world') | |
| 98 | t.same(rc.get('hello'), ['world']) | |
| 99 | rc.remove('hello', 'world') | |
| 100 | t.same(rc.get('hello'), []) | |
| 101 | t.end() | |
| 102 | }) | |
| 103 | ||
| 104 | tape('remove with other value', function (t) { | |
| 105 | var rc = recordCache() | |
| 106 | ||
| 107 | rc.add('hello', 'hi') | |
| 108 | t.same(rc.get('hello'), ['hi']) | |
| 109 | rc.remove('hello', 'world') | |
| 110 | t.same(rc.get('hello'), ['hi']) | |
| 111 | rc.add('hello', 'world') | |
| 112 | t.same(rc.get('hello').sort(), ['hi', 'world']) | |
| 113 | rc.remove('hello', 'world') | |
| 114 | t.same(rc.get('hello'), ['hi']) | |
| 115 | t.end() | |
| 116 | }) | |
| 117 | ||
| 118 | tape('clear', function (t) { | |
| 119 | var rc = recordCache() | |
| 120 | ||
| 121 | rc.clear() | |
| 122 | t.same(rc.get('hello'), []) | |
| 123 | rc.add('hello', 'a') | |
| 124 | rc.add('hello', 'b') | |
| 125 | rc.add('foo', 'bar') | |
| 126 | t.same(rc.get('hello').sort(), ['a', 'b']) | |
| 127 | rc.clear() | |
| 128 | t.same(rc.get('hello'), []) | |
| 129 | t.same(rc.get('foo'), []) | |
| 130 | t.end() | |
| 131 | }) | |
| 132 | ||
| 133 | tape('maxAge', function (t) { | |
| 134 | var rc = recordCache({maxAge: 20}) | |
| 135 | ||
| 136 | rc.add('hello', 'world') | |
| 137 | rc.add('hello', 'verden') | |
| 138 | setTimeout(function () { | |
| 139 | t.same(rc.get('hello').sort(), ['verden', 'world']) | |
| 140 | setTimeout(function () { | |
| 141 | t.same(rc.get('hello'), []) | |
| 142 | rc.destroy() | |
| 143 | t.end() | |
| 144 | }, 35) | |
| 145 | }, 5) | |
| 146 | }) | |
| 147 | ||
| 148 | tape('maxAge but one value is staying alive', function (t) { | |
| 149 | var rc = recordCache({maxAge: 20}) | |
| 150 | ||
| 151 | rc.add('hello', 'world') | |
| 152 | rc.add('hello', 'verden') | |
| 153 | var interval = setInterval(function () { | |
| 154 | rc.add('hello', 'verden') | |
| 155 | }, 5) | |
| 156 | setTimeout(function () { | |
| 157 | t.same(rc.get('hello').sort(), ['verden', 'world']) | |
| 158 | setTimeout(function () { | |
| 159 | clearInterval(interval) | |
| 160 | t.same(rc.get('hello'), ['verden']) | |
| 161 | rc.destroy() | |
| 162 | t.end() | |
| 163 | }, 35) | |
| 164 | }, 5) | |
| 165 | }) | |
| 166 | ||
| 167 | tape('add dedups buffers', function (t) { | |
| 168 | var rc = recordCache() | |
| 169 | ||
| 170 | rc.add('hello', Buffer.from('world')) | |
| 171 | rc.add('hello', Buffer.from('world')) | |
| 172 | ||
| 173 | t.same(rc.get('hello'), [Buffer.from('world')]) | |
| 174 | t.end() | |
| 175 | }) | |
| 176 | ||
| 177 | tape('add and remove buffer', function (t) { | |
| 178 | var rc = recordCache() | |
| 179 | ||
| 180 | rc.add('hello', Buffer.from('world')) | |
| 181 | rc.remove('hello', Buffer.from('world')) | |
| 182 | ||
| 183 | t.same(rc.get('hello'), []) | |
| 184 | t.end() | |
| 185 | }) |
| … | … | @@ -0,0 +1,23 @@ |
| 1 | name: Build Status | |
| 2 | on: | |
| 3 | push: | |
| 4 | branches: | |
| 5 | - main | |
| 6 | pull_request: | |
| 7 | branches: | |
| 8 | - main | |
| 9 | jobs: | |
| 10 | build: | |
| 11 | strategy: | |
| 12 | matrix: | |
| 13 | node-version: [lts/*] | |
| 14 | os: [ubuntu-latest, macos-latest, windows-latest] | |
| 15 | runs-on: ${{ matrix.os }} | |
| 16 | steps: | |
| 17 | - uses: actions/checkout@v2 | |
| 18 | - name: Use Node.js ${{ matrix.node-version }} | |
| 19 | uses: actions/setup-node@v2 | |
| 20 | with: | |
| 21 | node-version: ${{ matrix.node-version }} | |
| 22 | - run: npm install | |
| 23 | - run: npm test |
| … | … | @@ -0,0 +1,21 @@ |
| 1 | MIT License | |
| 2 | ||
| 3 | Copyright (c) 2023 Contributors | |
| 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. |
| … | … | @@ -0,0 +1,53 @@ |
| 1 | # same-object | |
| 2 | ||
| 3 | Determine if two objects are deeply equal | |
| 4 | ||
| 5 | ``` | |
| 6 | npm install same-object | |
| 7 | ``` | |
| 8 | ||
| 9 | Supports circular references, Maps, Symbols, etc. | |
| 10 | ||
| 11 | Aims for ~99% compatibility with `deep-equal` or `assert.deepEqual` without requiring native dependencies.\ | |
| 12 | Useful for JavaScript runtimes without native Node modules like `util`, etc. | |
| 13 | ||
| 14 | ## Usage | |
| 15 | ``` js | |
| 16 | const sameObject = require('same-object') | |
| 17 | ||
| 18 | console.log(sameObject(1, '1')) // true | |
| 19 | console.log(sameObject(1, '1', { strict: true })) // false | |
| 20 | ||
| 21 | console.log(sameObject({ a: 1 }, { a: 1 })) // true | |
| 22 | console.log(sameObject({ a: 1 }, { a: 1, b: 2 })) // false | |
| 23 | ||
| 24 | console.log(sameObject( | |
| 25 | new Set(['a', 1, 'b', 2]), | |
| 26 | new Set(['b', 2, 'a', 1]) | |
| 27 | )) // true | |
| 28 | ``` | |
| 29 | ||
| 30 | ## API | |
| 31 | ||
| 32 | #### `const bool = sameObject(a, b, [options])` | |
| 33 | ||
| 34 | Compares `a` and `b`, returning whether they are equal or not. | |
| 35 | ||
| 36 | Available `options`: | |
| 37 | ```js | |
| 38 | { | |
| 39 | strict: false | |
| 40 | } | |
| 41 | ``` | |
| 42 | ||
| 43 | Loosely comparison (`==`) by default.\ | |
| 44 | Use `{ strict: true }` for a stronger equality check (`===`). | |
| 45 | ||
| 46 | ## References | |
| 47 | The source code is based on:\ | |
| 48 | [node/comparisons.js](https://github.com/nodejs/node/blob/2adea16e394448c4c87b0639514f8babbeb7a080/lib/internal/util/comparisons.js)\ | |
| 49 | [inspect-js/node-deep-equal](https://github.com/inspect-js/node-deep-equal)\ | |
| 50 | [chaijs/deep-eql](https://github.com/chaijs/deep-eql) | |
| 51 | ||
| 52 | ## License | |
| 53 | MIT |
| … | … | @@ -0,0 +1,116 @@ |
| 1 | module.exports = same | |
| 2 | ||
| 3 | const isPrimitive = require('./lib/is-primitive.js') | |
| 4 | const sameKeys = require('./lib/same-keys.js') | |
| 5 | const sameKeyValues = require('./lib/same-key-values.js') | |
| 6 | const sameIterable = require('./lib/same-iterable.js') | |
| 7 | const sameSet = require('./lib/same-set.js') | |
| 8 | const sameMap = require('./lib/same-map.js') | |
| 9 | const sameArray = require('./lib/same-array.js') | |
| 10 | const sameRegExp = require('./lib/same-regexp.js') | |
| 11 | ||
| 12 | function same (a, b, opts, memos) { | |
| 13 | // Short path optimization | |
| 14 | if (a === b) { | |
| 15 | if (a !== 0) return true | |
| 16 | return opts && opts.strict ? Object.is(a, b) : true | |
| 17 | } | |
| 18 | ||
| 19 | const aIsPrimitive = isPrimitive(a) | |
| 20 | const bIsPrimitive = isPrimitive(b) | |
| 21 | ||
| 22 | if (aIsPrimitive && bIsPrimitive) { | |
| 23 | if (opts && opts.strict) { | |
| 24 | return typeof a === 'number' ? (Number.isNaN(a) && Number.isNaN(b)) : a === b | |
| 25 | } | |
| 26 | return a == b || (Number.isNaN(a) && Number.isNaN(b)) // eslint-disable-line eqeqeq | |
| 27 | } | |
| 28 | ||
| 29 | if (aIsPrimitive !== bIsPrimitive) return false | |
| 30 | ||
| 31 | if (opts && opts.strict) { | |
| 32 | if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) return false | |
| 33 | } | |
| 34 | ||
| 35 | const aType = getType(a) | |
| 36 | const bType = getType(b) | |
| 37 | ||
| 38 | if (aType !== bType) return false | |
| 39 | ||
| 40 | if (!sameKeys(a, b, opts, memos)) return false | |
| 41 | ||
| 42 | switch (aType) { | |
| 43 | case 'String': | |
| 44 | case 'Number': | |
| 45 | case 'Boolean': | |
| 46 | case 'Date': | |
| 47 | return same(a.valueOf(), b.valueOf(), opts, memos) | |
| 48 | } | |
| 49 | ||
| 50 | if (aType === 'Error') { | |
| 51 | return sameKeyValues(a, b, opts, memos, ['name', 'message']) | |
| 52 | } | |
| 53 | ||
| 54 | switch (aType) { | |
| 55 | case 'Int8Array': | |
| 56 | case 'Uint8Array': | |
| 57 | case 'Uint8ClampedArray': | |
| 58 | case 'Int16Array': | |
| 59 | case 'Uint16Array': | |
| 60 | case 'Int32Array': | |
| 61 | case 'Uint32Array': | |
| 62 | case 'Float32Array': | |
| 63 | case 'Float64Array': | |
| 64 | return sameIterable(a, b, opts, memos) | |
| 65 | case 'DataView': | |
| 66 | return sameIterable(new Uint8Array(a.buffer, a.byteOffset, a.byteLength), new Uint8Array(b.buffer, b.byteOffset, b.byteLength), opts, memos) | |
| 67 | case 'ArrayBuffer': | |
| 68 | return sameIterable(new Uint8Array(a), new Uint8Array(b), opts, memos) | |
| 69 | } | |
| 70 | ||
| 71 | if (aType === 'RegExp') return sameRegExp(a, b) | |
| 72 | ||
| 73 | if (!memos) { | |
| 74 | memos = { a: new Map(), b: new Map(), position: 0 } | |
| 75 | } else { | |
| 76 | const memoA = memos.a.get(a) | |
| 77 | if (memoA !== undefined) { | |
| 78 | const memoB = memos.b.get(b) | |
| 79 | if (memoB !== undefined) return memoA === memoB | |
| 80 | } | |
| 81 | memos.position++ | |
| 82 | } | |
| 83 | ||
| 84 | memos.a.set(a, memos.position) | |
| 85 | memos.b.set(b, memos.position) | |
| 86 | ||
| 87 | const equals = sameObject(a, b, opts, memos, aType) | |
| 88 | ||
| 89 | memos.a.delete(a) | |
| 90 | memos.b.delete(b) | |
| 91 | ||
| 92 | return equals | |
| 93 | } | |
| 94 | ||
| 95 | function sameObject (a, b, opts, memos, aType) { | |
| 96 | if (aType === 'Array' || aType === 'Arguments') { | |
| 97 | if (!sameArray(a, b, opts, memos)) return false | |
| 98 | } | |
| 99 | ||
| 100 | if (aType === 'Set') { | |
| 101 | if (!sameSet(a, b, opts, memos)) return false | |
| 102 | } | |
| 103 | ||
| 104 | if (aType === 'Map') { | |
| 105 | if (!sameMap(a, b, opts, memos)) return false | |
| 106 | } | |
| 107 | ||
| 108 | if (!sameKeyValues(a, b, opts, memos)) return false | |
| 109 | ||
| 110 | return true | |
| 111 | } | |
| 112 | ||
| 113 | function getType (o) { | |
| 114 | // Note: it returns 'Null' for null prototypes | |
| 115 | return Object.prototype.toString.call(o).slice(8, -1) | |
| 116 | } |
| … | … | @@ -0,0 +1,18 @@ |
| 1 | module.exports = function findLooseMatchingPrimitives (primitive) { | |
| 2 | switch (typeof primitive) { | |
| 3 | case 'undefined': | |
| 4 | return null | |
| 5 | case 'object': | |
| 6 | return undefined | |
| 7 | case 'symbol': | |
| 8 | return false | |
| 9 | case 'string': | |
| 10 | primitive = +primitive | |
| 11 | // Falls through | |
| 12 | case 'number': | |
| 13 | if (Number.isNaN(primitive)) { | |
| 14 | return false | |
| 15 | } | |
| 16 | } | |
| 17 | return true | |
| 18 | } |
| … | … | @@ -0,0 +1,3 @@ |
| 1 | module.exports = function isPrimitive (value) { | |
| 2 | return value === null || typeof value !== 'object' | |
| 3 | } |
| … | … | @@ -0,0 +1,35 @@ |
| 1 | const same = require('../index.js') | |
| 2 | ||
| 3 | module.exports = function sameArray (a, b, opts, memos) { | |
| 4 | if (a.length !== b.length) return false | |
| 5 | ||
| 6 | for (let i = 0; i < a.length; i++) { | |
| 7 | const aHasProperty = Object.prototype.hasOwnProperty.call(a, i) | |
| 8 | const bHasProperty = Object.prototype.hasOwnProperty.call(b, i) | |
| 9 | ||
| 10 | if (aHasProperty) { | |
| 11 | if (!bHasProperty || !same(a[i], b[i], opts, memos)) { | |
| 12 | return false | |
| 13 | } | |
| 14 | } else if (bHasProperty) { | |
| 15 | return false | |
| 16 | } else { | |
| 17 | // Array is sparse | |
| 18 | const aKeys = Object.keys(a) | |
| 19 | ||
| 20 | for (; i < aKeys.length; i++) { | |
| 21 | const key = aKeys[i] | |
| 22 | if (!Object.prototype.hasOwnProperty.call(b, key) || | |
| 23 | !same(a[key], b[key], opts, memos)) { | |
| 24 | return false | |
| 25 | } | |
| 26 | } | |
| 27 | ||
| 28 | if (aKeys.length !== Object.keys(b).length) return false | |
| 29 | ||
| 30 | return true | |
| 31 | } | |
| 32 | } | |
| 33 | ||
| 34 | return true | |
| 35 | } |
| … | … | @@ -0,0 +1,13 @@ |
| 1 | const same = require('../index.js') | |
| 2 | ||
| 3 | module.exports = function sameIterable (a, b, opts, memos) { | |
| 4 | if (a.length !== b.length) return false | |
| 5 | ||
| 6 | for (let i = 0; i < a.length; i++) { | |
| 7 | if (!same(a[i], b[i], opts, memos)) { | |
| 8 | return false | |
| 9 | } | |
| 10 | } | |
| 11 | ||
| 12 | return true | |
| 13 | } |
| … | … | @@ -0,0 +1,11 @@ |
| 1 | const same = require('../index.js') | |
| 2 | ||
| 3 | module.exports = function sameKeyValues (a, b, opts, memos, keys = null) { | |
| 4 | if (keys === null) keys = Object.keys(a) | |
| 5 | ||
| 6 | for (const key of keys) { | |
| 7 | if (!same(a[key], b[key], opts, memos)) return false | |
| 8 | } | |
| 9 | ||
| 10 | return true | |
| 11 | } |
| … | … | @@ -0,0 +1,13 @@ |
| 1 | module.exports = function sameKeys (a, b, opts, memos) { | |
| 2 | const aKeys = Object.keys(a) | |
| 3 | const bKeys = Object.keys(b) | |
| 4 | if (aKeys.length !== bKeys.length) return false | |
| 5 | ||
| 6 | for (let i = 0; i < aKeys.length; i++) { | |
| 7 | if (!Object.prototype.propertyIsEnumerable.call(b, aKeys[i])) return false | |
| 8 | } | |
| 9 | ||
| 10 | // + if strict, then check symbol properties: https://github.com/nodejs/node/blob/2adea16e394448c4c87b0639514f8babbeb7a080/lib/internal/util/comparisons.js#L290 | |
| 11 | ||
| 12 | return true | |
| 13 | } |
| … | … | @@ -0,0 +1,70 @@ |
| 1 | const same = require('../index.js') | |
| 2 | const isPrimitive = require('./is-primitive.js') | |
| 3 | const findLooseMatchingPrimitives = require('./find-loose-matching-primitives.js') | |
| 4 | ||
| 5 | module.exports = function sameMap (a, b, opts, memos) { | |
| 6 | if (a.size !== b.size) return false | |
| 7 | ||
| 8 | const set = new Set() | |
| 9 | ||
| 10 | for (const [key, item1] of a) { | |
| 11 | if (!isPrimitive(key)) { | |
| 12 | set.add(key) | |
| 13 | } else { | |
| 14 | const item2 = b.get(key) | |
| 15 | if (((item2 === undefined && !b.has(key)) || !same(item1, item2, opts, memos))) { | |
| 16 | if (opts && opts.strict) return false | |
| 17 | ||
| 18 | // Discard string, symbol, undefined, and null keys | |
| 19 | if (!mapMightHaveLoosePrim(a, b, key, item1, memos)) return false | |
| 20 | ||
| 21 | set.add(key) | |
| 22 | } | |
| 23 | } | |
| 24 | } | |
| 25 | ||
| 26 | if (set.size > 0) { | |
| 27 | for (const [key, item] of b) { | |
| 28 | if (!isPrimitive(key)) { | |
| 29 | if (!mapHasEqualEntry(set, a, key, item, opts, memos)) { | |
| 30 | return false | |
| 31 | } | |
| 32 | continue | |
| 33 | } | |
| 34 | ||
| 35 | if (opts && opts.strict) continue | |
| 36 | ||
| 37 | if (a.has(key) && same(a.get(key), item, { strict: false }, memos)) continue | |
| 38 | ||
| 39 | if (mapHasEqualEntry(set, a, key, item, { strict: false }, memos)) continue | |
| 40 | ||
| 41 | return false | |
| 42 | } | |
| 43 | ||
| 44 | return set.size === 0 | |
| 45 | } | |
| 46 | ||
| 47 | return true | |
| 48 | } | |
| 49 | ||
| 50 | function mapHasEqualEntry (set, a, key, bValue, opts, memos) { | |
| 51 | for (const key2 of set) { | |
| 52 | if (same(key, key2, opts, memos) && same(bValue, a.get(key2), opts, memos)) { | |
| 53 | set.delete(key2) | |
| 54 | return true | |
| 55 | } | |
| 56 | } | |
| 57 | ||
| 58 | return false | |
| 59 | } | |
| 60 | ||
| 61 | function mapMightHaveLoosePrim (a, b, primitive, item, memos) { | |
| 62 | const altValue = findLooseMatchingPrimitives(primitive) | |
| 63 | if (altValue != null) return altValue | |
| 64 | ||
| 65 | const bValue = b.get(altValue) | |
| 66 | if (bValue === undefined && !b.has(altValue)) return false | |
| 67 | if (!same(item, bValue, { strict: false }, memos)) return false | |
| 68 | ||
| 69 | return !a.has(altValue) && same(item, bValue, { strict: false }, memos) | |
| 70 | } |
| … | … | @@ -0,0 +1,3 @@ |
| 1 | module.exports = function sameRegExp (a, b) { | |
| 2 | return a.source === b.source && a.flags === b.flags | |
| 3 | } |
| … | … | @@ -0,0 +1,64 @@ |
| 1 | const same = require('../index.js') | |
| 2 | const isPrimitive = require('./is-primitive.js') | |
| 3 | const findLooseMatchingPrimitives = require('./find-loose-matching-primitives.js') | |
| 4 | ||
| 5 | module.exports = function sameSet (a, b, opts, memos) { | |
| 6 | if (a.size !== b.size) return false | |
| 7 | ||
| 8 | const set = new Set() | |
| 9 | ||
| 10 | for (const aValue of a) { | |
| 11 | if (!isPrimitive(aValue)) { | |
| 12 | // Non-null object (non strict only: a not matching primitive) | |
| 13 | set.add(aValue) | |
| 14 | } else if (!b.has(aValue)) { | |
| 15 | if (opts && opts.strict) return false | |
| 16 | ||
| 17 | // Discard string, symbol, undefined, and null values | |
| 18 | if (!setMightHaveLoosePrim(a, b, aValue)) return false | |
| 19 | ||
| 20 | set.add(aValue) | |
| 21 | } | |
| 22 | } | |
| 23 | ||
| 24 | if (set.size > 0) { | |
| 25 | for (const bValue of b) { | |
| 26 | if (!isPrimitive(bValue)) { | |
| 27 | if (!setHasEqualElement(set, bValue, opts, memos)) { | |
| 28 | return false | |
| 29 | } | |
| 30 | continue | |
| 31 | } | |
| 32 | ||
| 33 | if (opts && opts.strict) continue | |
| 34 | ||
| 35 | if (a.has(bValue)) continue | |
| 36 | ||
| 37 | if (setHasEqualElement(set, bValue, opts, memos)) continue | |
| 38 | ||
| 39 | return false | |
| 40 | } | |
| 41 | ||
| 42 | return set.size === 0 | |
| 43 | } | |
| 44 | ||
| 45 | return true | |
| 46 | } | |
| 47 | ||
| 48 | function setMightHaveLoosePrim (a, b, primitive) { | |
| 49 | const altValue = findLooseMatchingPrimitives(primitive) | |
| 50 | if (altValue != null) return altValue | |
| 51 | ||
| 52 | return b.has(altValue) && !a.has(altValue) | |
| 53 | } | |
| 54 | ||
| 55 | function setHasEqualElement (set, val1, opts, memos) { | |
| 56 | for (const val2 of set) { | |
| 57 | if (same(val1, val2, opts, memos)) { | |
| 58 | set.delete(val2) | |
| 59 | return true | |
| 60 | } | |
| 61 | } | |
| 62 | ||
| 63 | return false | |
| 64 | } |
| … | … | @@ -0,0 +1,24 @@ |
| 1 | { | |
| 2 | "name": "same-object", | |
| 3 | "version": "1.0.2", | |
| 4 | "description": "Determine if two objects are deeply equal", | |
| 5 | "main": "index.js", | |
| 6 | "scripts": { | |
| 7 | "test": "standard && brittle test.js --coverage" | |
| 8 | }, | |
| 9 | "repository": { | |
| 10 | "type": "git", | |
| 11 | "url": "https://github.com/holepunchto/same-object.git" | |
| 12 | }, | |
| 13 | "author": "Lucas Barrena (LuKks)", | |
| 14 | "license": "MIT", | |
| 15 | "bugs": { | |
| 16 | "url": "https://github.com/holepunchto/same-object/issues" | |
| 17 | }, | |
| 18 | "homepage": "https://github.com/holepunchto/same-object", | |
| 19 | "devDependencies": { | |
| 20 | "brittle": "^3.1.1", | |
| 21 | "deep-equal": "^2.2.0", | |
| 22 | "standard": "^17.0.0" | |
| 23 | } | |
| 24 | } |
| … | … | @@ -0,0 +1,1399 @@ |
| 1 | const test = require('brittle') | |
| 2 | const sameObject = require('./') | |
| 3 | const deepEqual = require('deep-equal') | |
| 4 | ||
| 5 | test.configure({ bail: true }) | |
| 6 | ||
| 7 | // NOTE: most tests were copied and adapted from deep-equal library | |
| 8 | ||
| 9 | const safeBuffer = typeof Buffer === 'function' ? Buffer.from : null | |
| 10 | const buffersAreTypedArrays = typeof Buffer === 'function' && safeBuffer('') instanceof Uint8Array | |
| 11 | ||
| 12 | test('basic', function (t) { | |
| 13 | t.ok(sameObject(1, '1')) | |
| 14 | t.absent(sameObject(1, '1', { strict: true })) | |
| 15 | }) | |
| 16 | ||
| 17 | test('basic helper', function (t) { | |
| 18 | unlike(t, 1, '1') | |
| 19 | alikeLoosely(t, 1, '1') | |
| 20 | }) | |
| 21 | ||
| 22 | test('equal', function (t) { | |
| 23 | alike(t, | |
| 24 | { a: [2, 3], b: [4] }, | |
| 25 | { a: [2, 3], b: [4] }, | |
| 26 | 'two equal objects' | |
| 27 | ) | |
| 28 | ||
| 29 | alikeLoosely(t, | |
| 30 | { a: 2, b: '4' }, | |
| 31 | { a: 2, b: 4 }, | |
| 32 | 'two loosely equal, strictly inequal objects' | |
| 33 | ) | |
| 34 | ||
| 35 | unlikeLoosely(t, | |
| 36 | { a: 2, b: 4 }, | |
| 37 | { a: 2, B: 4 }, | |
| 38 | 'two inequal objects' | |
| 39 | ) | |
| 40 | ||
| 41 | alikeLoosely(t, | |
| 42 | '-000', | |
| 43 | false, | |
| 44 | '`false` and `"-000"`' | |
| 45 | ) | |
| 46 | }) | |
| 47 | ||
| 48 | test('Maps', function (t) { | |
| 49 | alike(t, | |
| 50 | new Map([['a', 1], ['b', 2]]), | |
| 51 | new Map([['b', 2], ['a', 1]]), | |
| 52 | 'two equal Maps' | |
| 53 | ) | |
| 54 | ||
| 55 | unlikeLoosely(t, | |
| 56 | new Map([['a', [1, 2]]]), | |
| 57 | new Map([['a', [2, 1]]]), | |
| 58 | 'two Maps with inequal values on the same key' | |
| 59 | ) | |
| 60 | ||
| 61 | unlikeLoosely(t, | |
| 62 | new Map([['a', 1]]), | |
| 63 | new Map([['b', 1]]), | |
| 64 | 'two inequal Maps' | |
| 65 | ) | |
| 66 | ||
| 67 | alike(t, | |
| 68 | new Map([[{}, 3], [{}, 2], [{}, 1]]), | |
| 69 | new Map([[{}, 1], [{}, 2], [{}, 3]]), | |
| 70 | 'two equal Maps in different orders with object keys' | |
| 71 | ) | |
| 72 | ||
| 73 | alikeLoosely(t, | |
| 74 | new Map([[undefined, undefined]]), | |
| 75 | new Map([[undefined, null]]), | |
| 76 | 'undefined keys, nullish values, loosely equal, strictly inequal' | |
| 77 | ) | |
| 78 | ||
| 79 | alike(t, | |
| 80 | new Map([[{}, null], [true, 2], [{}, 1], [undefined, {}]]), | |
| 81 | new Map([[{}, 1], [true, 2], [{}, null], [undefined, {}]]), | |
| 82 | 'two equal Maps in different orders with primitive keys' | |
| 83 | ) | |
| 84 | ||
| 85 | alike(t, | |
| 86 | new Map([[false, 3], [{}, 2], [{}, 1]]), | |
| 87 | new Map([[{}, 1], [{}, 2], [false, 3]]), | |
| 88 | 'two equal Maps in different orders with a mix of keys' | |
| 89 | ) | |
| 90 | ||
| 91 | alikeLoosely(t, | |
| 92 | new Map([[null, undefined]]), | |
| 93 | new Map([[null, null]]), | |
| 94 | 'null keys, nullish values, loosely equal, strictly inequal' | |
| 95 | ) | |
| 96 | ||
| 97 | alikeLoosely(t, | |
| 98 | new Map([[undefined, 3]]), | |
| 99 | new Map([[null, 3]]), | |
| 100 | 'nullish keys, loosely equal, strictly inequal' | |
| 101 | ) | |
| 102 | ||
| 103 | alike(t, | |
| 104 | new Map([[{}, null], [true, 2], [{}, 1], [undefined, {}]]), | |
| 105 | new Map([[{}, 1], [true, 2], [{}, null], [undefined, {}]]), | |
| 106 | 'two equal Maps in different orders with primitive keys' | |
| 107 | ) | |
| 108 | ||
| 109 | alike(t, | |
| 110 | new Map([[false, 3], [{}, 2], [{}, 1]]), | |
| 111 | new Map([[{}, 1], [{}, 2], [false, 3]]), | |
| 112 | 'two equal Maps in different orders with a mix of keys' | |
| 113 | ) | |
| 114 | ||
| 115 | unlikeLoosely(t, | |
| 116 | new Map(), | |
| 117 | new Map([[{}, 1]]), | |
| 118 | 'two inequal Maps' | |
| 119 | ) | |
| 120 | ||
| 121 | unlikeLoosely(t, | |
| 122 | new Map([[{}, null], [false, 3]]), | |
| 123 | new Map([[{}, null], [true, 2]]), | |
| 124 | 'two inequal maps, same size, primitive key, start with object key' | |
| 125 | ) | |
| 126 | ||
| 127 | unlikeLoosely(t, | |
| 128 | new Map([[false, 3], [{}, null]]), | |
| 129 | new Map([[true, 2], [{}, null]]), | |
| 130 | 'two inequal maps, same size, primitive key, start with primitive key' | |
| 131 | ) | |
| 132 | ||
| 133 | alikeLoosely(t, | |
| 134 | new Map([[undefined, null], ['+000', 2]]), | |
| 135 | new Map([[null, undefined], [false, '2']]), | |
| 136 | 'primitive comparisons' | |
| 137 | ) | |
| 138 | }) | |
| 139 | ||
| 140 | // + | |
| 141 | test('WeakMaps', function (t) { | |
| 142 | alike(t, | |
| 143 | new WeakMap([[Object, null], [Function, true]]), | |
| 144 | new WeakMap([[Function, true], [Object, null]]), | |
| 145 | 'two equal WeakMaps' | |
| 146 | ) | |
| 147 | ||
| 148 | alike(t, | |
| 149 | new WeakMap([[Object, null]]), | |
| 150 | new WeakMap([[Object, true]]), | |
| 151 | 'two WeakMaps with inequal values on the same key' | |
| 152 | ) | |
| 153 | ||
| 154 | alike(t, | |
| 155 | new WeakMap([[Object, null], [Function, true]]), | |
| 156 | new WeakMap([[Object, null]]), | |
| 157 | 'two inequal WeakMaps' | |
| 158 | ) | |
| 159 | }) | |
| 160 | ||
| 161 | test('Sets', function (t) { | |
| 162 | alike(t, | |
| 163 | new Set(['a', 1, 'b', 2]), | |
| 164 | new Set(['b', 2, 'a', 1]), | |
| 165 | 'two equal Sets' | |
| 166 | ) | |
| 167 | ||
| 168 | unlikeLoosely(t, | |
| 169 | new Set(['a', 1]), | |
| 170 | new Set(['b', 1]), | |
| 171 | 'two inequal Sets' | |
| 172 | ) | |
| 173 | ||
| 174 | alike(t, | |
| 175 | new Set([{}, 1, {}, {}, 2]), | |
| 176 | new Set([{}, 1, {}, 2, {}]), | |
| 177 | 'two equal Sets in different orders' | |
| 178 | ) | |
| 179 | ||
| 180 | unlikeLoosely(t, | |
| 181 | new Set(), | |
| 182 | new Set([1]), | |
| 183 | 'two inequally sized Sets' | |
| 184 | ) | |
| 185 | ||
| 186 | alikeLoosely(t, | |
| 187 | new Set([{ a: 1 }, 2]), | |
| 188 | new Set(['2', { a: '1' }]), | |
| 189 | 'two loosely equal, strictly inequal Sets' | |
| 190 | ) | |
| 191 | ||
| 192 | unlikeLoosely(t, | |
| 193 | new Set([{ a: 1 }, 2]), | |
| 194 | new Set(['2', { a: 2 }]), | |
| 195 | 'two inequal Sets' | |
| 196 | ) | |
| 197 | ||
| 198 | alikeLoosely(t, | |
| 199 | new Set([null, '', 1, 5, 2, false]), | |
| 200 | new Set([undefined, 0, '5', true, '2', '-000']), | |
| 201 | 'more primitive comparisons' | |
| 202 | ) | |
| 203 | ||
| 204 | alike(t, | |
| 205 | new Set([1, 2]), | |
| 206 | new Set([2, 1]), | |
| 207 | 'primitives in different keys' | |
| 208 | ) | |
| 209 | ||
| 210 | alike(t, | |
| 211 | new Set([{ a: 1 }, { b: 2 }]), | |
| 212 | new Set([{ b: 2 }, { a: 1 }]), | |
| 213 | 'object values in different keys' | |
| 214 | ) | |
| 215 | ||
| 216 | alike(t, | |
| 217 | new Set([new Set([1, 2]), new Set([3, 4])]), | |
| 218 | new Set([new Set([4, 3]), new Set([2, 1])]), | |
| 219 | 'Set of Sets, all in different keys' | |
| 220 | ) | |
| 221 | ||
| 222 | unlikeLoosely(t, | |
| 223 | new Set([{ a: 1 }, 1]), | |
| 224 | new Set([{ a: 1 }, 2]), | |
| 225 | 'non primitive first, and non alike primitive later' | |
| 226 | ) | |
| 227 | ||
| 228 | alikeLoosely(t, | |
| 229 | new Set([{ a: 1 }, Infinity]), | |
| 230 | new Set([{ a: 1 }, Infinity]), | |
| 231 | 'primitive that is not loose' | |
| 232 | ) | |
| 233 | ||
| 234 | unlikeLoosely(t, | |
| 235 | new Set([Symbol.for('hi')]), | |
| 236 | new Set([Symbol.for('hi2')]), | |
| 237 | 'different symbols in a Set' | |
| 238 | ) | |
| 239 | }) | |
| 240 | ||
| 241 | test('Set and Map', function (t) { | |
| 242 | unlikeLoosely(t, | |
| 243 | new Set(), | |
| 244 | new Map(), | |
| 245 | 'Map and Set' | |
| 246 | ) | |
| 247 | ||
| 248 | // + | |
| 249 | /* const maplikeSet = new Set() | |
| 250 | Object.defineProperty(maplikeSet, 'constructor', { enumerable: false, value: Map }) | |
| 251 | maplikeSet.__proto__ = Map.prototype // eslint-disable-line no-proto | |
| 252 | unlikeLoosely(t, | |
| 253 | maplikeSet, | |
| 254 | new Map(), | |
| 255 | 'Map-like Set, and Map' | |
| 256 | ) */ | |
| 257 | }) | |
| 258 | ||
| 259 | // + | |
| 260 | test('WeakSets', function (t) { | |
| 261 | alike(t, | |
| 262 | new WeakSet([Object, Function]), | |
| 263 | new WeakSet([Function, Object]), | |
| 264 | 'two equal WeakSets' | |
| 265 | ) | |
| 266 | ||
| 267 | alike(t, | |
| 268 | new WeakSet([Object, Function]), | |
| 269 | new WeakSet([Object]), | |
| 270 | 'two inequal WeakSets' | |
| 271 | ) | |
| 272 | }) | |
| 273 | ||
| 274 | test('not equal', function (t) { | |
| 275 | unlikeLoosely(t, | |
| 276 | { x: 5, y: [6] }, | |
| 277 | { x: 5, y: 6 }, | |
| 278 | 'two inequal objects are' | |
| 279 | ) | |
| 280 | }) | |
| 281 | ||
| 282 | test('nested nulls', function (t) { | |
| 283 | alike(t, | |
| 284 | [null, null, null], | |
| 285 | [null, null, null], | |
| 286 | 'same-length arrays of nulls' | |
| 287 | ) | |
| 288 | }) | |
| 289 | ||
| 290 | test('objects with strings vs numbers', function (t) { | |
| 291 | alikeLoosely(t, | |
| 292 | [{ a: 3 }, { b: 4 }], | |
| 293 | [{ a: '3' }, { b: '4' }], | |
| 294 | 'objects with equivalent string/number values' | |
| 295 | ) | |
| 296 | }) | |
| 297 | ||
| 298 | test('non-objects', function (t) { | |
| 299 | alike(t, 3, 3, 'same numbers', true, true, true) | |
| 300 | alike(t, 'beep', 'beep', 'same strings', true, true, true) | |
| 301 | alikeLoosely(t, '3', 3, 'numeric string and number', true, false) | |
| 302 | unlikeLoosely(t, '3', [3], 'numeric string and array containing number', false, false) | |
| 303 | unlikeLoosely(t, 3, [3], 'number and array containing number', false, false) | |
| 304 | }) | |
| 305 | ||
| 306 | test('infinities', function (t) { | |
| 307 | alike(t, Infinity, Infinity, '∞ and ∞', true, true, true) | |
| 308 | alike(t, -Infinity, -Infinity, '-∞ and -∞', true, true, true) | |
| 309 | unlikeLoosely(t, Infinity, -Infinity, '∞ and -∞', false, false) | |
| 310 | }) | |
| 311 | ||
| 312 | test('arguments class', function (t) { | |
| 313 | function getArgs () { | |
| 314 | return arguments | |
| 315 | } | |
| 316 | ||
| 317 | alike(t, | |
| 318 | getArgs(1, 2, 3), | |
| 319 | getArgs(1, 2, 3), | |
| 320 | 'equivalent arguments objects are equal' | |
| 321 | ) | |
| 322 | ||
| 323 | unlikeLoosely(t, | |
| 324 | getArgs(1, 2, 3), | |
| 325 | [1, 2, 3], | |
| 326 | 'array and arguments with same contents' | |
| 327 | ) | |
| 328 | ||
| 329 | const args = getArgs() | |
| 330 | const notArgs = tag({ length: 0 }, 'Arguments') | |
| 331 | unlikeLoosely(t, | |
| 332 | args, | |
| 333 | notArgs, | |
| 334 | 'args and similar arraylike object' | |
| 335 | ) | |
| 336 | }) | |
| 337 | ||
| 338 | test('Dates', function (t) { | |
| 339 | const d0 = new Date(1387585278000) | |
| 340 | const d1 = new Date('Fri Dec 20 2013 16:21:18 GMT-0800 (PST)') | |
| 341 | ||
| 342 | alike(t, d0, d1, 'two Dates with the same timestamp', true, true) | |
| 343 | ||
| 344 | d1.a = true | |
| 345 | ||
| 346 | unlikeLoosely(t, d0, d1, 'two Dates with the same timestamp but different own properties', false, false) | |
| 347 | ||
| 348 | t.test('overriding `getTime`', function (st) { | |
| 349 | const a = new Date('2000') | |
| 350 | const b = new Date('2000') | |
| 351 | Object.defineProperty(a, 'getTime', { value: function () { return 5 } }) | |
| 352 | alike(st, a, b, 'two Dates with the same timestamp but one has overridden `getTime`', true, true) | |
| 353 | }) | |
| 354 | ||
| 355 | // + | |
| 356 | /* t.test('fake Date', { skip: !hasDunderProto }, function (st) { | |
| 357 | const a = new Date(2000) | |
| 358 | const b = tag(Object.create( | |
| 359 | a.__proto__, // eslint-disable-line no-proto | |
| 360 | Object.getOwnPropertyDescriptors(a) | |
| 361 | ), 'Date') | |
| 362 | ||
| 363 | unlikeLoosely(st, | |
| 364 | a, | |
| 365 | b, | |
| 366 | 'Date, and fake Date' | |
| 367 | ) | |
| 368 | }) */ | |
| 369 | ||
| 370 | const a = new Date('2000') | |
| 371 | const b = new Date('2000') | |
| 372 | b.foo = true | |
| 373 | unlikeLoosely(t, | |
| 374 | a, | |
| 375 | b, | |
| 376 | 'two identical Dates, one with an extra property' | |
| 377 | ) | |
| 378 | ||
| 379 | unlikeLoosely(t, | |
| 380 | new Date('2000'), | |
| 381 | new Date('2001'), | |
| 382 | 'two inequal Dates' | |
| 383 | ) | |
| 384 | }) | |
| 385 | ||
| 386 | test('buffers', { skip: typeof Buffer !== 'function' }, function (t) { | |
| 387 | alike(t, | |
| 388 | safeBuffer('xyz'), | |
| 389 | safeBuffer('xyz'), | |
| 390 | 'buffers with same contents are equal' | |
| 391 | ) | |
| 392 | ||
| 393 | unlikeLoosely(t, | |
| 394 | safeBuffer('xyz'), | |
| 395 | safeBuffer('xyy'), | |
| 396 | 'buffers with same length and different contents are inequal' | |
| 397 | ) | |
| 398 | ||
| 399 | unlikeLoosely(t, | |
| 400 | safeBuffer('xyz'), | |
| 401 | safeBuffer('xy'), | |
| 402 | 'buffers with different length are inequal' | |
| 403 | ) | |
| 404 | ||
| 405 | unlikeLoosely(t, | |
| 406 | safeBuffer('abc'), | |
| 407 | safeBuffer('xyz'), | |
| 408 | 'buffers with different contents' | |
| 409 | ) | |
| 410 | ||
| 411 | const emptyBuffer = safeBuffer('') | |
| 412 | ||
| 413 | unlikeLoosely(t, | |
| 414 | emptyBuffer, | |
| 415 | [], | |
| 416 | 'empty buffer and empty array' | |
| 417 | ) | |
| 418 | ||
| 419 | t.test('bufferlikes', function (st) { | |
| 420 | const fakeBuffer = { | |
| 421 | 0: 'a', | |
| 422 | length: 1, | |
| 423 | __proto__: emptyBuffer.__proto__, // eslint-disable-line no-proto | |
| 424 | copy: emptyBuffer.copy, | |
| 425 | slice: emptyBuffer.slice | |
| 426 | } | |
| 427 | Object.defineProperty(fakeBuffer, '0', { enumerable: false }) | |
| 428 | Object.defineProperty(fakeBuffer, 'length', { enumerable: false }) | |
| 429 | Object.defineProperty(fakeBuffer, 'copy', { enumerable: false }) | |
| 430 | Object.defineProperty(fakeBuffer, 'slice', { enumerable: false }) | |
| 431 | ||
| 432 | unlikeLoosely(st, | |
| 433 | safeBuffer('a'), | |
| 434 | fakeBuffer, | |
| 435 | 'real buffer, and mildly fake buffer' | |
| 436 | ) | |
| 437 | ||
| 438 | st.test('bufferlike', function (s2t) { | |
| 439 | const bufferlike = buffersAreTypedArrays ? new Uint8Array() : {} | |
| 440 | Object.defineProperty(bufferlike, 'length', { | |
| 441 | enumerable: false, | |
| 442 | value: bufferlike.length || 0 | |
| 443 | }) | |
| 444 | Object.defineProperty(bufferlike, 'copy', { | |
| 445 | enumerable: false, | |
| 446 | value: emptyBuffer.copy | |
| 447 | }) | |
| 448 | bufferlike.__proto__ = emptyBuffer.__proto__ // eslint-disable-line no-proto | |
| 449 | ||
| 450 | alike(s2t, | |
| 451 | emptyBuffer, | |
| 452 | bufferlike, | |
| 453 | 'empty buffer and empty bufferlike' | |
| 454 | ) | |
| 455 | s2t.end() | |
| 456 | }) | |
| 457 | ||
| 458 | st.end() | |
| 459 | }) | |
| 460 | ||
| 461 | t.end() | |
| 462 | }) | |
| 463 | ||
| 464 | test('DataView', function (t) { | |
| 465 | const view1 = new DataView(new ArrayBuffer(10)) | |
| 466 | const view2 = new DataView(new ArrayBuffer(10)) | |
| 467 | ||
| 468 | alike(t, | |
| 469 | view1, | |
| 470 | view2, | |
| 471 | 'two equals DataViews' | |
| 472 | ) | |
| 473 | ||
| 474 | view1[3] = 7 | |
| 475 | ||
| 476 | unlike(t, | |
| 477 | view1, | |
| 478 | view2, | |
| 479 | 'two inequal DataViews' | |
| 480 | ) | |
| 481 | }) | |
| 482 | ||
| 483 | test('Arrays', function (t) { | |
| 484 | const a = [] | |
| 485 | const b = [] | |
| 486 | b.foo = true | |
| 487 | ||
| 488 | unlikeLoosely(t, | |
| 489 | a, | |
| 490 | b, | |
| 491 | 'two identical arrays, one with an extra property' | |
| 492 | ) | |
| 493 | ||
| 494 | const c = [undefined, 'test'] | |
| 495 | const d = [] | |
| 496 | d[1] = 'test' | |
| 497 | unlikeLoosely(t, c, d, 'sparse array') | |
| 498 | ||
| 499 | const e = [undefined, 'test', undefined, undefined, 'test2', undefined] | |
| 500 | const f = [] | |
| 501 | e[1] = 'test' | |
| 502 | e[4] = 'test2' | |
| 503 | unlikeLoosely(t, e, f, 'sparse array') | |
| 504 | ||
| 505 | const g = new Array(10) | |
| 506 | const h = new Array(10) | |
| 507 | g[2] = 'test' | |
| 508 | g[5] = 'test2' | |
| 509 | g[8] = 'test3' | |
| 510 | h[2] = 'test' | |
| 511 | h[5] = 'test2' | |
| 512 | h[8] = 'test3' | |
| 513 | alike(t, g, h, 'sparse array') | |
| 514 | ||
| 515 | t.end() | |
| 516 | }) | |
| 517 | ||
| 518 | test('booleans', function (t) { | |
| 519 | alike(t, | |
| 520 | true, | |
| 521 | true, | |
| 522 | 'trues' | |
| 523 | ) | |
| 524 | ||
| 525 | alike(t, | |
| 526 | false, | |
| 527 | false, | |
| 528 | 'falses' | |
| 529 | ) | |
| 530 | ||
| 531 | unlikeLoosely(t, | |
| 532 | true, | |
| 533 | false, | |
| 534 | 'true and false' | |
| 535 | ) | |
| 536 | ||
| 537 | t.end() | |
| 538 | }) | |
| 539 | ||
| 540 | test('booleans and arrays', function (t) { | |
| 541 | unlikeLoosely(t, | |
| 542 | true, | |
| 543 | [], | |
| 544 | 'true and an empty array', | |
| 545 | false, | |
| 546 | false | |
| 547 | ) | |
| 548 | unlikeLoosely(t, | |
| 549 | false, | |
| 550 | [], | |
| 551 | 'false and an empty array' | |
| 552 | ) | |
| 553 | t.end() | |
| 554 | }) | |
| 555 | ||
| 556 | test('arrays initiated', function (t) { | |
| 557 | const a0 = [ | |
| 558 | undefined, | |
| 559 | null, | |
| 560 | -1, | |
| 561 | 0, | |
| 562 | 1, | |
| 563 | false, | |
| 564 | true, | |
| 565 | undefined, | |
| 566 | '', | |
| 567 | 'abc', | |
| 568 | null, | |
| 569 | undefined | |
| 570 | ] | |
| 571 | const a1 = [ | |
| 572 | undefined, | |
| 573 | null, | |
| 574 | -1, | |
| 575 | 0, | |
| 576 | 1, | |
| 577 | false, | |
| 578 | true, | |
| 579 | undefined, | |
| 580 | '', | |
| 581 | 'abc', | |
| 582 | null, | |
| 583 | undefined | |
| 584 | ] | |
| 585 | ||
| 586 | alike(t, | |
| 587 | a0, | |
| 588 | a1, | |
| 589 | 'arrays with equal contents are equal' | |
| 590 | ) | |
| 591 | t.end() | |
| 592 | }) | |
| 593 | ||
| 594 | test('arrays assigned', function (t) { | |
| 595 | const a0 = [ | |
| 596 | undefined, | |
| 597 | null, | |
| 598 | -1, | |
| 599 | 0, | |
| 600 | 1, | |
| 601 | false, | |
| 602 | true, | |
| 603 | undefined, | |
| 604 | '', | |
| 605 | 'abc', | |
| 606 | null, | |
| 607 | undefined | |
| 608 | ] | |
| 609 | const a1 = [] | |
| 610 | ||
| 611 | a1[0] = undefined | |
| 612 | a1[1] = null | |
| 613 | a1[2] = -1 | |
| 614 | a1[3] = 0 | |
| 615 | a1[4] = 1 | |
| 616 | a1[5] = false | |
| 617 | a1[6] = true | |
| 618 | a1[7] = undefined | |
| 619 | a1[8] = '' | |
| 620 | a1[9] = 'abc' | |
| 621 | a1[10] = null | |
| 622 | a1[11] = undefined | |
| 623 | a1.length = 12 | |
| 624 | ||
| 625 | alike(t, a0, a1, 'a literal array and an assigned array', true, true) | |
| 626 | t.end() | |
| 627 | }) | |
| 628 | ||
| 629 | test('arrays push', function (t) { | |
| 630 | const a0 = [ | |
| 631 | undefined, | |
| 632 | null, | |
| 633 | -1, | |
| 634 | 0, | |
| 635 | 1, | |
| 636 | false, | |
| 637 | true, | |
| 638 | undefined, | |
| 639 | '', | |
| 640 | 'abc', | |
| 641 | null, | |
| 642 | undefined | |
| 643 | ] | |
| 644 | const a1 = [] | |
| 645 | ||
| 646 | a1.push(undefined) | |
| 647 | a1.push(null) | |
| 648 | a1.push(-1) | |
| 649 | a1.push(0) | |
| 650 | a1.push(1) | |
| 651 | a1.push(false) | |
| 652 | a1.push(true) | |
| 653 | a1.push(undefined) | |
| 654 | a1.push('') | |
| 655 | a1.push('abc') | |
| 656 | a1.push(null) | |
| 657 | a1.push(undefined) | |
| 658 | a1.length = 12 | |
| 659 | ||
| 660 | alike(t, a0, a1, 'a literal array and a pushed array', true, true) | |
| 661 | t.end() | |
| 662 | }) | |
| 663 | ||
| 664 | test('null == undefined', function (t) { | |
| 665 | alikeLoosely(t, null, undefined, 'null and undefined', true, false) | |
| 666 | alikeLoosely(t, [null], [undefined], '[null] and [undefined]', true, false) | |
| 667 | ||
| 668 | t.end() | |
| 669 | }) | |
| 670 | ||
| 671 | // node 14 changed `deepEqual` to make two NaNs loosely equal | |
| 672 | test('NaNs', function (t) { | |
| 673 | alike(t, | |
| 674 | NaN, | |
| 675 | NaN, | |
| 676 | 'two NaNs' | |
| 677 | ) | |
| 678 | ||
| 679 | alike(t, | |
| 680 | { a: NaN }, | |
| 681 | { a: NaN }, | |
| 682 | 'two equiv objects with a NaN value' | |
| 683 | ) | |
| 684 | ||
| 685 | unlikeLoosely(t, NaN, 1, 'NaN and 1', false, false) | |
| 686 | ||
| 687 | t.end() | |
| 688 | }) | |
| 689 | ||
| 690 | test('zeroes', function (t) { | |
| 691 | alikeLoosely(t, 0, -0, '0 and -0', true, false) | |
| 692 | unlike(t, 0, -0, '0 and -0', true, false) | |
| 693 | ||
| 694 | alikeLoosely(t, { a: 0 }, { a: -0 }, 'two objects with a same-keyed 0/-0 value', true, false) | |
| 695 | unlike(t, { a: 0 }, { a: -0 }, 'two objects with a same-keyed 0/-0 value', true, false) | |
| 696 | ||
| 697 | t.end() | |
| 698 | }) | |
| 699 | ||
| 700 | test('Object.create', function (t) { | |
| 701 | const a = { a: 'A' } | |
| 702 | const b = Object.create(a) | |
| 703 | b.b = 'B' | |
| 704 | const c = Object.create(a) | |
| 705 | c.b = 'C' | |
| 706 | ||
| 707 | unlikeLoosely(t, | |
| 708 | b, | |
| 709 | c, | |
| 710 | 'two objects with the same [[Prototype]] but a different own property' | |
| 711 | ) | |
| 712 | ||
| 713 | t.end() | |
| 714 | }) | |
| 715 | ||
| 716 | test('Object.create(null)', function (t) { | |
| 717 | alike(t, | |
| 718 | Object.create(null), | |
| 719 | Object.create(null), | |
| 720 | 'two empty null objects' | |
| 721 | ) | |
| 722 | ||
| 723 | alike(t, | |
| 724 | Object.create(null, { a: { value: 'b' } }), | |
| 725 | Object.create(null, { a: { value: 'b' } }), | |
| 726 | 'two null objects with the same property pair' | |
| 727 | ) | |
| 728 | ||
| 729 | t.end() | |
| 730 | }) | |
| 731 | ||
| 732 | test('regexes vs dates', function (t) { | |
| 733 | const d = new Date(1387585278000) | |
| 734 | const r = /abc/ | |
| 735 | ||
| 736 | unlikeLoosely(t, d, r, 'Date and RegExp', false, false) | |
| 737 | ||
| 738 | t.end() | |
| 739 | }) | |
| 740 | ||
| 741 | test('regexp', function (t) { | |
| 742 | unlikeLoosely(t, /abc/, /xyz/, 'two different regexes', false, false) | |
| 743 | alike(t, /abc/, /abc/, 'two abc regexes', true, true, false) | |
| 744 | alike(t, /xyz/, /xyz/, 'two xyz regexes', true, true, false) | |
| 745 | unlike(t, /abc/i, /def/g, 'two xyz regexes') | |
| 746 | ||
| 747 | // + | |
| 748 | /* t.test('fake RegExp', function (st) { | |
| 749 | const a = /abc/g | |
| 750 | const b = tag(Object.create( | |
| 751 | a.__proto__, // eslint-disable-line no-proto | |
| 752 | Object.getOwnPropertyDescriptors(a) | |
| 753 | ), 'RegExp') | |
| 754 | ||
| 755 | unlikeLoosely(st,a, b, 'regex and fake regex', false, false) | |
| 756 | ||
| 757 | st.end() | |
| 758 | }) */ | |
| 759 | ||
| 760 | const a = /abc/gi | |
| 761 | const b = /abc/gi | |
| 762 | b.foo = true | |
| 763 | unlikeLoosely(t, | |
| 764 | a, | |
| 765 | b, | |
| 766 | 'two identical regexes, one with an extra property' | |
| 767 | ) | |
| 768 | ||
| 769 | const c = /abc/g | |
| 770 | const d = /abc/i | |
| 771 | unlikeLoosely(t, | |
| 772 | c, | |
| 773 | d, | |
| 774 | 'two regexes with the same source but different flags' | |
| 775 | ) | |
| 776 | ||
| 777 | t.end() | |
| 778 | }) | |
| 779 | ||
| 780 | test('object literals', function (t) { | |
| 781 | alikeLoosely(t, | |
| 782 | { prototype: 2 }, | |
| 783 | { prototype: '2' }, | |
| 784 | 'two loosely equal, strictly inequal prototype properties' | |
| 785 | ) | |
| 786 | ||
| 787 | t.end() | |
| 788 | }) | |
| 789 | ||
| 790 | test('arrays and objects', function (t) { | |
| 791 | unlikeLoosely(t, [], {}, 'empty array and empty object', false, false) | |
| 792 | unlikeLoosely(t, [], { length: 0 }, 'empty array and empty arraylike object', false, false) | |
| 793 | unlikeLoosely(t, [1], { 0: 1 }, 'array and similar object', false, false) | |
| 794 | ||
| 795 | t.end() | |
| 796 | }) | |
| 797 | ||
| 798 | test('functions', function (t) { | |
| 799 | function f () {} | |
| 800 | ||
| 801 | alike(t, f, f, 'a function and itself', true, true, true) | |
| 802 | alike(t, [f], [f], 'a function and itself in an array', true, true, true) | |
| 803 | ||
| 804 | unlikeLoosely(t, function () {}, function () {}, 'two distinct functions', false, false, true) | |
| 805 | unlikeLoosely(t, [function () {}], [function () {}], 'two distinct functions in an array', false, false, true) | |
| 806 | ||
| 807 | unlikeLoosely(t, f, {}, 'function and object', false, false, true) | |
| 808 | unlikeLoosely(t, [f], [{}], 'function and object in an array', false, false, true) | |
| 809 | ||
| 810 | t.end() | |
| 811 | }) | |
| 812 | ||
| 813 | test('Errors', function (t) { | |
| 814 | alike(t, new Error('xyz'), new Error('xyz'), 'two errors of the same type with the same message', true, true, false) | |
| 815 | unlikeLoosely(t, new Error('xyz'), new TypeError('xyz'), 'two errors of different types with the same message', false, false) | |
| 816 | unlikeLoosely(t, new Error('xyz'), new Error('zyx'), 'two errors of the same type with a different message', false, false) | |
| 817 | ||
| 818 | // + | |
| 819 | /* t.test('errorlike', { skip: !Object.defineProperty }, function (st) { | |
| 820 | const err = new Error('foo') | |
| 821 | // TODO: add `__proto__` when brand check is available | |
| 822 | const errorlike = tag({ message: err.message, stack: err.stack, name: err.name, constructor: err.constructor }, 'Error') | |
| 823 | Object.defineProperty(errorlike, 'message', { enumerable: false }) | |
| 824 | Object.defineProperty(errorlike, 'stack', { enumerable: false }) | |
| 825 | Object.defineProperty(errorlike, 'name', { enumerable: false }) | |
| 826 | Object.defineProperty(errorlike, 'constructor', { enumerable: false }) | |
| 827 | st.absent(errorlike instanceof Error) | |
| 828 | st.ok(err instanceof Error) | |
| 829 | unlikeLoosely(st, | |
| 830 | err, | |
| 831 | errorlike, | |
| 832 | 'error, and errorlike object' | |
| 833 | ) | |
| 834 | ||
| 835 | st.end() | |
| 836 | }) */ | |
| 837 | ||
| 838 | // + | |
| 839 | unlikeLoosely(t, | |
| 840 | new Error('a'), | |
| 841 | Object.assign(new Error('a'), { code: 10 }), | |
| 842 | 'two otherwise equal errors with different own properties' | |
| 843 | ) | |
| 844 | ||
| 845 | // + | |
| 846 | /* t.test('fake error', { skip: !process.env.ASSERT || !hasDunderProto }, function (st) { | |
| 847 | const a = tag({ | |
| 848 | __proto__: null | |
| 849 | }, 'Error') | |
| 850 | const b = new RangeError('abc') | |
| 851 | b.__proto__ = null // eslint-disable-line no-proto | |
| 852 | ||
| 853 | unlikeLoosely(st, | |
| 854 | a, | |
| 855 | b, | |
| 856 | 'null object faking as an Error, RangeError with null proto' | |
| 857 | ) | |
| 858 | st.end() | |
| 859 | }) */ | |
| 860 | ||
| 861 | t.end() | |
| 862 | }) | |
| 863 | ||
| 864 | test('object and null', function (t) { | |
| 865 | unlikeLoosely(t, | |
| 866 | {}, | |
| 867 | null, | |
| 868 | 'null and an object' | |
| 869 | ) | |
| 870 | ||
| 871 | t.end() | |
| 872 | }) | |
| 873 | ||
| 874 | test('error = Object', function (t) { | |
| 875 | unlikeLoosely(t, | |
| 876 | new Error('a'), | |
| 877 | { message: 'a' } | |
| 878 | ) | |
| 879 | ||
| 880 | t.end() | |
| 881 | }) | |
| 882 | ||
| 883 | test('[[Prototypes]]', function (t) { | |
| 884 | function C () {} | |
| 885 | const instance = new C() | |
| 886 | delete instance.constructor | |
| 887 | ||
| 888 | alikeLoosely(t, {}, instance, 'two identical objects with different [[Prototypes]]', true, false) | |
| 889 | ||
| 890 | t.test('Dates with different prototypes', function (st) { | |
| 891 | const d1 = new Date(0) | |
| 892 | const d2 = new Date(0) | |
| 893 | ||
| 894 | alike(st, d1, d2, 'two dates with the same timestamp', true, true) | |
| 895 | ||
| 896 | const newProto = { | |
| 897 | __proto__: Date.prototype | |
| 898 | } | |
| 899 | d2.__proto__ = newProto // eslint-disable-line no-proto | |
| 900 | st.ok(d2 instanceof Date, 'd2 is still a Date instance after tweaking [[Prototype]]') | |
| 901 | ||
| 902 | alikeLoosely(st, d1, d2, 'two dates with the same timestamp and different [[Prototype]]', true, false) | |
| 903 | ||
| 904 | st.end() | |
| 905 | }) | |
| 906 | ||
| 907 | t.end() | |
| 908 | }) | |
| 909 | ||
| 910 | test('toStringTag', function (t) { | |
| 911 | const o1 = {} | |
| 912 | t.is(Object.prototype.toString.call(o1), '[object Object]', 'o1: Symbol.toStringTag works') | |
| 913 | ||
| 914 | const o2 = {} | |
| 915 | t.is(Object.prototype.toString.call(o2), '[object Object]', 'o2: original Symbol.toStringTag works') | |
| 916 | ||
| 917 | alike(t, o1, o2, 'two normal empty objects', true, true) | |
| 918 | ||
| 919 | o2[Symbol.toStringTag] = 'jifasnif' | |
| 920 | t.is(Object.prototype.toString.call(o2), '[object jifasnif]', 'o2: modified Symbol.toStringTag works') | |
| 921 | ||
| 922 | unlikeLoosely(t, o1, o2, 'two normal empty objects with different toStringTags', false, false) | |
| 923 | ||
| 924 | t.end() | |
| 925 | }) | |
| 926 | ||
| 927 | test('boxed primitives', function (t) { | |
| 928 | unlikeLoosely(t, Object(false), false, 'boxed and primitive `false`', false, false) | |
| 929 | unlikeLoosely(t, Object(true), true, 'boxed and primitive `true`', false, false) | |
| 930 | unlikeLoosely(t, Object(3), 3, 'boxed and primitive `3`', false, false) | |
| 931 | unlikeLoosely(t, Object(NaN), NaN, 'boxed and primitive `NaN`', false, false) | |
| 932 | unlikeLoosely(t, Object(''), '', 'boxed and primitive `""`', false, false) | |
| 933 | unlikeLoosely(t, Object('str'), 'str', 'boxed and primitive `"str"`', false, false) | |
| 934 | ||
| 935 | t.test('symbol', function (st) { | |
| 936 | const s = Symbol('') | |
| 937 | unlikeLoosely(st, Object(s), s, 'boxed and primitive `Symbol()`', false, false) | |
| 938 | st.end() | |
| 939 | }) | |
| 940 | ||
| 941 | t.test('bigint', function (st) { | |
| 942 | const hhgtg = BigInt(42) | |
| 943 | unlikeLoosely(st, Object(hhgtg), hhgtg, 'boxed and primitive `BigInt(42)`', false, false) | |
| 944 | st.end() | |
| 945 | }) | |
| 946 | ||
| 947 | // + | |
| 948 | /* t.test('`valueOf` is called for boxed primitives', function (st) { | |
| 949 | const a = Object(5) | |
| 950 | a.valueOf = function () { throw new Error('failed') } | |
| 951 | const b = Object(5) | |
| 952 | b.valueOf = function () { throw new Error('failed') } | |
| 953 | ||
| 954 | unlikeLoosely(st, a, b, 'two boxed numbers with a thrower valueOf', false, false) | |
| 955 | ||
| 956 | st.end() | |
| 957 | }) */ | |
| 958 | ||
| 959 | t.end() | |
| 960 | }) | |
| 961 | ||
| 962 | test('getters', function (t) { | |
| 963 | const a = {} | |
| 964 | Object.defineProperty(a, 'a', { enumerable: true, get: function () { return 5 } }) | |
| 965 | const b = {} | |
| 966 | Object.defineProperty(b, 'a', { enumerable: true, get: function () { return 6 } }) | |
| 967 | ||
| 968 | unlikeLoosely(t, a, b, 'two objects with the same getter but producing different values', false, false) | |
| 969 | ||
| 970 | t.end() | |
| 971 | }) | |
| 972 | ||
| 973 | test('fake arrays: extra keys will be tested', function (t) { | |
| 974 | const a = tag({ | |
| 975 | __proto__: Array.prototype, | |
| 976 | 0: 1, | |
| 977 | 1: 1, | |
| 978 | 2: 'broken', | |
| 979 | length: 2 | |
| 980 | }, 'Array') | |
| 981 | ||
| 982 | if (Object.defineProperty) { | |
| 983 | Object.defineProperty(a, 'length', { | |
| 984 | enumerable: false | |
| 985 | }) | |
| 986 | } | |
| 987 | ||
| 988 | unlikeLoosely(t, a, [1, 1], 'fake and real array with same contents and [[Prototype]]', false, false) | |
| 989 | ||
| 990 | const b = tag(/abc/, 'Array') | |
| 991 | b.__proto__ = Array.prototype // eslint-disable-line no-proto | |
| 992 | b.length = 3 | |
| 993 | if (Object.defineProperty) { | |
| 994 | Object.defineProperty(b, 'length', { | |
| 995 | enumerable: false | |
| 996 | }) | |
| 997 | } | |
| 998 | unlikeLoosely(t, b, ['a', 'b', 'c'], 'regex faking as array, and array', false, false) | |
| 999 | ||
| 1000 | t.end() | |
| 1001 | }) | |
| 1002 | ||
| 1003 | test('circular references', function (t) { | |
| 1004 | const b = {} | |
| 1005 | b.b = b | |
| 1006 | ||
| 1007 | const c = {} | |
| 1008 | c.b = c | |
| 1009 | ||
| 1010 | alike(t, | |
| 1011 | b, | |
| 1012 | c, | |
| 1013 | 'two self-referencing objects' | |
| 1014 | ) | |
| 1015 | ||
| 1016 | const d = {} | |
| 1017 | d.a = 1 | |
| 1018 | d.b = d | |
| 1019 | ||
| 1020 | const e = {} | |
| 1021 | e.a = 1 | |
| 1022 | e.b = e.a | |
| 1023 | ||
| 1024 | unlikeLoosely(t, | |
| 1025 | d, | |
| 1026 | e, | |
| 1027 | 'two deeply self-referencing objects' | |
| 1028 | ) | |
| 1029 | ||
| 1030 | t.end() | |
| 1031 | }) | |
| 1032 | ||
| 1033 | test('TypedArrays', function (t) { | |
| 1034 | // + | |
| 1035 | /* t.test('Buffer faked as Uint8Array', function (st) { | |
| 1036 | const a = safeBuffer('test') | |
| 1037 | const b = tag(Object.create( | |
| 1038 | a.__proto__, // eslint-disable-line no-proto | |
| 1039 | Object.assign(Object.getOwnPropertyDescriptors(a), { | |
| 1040 | length: { | |
| 1041 | enumerable: false, | |
| 1042 | value: 4 | |
| 1043 | } | |
| 1044 | }) | |
| 1045 | ), 'Uint8Array') | |
| 1046 | ||
| 1047 | unlikeLoosely(st, | |
| 1048 | a, | |
| 1049 | b, | |
| 1050 | 'Buffer and Uint8Array' | |
| 1051 | ) | |
| 1052 | ||
| 1053 | st.end() | |
| 1054 | }) */ | |
| 1055 | ||
| 1056 | // + | |
| 1057 | /* t.test('one TypedArray faking as another', { skip: !hasDunderProto }, function (st) { | |
| 1058 | const a = new Uint8Array(10) | |
| 1059 | const b = tag(new Int8Array(10), 'Uint8Array') | |
| 1060 | b.__proto__ = Uint8Array.prototype // eslint-disable-line no-proto | |
| 1061 | ||
| 1062 | unlikeLoosely(st, | |
| 1063 | a, | |
| 1064 | b, | |
| 1065 | 'Uint8Array, and Int8Array pretending to be a Uint8Array' | |
| 1066 | ) | |
| 1067 | ||
| 1068 | st.end() | |
| 1069 | }) */ | |
| 1070 | ||
| 1071 | t.test('ArrayBuffers', { skip: typeof ArrayBuffer !== 'function' }, function (st) { | |
| 1072 | const buffer1 = new ArrayBuffer(8) // initial value of 0's | |
| 1073 | const buffer2 = new ArrayBuffer(8) // initial value of 0's | |
| 1074 | ||
| 1075 | const view1 = new Int8Array(buffer1) | |
| 1076 | const view2 = new Int8Array(buffer2) | |
| 1077 | ||
| 1078 | alike(st, | |
| 1079 | view1, | |
| 1080 | view2, | |
| 1081 | 'Int8Arrays of similar ArrayBuffers' | |
| 1082 | ) | |
| 1083 | ||
| 1084 | alike(st, | |
| 1085 | buffer1, | |
| 1086 | buffer2, | |
| 1087 | 'similar ArrayBuffers' | |
| 1088 | ) | |
| 1089 | ||
| 1090 | for (let i = 0; i < view1.byteLength; i += 1) { | |
| 1091 | view1[i] = 9 // change all values to 9's | |
| 1092 | } | |
| 1093 | ||
| 1094 | unlikeLoosely(st, | |
| 1095 | view1, | |
| 1096 | view2, | |
| 1097 | 'Int8Arrays of different ArrayBuffers' | |
| 1098 | ) | |
| 1099 | ||
| 1100 | unlikeLoosely(st, | |
| 1101 | buffer1, | |
| 1102 | buffer2, | |
| 1103 | 'different ArrayBuffers' | |
| 1104 | ) | |
| 1105 | ||
| 1106 | // node < 0.11 has a nonconfigurable own byteLength property | |
| 1107 | t.test('lies about byteLength', { skip: !('byteLength' in ArrayBuffer.prototype) }, function (s2t) { | |
| 1108 | const empty4 = new ArrayBuffer(4) | |
| 1109 | const empty6 = new ArrayBuffer(6) | |
| 1110 | Object.defineProperty(empty6, 'byteLength', { value: 4 }) | |
| 1111 | ||
| 1112 | unlikeLoosely(s2t, | |
| 1113 | empty4, | |
| 1114 | empty6, | |
| 1115 | 'different-length ArrayBuffers, one lying' | |
| 1116 | ) | |
| 1117 | s2t.end() | |
| 1118 | }) | |
| 1119 | ||
| 1120 | st.end() | |
| 1121 | }) | |
| 1122 | ||
| 1123 | t.test('SharedArrayBuffers', { skip: typeof SharedArrayBuffer !== 'function' }, function (st) { | |
| 1124 | const buffer1 = new SharedArrayBuffer(8) // initial value of 0's | |
| 1125 | const buffer2 = new SharedArrayBuffer(8) // initial value of 0's | |
| 1126 | ||
| 1127 | const view1 = new Int8Array(buffer1) | |
| 1128 | const view2 = new Int8Array(buffer2) | |
| 1129 | ||
| 1130 | alike(st, | |
| 1131 | view1, | |
| 1132 | view2, | |
| 1133 | 'Int8Arrays of similar SharedArrayBuffers' | |
| 1134 | ) | |
| 1135 | ||
| 1136 | alike(st, | |
| 1137 | buffer1, | |
| 1138 | buffer2, | |
| 1139 | 'similar SharedArrayBuffers' | |
| 1140 | ) | |
| 1141 | ||
| 1142 | for (let i = 0; i < view1.byteLength; i += 1) { | |
| 1143 | view1[i] = 9 // change all values to 9's | |
| 1144 | } | |
| 1145 | ||
| 1146 | unlikeLoosely(st, | |
| 1147 | view1, | |
| 1148 | view2, | |
| 1149 | 'Int8Arrays of different SharedArrayBuffers' | |
| 1150 | ) | |
| 1151 | ||
| 1152 | // + | |
| 1153 | /* unlikeLoosely(st, | |
| 1154 | buffer1, | |
| 1155 | buffer2, | |
| 1156 | 'different SharedArrayBuffers' | |
| 1157 | ) */ | |
| 1158 | ||
| 1159 | // + | |
| 1160 | /* t.test('lies about byteLength', { skip: !('byteLength' in SharedArrayBuffer.prototype) }, function (s2t) { | |
| 1161 | const empty4 = new SharedArrayBuffer(4) | |
| 1162 | const empty6 = new SharedArrayBuffer(6) | |
| 1163 | Object.defineProperty(empty6, 'byteLength', { value: 4 }) | |
| 1164 | ||
| 1165 | unlikeLoosely(s2t, | |
| 1166 | empty4, | |
| 1167 | empty6, | |
| 1168 | 'different-length SharedArrayBuffers, one lying' | |
| 1169 | ) | |
| 1170 | s2t.end() | |
| 1171 | }) */ | |
| 1172 | ||
| 1173 | st.end() | |
| 1174 | }) | |
| 1175 | ||
| 1176 | t.end() | |
| 1177 | }) | |
| 1178 | ||
| 1179 | test('String object', function (t) { | |
| 1180 | alike(t, | |
| 1181 | new String('hi'), // eslint-disable-line no-new-wrappers | |
| 1182 | new String('hi'), // eslint-disable-line no-new-wrappers | |
| 1183 | 'two same String objects' | |
| 1184 | ) | |
| 1185 | ||
| 1186 | unlike(t, | |
| 1187 | new String('hi'), // eslint-disable-line no-new-wrappers | |
| 1188 | new String('hi2'), // eslint-disable-line no-new-wrappers | |
| 1189 | 'two different String objects' | |
| 1190 | ) | |
| 1191 | }) | |
| 1192 | ||
| 1193 | test('Number object', function (t) { | |
| 1194 | alike(t, | |
| 1195 | new Number(1), // eslint-disable-line no-new-wrappers | |
| 1196 | new Number(1), // eslint-disable-line no-new-wrappers | |
| 1197 | 'two same Number objects' | |
| 1198 | ) | |
| 1199 | ||
| 1200 | t.absent(sameObject( | |
| 1201 | new Number(1), // eslint-disable-line no-new-wrappers | |
| 1202 | new Number(2) // eslint-disable-line no-new-wrappers | |
| 1203 | ), 'two different Number objects') | |
| 1204 | }) | |
| 1205 | ||
| 1206 | test('Boolean object', function (t) { | |
| 1207 | alike(t, | |
| 1208 | new Boolean(true), // eslint-disable-line no-new-wrappers | |
| 1209 | new Boolean(true), // eslint-disable-line no-new-wrappers | |
| 1210 | 'two same Boolean objects' | |
| 1211 | ) | |
| 1212 | ||
| 1213 | t.absent(sameObject( | |
| 1214 | new Boolean(true), // eslint-disable-line no-new-wrappers | |
| 1215 | new Boolean(false) // eslint-disable-line no-new-wrappers | |
| 1216 | ), 'two different Boolean objects') | |
| 1217 | }) | |
| 1218 | ||
| 1219 | test('objects', function (t) { | |
| 1220 | t.is(sameObject({ foo: 1 }, { foo: 1 }), true) | |
| 1221 | t.is(sameObject({ foo: 1 }, { foo: 1, bar: true }), false) | |
| 1222 | t.is(sameObject({ foo: 1, nested: { a: 1 } }, { foo: 1, nested: { a: 1 } }), true) | |
| 1223 | t.is(sameObject([{ a: 1 }, { b: 1 }], [{ a: 1 }, { b: 1 }]), true) | |
| 1224 | }) | |
| 1225 | ||
| 1226 | test('typed arrays', function (t) { | |
| 1227 | t.is(sameObject(new Uint8Array([1, 2, 3]), new Uint8Array([1, 2, 3])), true) | |
| 1228 | t.is(sameObject(new Uint8Array([1, 2, 1]), new Uint8Array([1, 2, 3])), false) | |
| 1229 | }) | |
| 1230 | ||
| 1231 | test('symbols', function (t) { | |
| 1232 | alike(t, Symbol.for('hello'), Symbol.for('hello'), 'same symbol', true, true) | |
| 1233 | unlike(t, Symbol.for('hello'), Symbol.for('holas'), 'diff symbol', false, true) | |
| 1234 | ||
| 1235 | alike(t, [Symbol.for('hello')], [Symbol.for('hello')], 'symbol inside object', true, true) | |
| 1236 | alike(t, { sym: Symbol.for('hello') }, { sym: Symbol.for('hello') }, 'symbol inside object', true, true) | |
| 1237 | ||
| 1238 | unlike(t, { sym: Symbol.for('hello') }, { sym: Symbol.for('holas') }, 'diff symbol inside object', false, true) | |
| 1239 | }) | |
| 1240 | ||
| 1241 | test('numbers', function (t) { | |
| 1242 | alike(t, BigInt('9007199254740991'), BigInt('9007199254740991'), 'BigInt', true, true) | |
| 1243 | alike(t, Infinity, Infinity, 'Infinity', true, true) | |
| 1244 | ||
| 1245 | // Note: deep-equal library fails, but Node's assert version passes, so we follow Node standard | |
| 1246 | t.ok(sameObject(NaN, NaN, { strict: true }), 'NaN (alike)') | |
| 1247 | t.ok(sameObject(NaN, NaN, { strict: false }), 'NaN (alike loosely)') | |
| 1248 | }) | |
| 1249 | ||
| 1250 | // + | |
| 1251 | test.skip('symbol as key', function (t) { | |
| 1252 | alike(t, { a: true, [Symbol.for('aa')]: true }, { a: true, [Symbol.for('aa')]: true }) | |
| 1253 | unlike(t, { a: true, [Symbol.for('aa')]: true }, { a: true, [Symbol.for('cc')]: true }) | |
| 1254 | }) | |
| 1255 | ||
| 1256 | test('promises', function (t) { | |
| 1257 | const promise = new Promise(noop) | |
| 1258 | alike(t, promise, promise, 'two promises with same reference') | |
| 1259 | ||
| 1260 | // + | |
| 1261 | /* alike(t, | |
| 1262 | new Promise(noop), | |
| 1263 | new Promise(noop), | |
| 1264 | 'two promises' | |
| 1265 | ) | |
| 1266 | ||
| 1267 | alike(t, | |
| 1268 | Promise.resolve('hi'), | |
| 1269 | Promise.resolve('hi'), | |
| 1270 | 'resolve with same primitive' | |
| 1271 | ) | |
| 1272 | ||
| 1273 | unlike(t, | |
| 1274 | Promise.resolve('hi1'), | |
| 1275 | Promise.resolve('hi2'), | |
| 1276 | 'resolve with different primitive' | |
| 1277 | ) | |
| 1278 | ||
| 1279 | alike(t, | |
| 1280 | Promise.resolve({ a: 1 }), | |
| 1281 | Promise.resolve({ a: 1 }), | |
| 1282 | 'resolve with same objects' | |
| 1283 | ) | |
| 1284 | ||
| 1285 | unlike(t, | |
| 1286 | Promise.resolve({ a: 1 }), | |
| 1287 | Promise.resolve({ a: 2 }), | |
| 1288 | 'resolve with different objects' | |
| 1289 | ) */ | |
| 1290 | ||
| 1291 | function noop (resolve, reject) {} | |
| 1292 | }) | |
| 1293 | ||
| 1294 | test('functions', function (t) { | |
| 1295 | unlike(t, function () {}, function () {}, 'two different functions', true, true) | |
| 1296 | ||
| 1297 | const fn = function () {} | |
| 1298 | alike(t, fn, fn, 'two same functions', true, true) | |
| 1299 | }) | |
| 1300 | ||
| 1301 | // + merge this case with the other one | |
| 1302 | test('circular references x2', function (t) { | |
| 1303 | const obj = { root: null } | |
| 1304 | obj.root = obj | |
| 1305 | const obj2 = { root: null } | |
| 1306 | obj2.root = obj2 | |
| 1307 | alike(t, obj, obj2) | |
| 1308 | ||
| 1309 | const obj3 = [{ root: null }] | |
| 1310 | obj3.root = obj3 | |
| 1311 | const obj4 = [{ root: null }] | |
| 1312 | obj4.root = obj4 | |
| 1313 | alike(t, obj3, obj4) | |
| 1314 | ||
| 1315 | const obj5 = { sub: { root: null, sub2: { obj2, obj3 } } } | |
| 1316 | obj5.sub.root = obj5 | |
| 1317 | const obj6 = { sub: { root: null, sub2: { obj2, obj3 } } } | |
| 1318 | obj6.sub.root = obj6 | |
| 1319 | alike(t, obj5, obj6) | |
| 1320 | ||
| 1321 | const obj7 = [{ root: null }] | |
| 1322 | obj7[0].root = obj7 | |
| 1323 | const obj8 = [{ root: null }] | |
| 1324 | obj8[0].root = obj8 | |
| 1325 | alike(t, obj7, obj8) | |
| 1326 | ||
| 1327 | const o = {} | |
| 1328 | const obj9 = { a: o, b: o } | |
| 1329 | const o2 = {} | |
| 1330 | const obj10 = { a: o2, b: o2 } | |
| 1331 | alike(t, obj9, obj10) | |
| 1332 | ||
| 1333 | const obj11 = [o, o] | |
| 1334 | const obj12 = [o, o] | |
| 1335 | alike(t, obj11, obj12) | |
| 1336 | ||
| 1337 | const obj13 = [o, obj, o] | |
| 1338 | obj13.obj = obj | |
| 1339 | const obj14 = [o, obj, o] | |
| 1340 | obj14.obj = obj | |
| 1341 | alike(t, obj13, obj14) | |
| 1342 | }) | |
| 1343 | ||
| 1344 | function alike (t, a, b, comment = '') { | |
| 1345 | try { | |
| 1346 | t.ok(deepEqual(a, b, { strict: true }), '[deep-equal normal] ' + comment) | |
| 1347 | t.ok(deepEqual(b, a, { strict: true }), '[deep-equal reversed] ' + comment) | |
| 1348 | } catch (error) { | |
| 1349 | if (error.message === 'Cannot convert a Symbol value to a string') t.comment('alike => ' + error.message + ' [deep-equal] ' + comment) | |
| 1350 | else throw error | |
| 1351 | } | |
| 1352 | ||
| 1353 | t.ok(sameObject(a, b, { strict: true }), '[same-object normal] ' + comment) | |
| 1354 | t.ok(sameObject(b, a, { strict: true }), '[same-object reversed] ' + comment) | |
| 1355 | } | |
| 1356 | ||
| 1357 | function alikeLoosely (t, a, b, comment = '') { // eslint-disable-line no-unused-vars | |
| 1358 | try { | |
| 1359 | t.ok(deepEqual(a, b, { strict: false }), '[deep-equal normal] ' + comment) | |
| 1360 | t.ok(deepEqual(b, a, { strict: false }), '[deep-equal reversed] ' + comment) | |
| 1361 | } catch (error) { | |
| 1362 | if (error.message === 'Cannot convert a Symbol value to a string') t.comment('alike loosely => ' + error.message + ' [deep-equal] ' + comment) | |
| 1363 | else throw error | |
| 1364 | } | |
| 1365 | ||
| 1366 | t.ok(sameObject(a, b, { strict: false }), '[same-object normal] ' + comment) | |
| 1367 | t.ok(sameObject(b, a, { strict: false }), '[same-object reversed] ' + comment) | |
| 1368 | } | |
| 1369 | ||
| 1370 | function unlike (t, a, b, comment = '') { | |
| 1371 | try { | |
| 1372 | t.absent(deepEqual(a, b, { strict: true }), '[deep-equal normal] ' + comment) | |
| 1373 | t.absent(deepEqual(b, a, { strict: true }), '[deep-equal reversed] ' + comment) | |
| 1374 | } catch (error) { | |
| 1375 | if (error.message === 'Cannot convert a Symbol value to a string') t.comment('unlike => ' + error.message + ' [deep-equal] ' + comment) | |
| 1376 | else throw error | |
| 1377 | } | |
| 1378 | ||
| 1379 | t.absent(sameObject(a, b, { strict: true }), '[same-object normal] ' + comment) | |
| 1380 | t.absent(sameObject(b, a, { strict: true }), '[same-object reversed] ' + comment) | |
| 1381 | } | |
| 1382 | ||
| 1383 | function unlikeLoosely (t, a, b, comment = '') { // eslint-disable-line no-unused-vars | |
| 1384 | try { | |
| 1385 | t.absent(deepEqual(a, b, { strict: false }), '[deep-equal normal] ' + comment) | |
| 1386 | t.absent(deepEqual(b, a, { strict: false }), '[deep-equal reversed] ' + comment) | |
| 1387 | } catch (error) { | |
| 1388 | if (error.message === 'Cannot convert a Symbol value to a string') t.comment('unlike loosely => ' + error.message + ' [deep-equal] ' + comment) | |
| 1389 | else throw error | |
| 1390 | } | |
| 1391 | ||
| 1392 | t.absent(sameObject(a, b, { strict: false }), '[same-object normal] ' + comment) | |
| 1393 | t.absent(sameObject(b, a, { strict: false }), '[same-object reversed] ' + comment) | |
| 1394 | } | |
| 1395 | ||
| 1396 | function tag (obj, value) { | |
| 1397 | Object.defineProperty(obj, Symbol.toStringTag, { value }) | |
| 1398 | return obj | |
| 1399 | } |
| … | … | @@ -0,0 +1,6 @@ |
| 1 | language: node_js | |
| 2 | node_js: | |
| 3 | - '6' | |
| 4 | - '8' | |
| 5 | - '10' | |
| 6 |
| … | … | @@ -0,0 +1,21 @@ |
| 1 | The MIT License (MIT) | |
| 2 | ||
| 3 | Copyright (c) 2017 Mathias Buus | |
| 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 | |
| 13 | all 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 | |
| 21 | THE SOFTWARE. |
| … | … | @@ -0,0 +1,116 @@ |
| 1 | # shuffled-priority-queue | |
| 2 | ||
| 3 | A priority queue that shuffles elements with the same priority. | |
| 4 | ||
| 5 | ``` | |
| 6 | npm install shuffled-priority-queue | |
| 7 | ``` | |
| 8 | ||
| 9 | [](https://travis-ci.org/mafintosh/shuffled-priority-queue) | |
| 10 | ||
| 11 | ## Usage | |
| 12 | ||
| 13 | ``` js | |
| 14 | const spq = require('shuffled-priority-queue') | |
| 15 | const queue = spq() | |
| 16 | ||
| 17 | queue.add({ | |
| 18 | priority: 0, | |
| 19 | value: 'hello' | |
| 20 | }) | |
| 21 | ||
| 22 | queue.add({ | |
| 23 | priority: 0, | |
| 24 | value: 'world' | |
| 25 | }) | |
| 26 | ||
| 27 | queue.add({ | |
| 28 | priority: 1, | |
| 29 | value: 'welt' | |
| 30 | }) | |
| 31 | ||
| 32 | queue.add({ | |
| 33 | priority: 2, | |
| 34 | value: 'verden' | |
| 35 | }) | |
| 36 | ||
| 37 | console.log(queue.shift()) // returns {value: 'verden'} | |
| 38 | console.log(queue.shift()) // returns {value: 'welt'} | |
| 39 | console.log(queue.shift()) // returns {value: 'hello'} or {value: 'world'} | |
| 40 | console.log(queue.shift()) // returns {value: 'hello'} or {value: 'world'} | |
| 41 | console.log(queue.shift()) // returns null (empty queue) | |
| 42 | ``` | |
| 43 | ||
| 44 | ## API | |
| 45 | ||
| 46 | #### `const queue = spq()` | |
| 47 | ||
| 48 | Create a new queue. | |
| 49 | ||
| 50 | #### `value = queue.add(value)` | |
| 51 | ||
| 52 | Add a new value to the queue. The value is returned for convenience | |
| 53 | If you set `value.priority` to a number, it'll be added to the queue at that priority. | |
| 54 | ||
| 55 | #### `queue.remove(value)` | |
| 56 | ||
| 57 | Remove a value from the queue. | |
| 58 | ||
| 59 | #### `bool = queue.has(value)` | |
| 60 | ||
| 61 | Check if a value is in the queue. | |
| 62 | ||
| 63 | #### `value = queue.shift()` | |
| 64 | ||
| 65 | Shift the next value off the queue. | |
| 66 | ||
| 67 | The value returned will have the highest priority off the queue. | |
| 68 | If multiple values have the same priority a random one is returned. | |
| 69 | ||
| 70 | #### `value = queue.head()` | |
| 71 | ||
| 72 | Same as `shift()` but does not mutate the queue. | |
| 73 | ||
| 74 | #### `value = queue.pop()` | |
| 75 | ||
| 76 | Same as `shift()` but returns a value with the lowest priority. | |
| 77 | ||
| 78 | #### `value = queue.tail()` | |
| 79 | ||
| 80 | Same as `pop()` but does not mutate the queue. | |
| 81 | ||
| 82 | #### `queue.length` | |
| 83 | ||
| 84 | Property containing how many items are in the queue | |
| 85 | ||
| 86 | #### `for (const value of queue)` | |
| 87 | ||
| 88 | Iterate the queue from highest priority to lowest using the `for of` syntax | |
| 89 | ||
| 90 | #### `value = queue.next([prevValue])` | |
| 91 | ||
| 92 | Iterate the queue from highest priority to lowest. | |
| 93 | ||
| 94 | ``` js | |
| 95 | let prevValue = null | |
| 96 | ||
| 97 | while (prevValue = queue.next(prevValue)) { | |
| 98 | console.log('value:', prevValue) | |
| 99 | } | |
| 100 | ``` | |
| 101 | ||
| 102 | #### `value = queue.prev([prevValue])` | |
| 103 | ||
| 104 | Iterate the queue from lowest priority to highest. | |
| 105 | ||
| 106 | ``` js | |
| 107 | let prevValue = null | |
| 108 | ||
| 109 | while (prevValue = queue.prev(prevValue)) { | |
| 110 | console.log('value:', prevValue) | |
| 111 | } | |
| 112 | ``` | |
| 113 | ||
| 114 | ## License | |
| 115 | ||
| 116 | MIT |
| … | … | @@ -0,0 +1,28 @@ |
| 1 | var spq = require('./') | |
| 2 | var queue = spq() | |
| 3 | ||
| 4 | queue.add({ | |
| 5 | priority: 0, | |
| 6 | value: 'hello' | |
| 7 | }) | |
| 8 | ||
| 9 | queue.add({ | |
| 10 | priority: 0, | |
| 11 | value: 'world' | |
| 12 | }) | |
| 13 | ||
| 14 | queue.add({ | |
| 15 | priority: 1, | |
| 16 | value: 'welt' | |
| 17 | }) | |
| 18 | ||
| 19 | queue.add({ | |
| 20 | priority: 2, | |
| 21 | value: 'verden' | |
| 22 | }) | |
| 23 | ||
| 24 | console.log(queue.pop()) // returns {value: 'verden'} | |
| 25 | console.log(queue.pop()) // returns {value: 'welt'} | |
| 26 | console.log(queue.pop()) // returns {value: 'hello'} or {value: 'world'} | |
| 27 | console.log(queue.pop()) // returns {value: 'hello'} or {value: 'world'} | |
| 28 | console.log(queue.pop()) // returns null (empty queue) |
| … | … | @@ -0,0 +1,134 @@ |
| 1 | const set = require('unordered-set') | |
| 2 | ||
| 3 | module.exports = opts => new ShuffledPriorityQueue(opts) | |
| 4 | ||
| 5 | class ShuffledPriorityQueue { | |
| 6 | constructor (opts) { | |
| 7 | this.priorities = [] | |
| 8 | this.equals = (opts && opts.equals) || null | |
| 9 | } | |
| 10 | ||
| 11 | get length () { | |
| 12 | return this.priorities.reduce(add, 0) | |
| 13 | } | |
| 14 | ||
| 15 | [Symbol.iterator] () { | |
| 16 | return new Iterator(this) | |
| 17 | } | |
| 18 | ||
| 19 | head () { | |
| 20 | for (let i = this.priorities.length - 1; i >= 0; i--) { | |
| 21 | const q = this.priorities[i] | |
| 22 | if (q.length) return shuffle(q, 0) | |
| 23 | } | |
| 24 | return null | |
| 25 | } | |
| 26 | ||
| 27 | tail () { | |
| 28 | for (let i = 0; i < this.priorities.length; i++) { | |
| 29 | const q = this.priorities[i] | |
| 30 | if (q.length) return shuffle(q, 0) | |
| 31 | } | |
| 32 | return null | |
| 33 | } | |
| 34 | ||
| 35 | prev (prev) { | |
| 36 | if (!prev) return this.tail() | |
| 37 | return next(this.priorities, prev, 1) | |
| 38 | } | |
| 39 | ||
| 40 | next (prev) { | |
| 41 | if (!prev) return this.head() | |
| 42 | return next(this.priorities, prev, -1) | |
| 43 | } | |
| 44 | ||
| 45 | shift () { | |
| 46 | return this.remove(this.head()) | |
| 47 | } | |
| 48 | ||
| 49 | pop () { | |
| 50 | return this.remove(this.tail()) | |
| 51 | } | |
| 52 | ||
| 53 | add (val) { | |
| 54 | const prio = val.priority || 0 | |
| 55 | while (prio >= this.priorities.length) this.priorities.push([]) | |
| 56 | set.add(this.priorities[prio], val) | |
| 57 | return val | |
| 58 | } | |
| 59 | ||
| 60 | remove (val) { | |
| 61 | if (!val) return null | |
| 62 | ||
| 63 | if (val._index === undefined) { | |
| 64 | val = this.find(val) | |
| 65 | if (!val) return null | |
| 66 | } | |
| 67 | ||
| 68 | return set.remove(this.priorities[val.priority || 0], val) | |
| 69 | } | |
| 70 | ||
| 71 | has (val) { | |
| 72 | if (val._index === undefined) return this.find(val) | |
| 73 | const priority = val.priority || 0 | |
| 74 | if (priority >= this.priorities.length) return false | |
| 75 | return set.has(this.priorities[priority], val) | |
| 76 | } | |
| 77 | ||
| 78 | find (val) { | |
| 79 | if (val._index !== undefined) return val | |
| 80 | ||
| 81 | const prio = val.priority || 0 | |
| 82 | const qs = this.priorities | |
| 83 | if (prio >= qs.length) return null | |
| 84 | ||
| 85 | const q = qs[prio] | |
| 86 | ||
| 87 | for (let i = 0; i < q.length; i++) { | |
| 88 | if (this.equals(q[i], val)) return q[i] | |
| 89 | } | |
| 90 | ||
| 91 | return null | |
| 92 | } | |
| 93 | } | |
| 94 | ||
| 95 | class Iterator { | |
| 96 | constructor (queue) { | |
| 97 | this.prev = null | |
| 98 | this.queue = queue | |
| 99 | } | |
| 100 | ||
| 101 | next () { | |
| 102 | const next = this.queue.next(this.prev) | |
| 103 | this.prev = next | |
| 104 | return { done: !next, value: next } | |
| 105 | } | |
| 106 | } | |
| 107 | ||
| 108 | function shuffle (q, i) { | |
| 109 | const ran = i + Math.floor(Math.random() * (q.length - i)) | |
| 110 | set.swap(q, q[ran], q[i]) | |
| 111 | return q[i] | |
| 112 | } | |
| 113 | ||
| 114 | function next (queues, prev, inc) { | |
| 115 | let i = prev.priority || 0 | |
| 116 | let j = (prev._index || 0) + 1 | |
| 117 | ||
| 118 | while (true) { | |
| 119 | if (i < 0 || i >= queues.length) return null | |
| 120 | const q = queues[i] | |
| 121 | ||
| 122 | if (j >= q.length) { | |
| 123 | i += inc | |
| 124 | j = 0 | |
| 125 | continue | |
| 126 | } | |
| 127 | ||
| 128 | return shuffle(q, j) | |
| 129 | } | |
| 130 | } | |
| 131 | ||
| 132 | function add (len, b) { | |
| 133 | return len + b.length | |
| 134 | } |
| … | … | @@ -0,0 +1,26 @@ |
| 1 | { | |
| 2 | "name": "shuffled-priority-queue", | |
| 3 | "version": "2.1.0", | |
| 4 | "description": "A priority queue that shuffles elements with the same priority.", | |
| 5 | "main": "index.js", | |
| 6 | "scripts": { | |
| 7 | "test": "standard && tape test.js" | |
| 8 | }, | |
| 9 | "dependencies": { | |
| 10 | "unordered-set": "^2.0.1" | |
| 11 | }, | |
| 12 | "devDependencies": { | |
| 13 | "standard": "^12.0.1", | |
| 14 | "tape": "^4.9.1" | |
| 15 | }, | |
| 16 | "repository": { | |
| 17 | "type": "git", | |
| 18 | "url": "https://github.com/mafintosh/shuffled-priority-queue.git" | |
| 19 | }, | |
| 20 | "author": "Mathias Buus (@mafintosh)", | |
| 21 | "license": "MIT", | |
| 22 | "bugs": { | |
| 23 | "url": "https://github.com/mafintosh/shuffled-priority-queue/issues" | |
| 24 | }, | |
| 25 | "homepage": "https://github.com/mafintosh/shuffled-priority-queue" | |
| 26 | } |
| … | … | @@ -0,0 +1,164 @@ |
| 1 | const spq = require('./') | |
| 2 | const tape = require('tape') | |
| 3 | ||
| 4 | tape('different prios', function (t) { | |
| 5 | const queue = spq() | |
| 6 | ||
| 7 | const a = queue.add({ | |
| 8 | hello: 'world', | |
| 9 | priority: 0 | |
| 10 | }) | |
| 11 | ||
| 12 | const b = queue.add({ | |
| 13 | hej: 'verden', | |
| 14 | priority: 1 | |
| 15 | }) | |
| 16 | ||
| 17 | t.ok(queue.has(b)) | |
| 18 | t.same(queue.length, 2) | |
| 19 | t.same(queue.shift(), b) | |
| 20 | t.ok(!queue.has(b)) | |
| 21 | t.same(queue.length, 1) | |
| 22 | t.same(queue.shift(), a) | |
| 23 | t.same(queue.length, 0) | |
| 24 | t.same(queue.shift(), null) | |
| 25 | t.end() | |
| 26 | }) | |
| 27 | ||
| 28 | tape('same prios', function (t) { | |
| 29 | const queue = spq() | |
| 30 | ||
| 31 | const a = queue.add({ | |
| 32 | hello: 'world', | |
| 33 | priority: 0 | |
| 34 | }) | |
| 35 | ||
| 36 | const b = queue.add({ | |
| 37 | hello: 'verden', | |
| 38 | priority: 0 | |
| 39 | }) | |
| 40 | ||
| 41 | const c = queue.add({ | |
| 42 | hej: 'verden', | |
| 43 | priority: 1 | |
| 44 | }) | |
| 45 | ||
| 46 | t.same(queue.shift(), c) | |
| 47 | ||
| 48 | let head = queue.shift() | |
| 49 | t.ok(head === a || head === b) | |
| 50 | ||
| 51 | head = queue.shift() | |
| 52 | t.ok(head === a || head === b) | |
| 53 | ||
| 54 | t.same(queue.shift(), null) | |
| 55 | t.end() | |
| 56 | }) | |
| 57 | ||
| 58 | tape('next', function (t) { | |
| 59 | const queue = spq() | |
| 60 | ||
| 61 | const a = queue.add({ | |
| 62 | hello: 'world', | |
| 63 | priority: 0 | |
| 64 | }) | |
| 65 | ||
| 66 | const b = queue.add({ | |
| 67 | hello: 'verden', | |
| 68 | priority: 0 | |
| 69 | }) | |
| 70 | ||
| 71 | const c = queue.add({ | |
| 72 | hej: 'verden', | |
| 73 | priority: 1 | |
| 74 | }) | |
| 75 | ||
| 76 | t.same(queue.next(), c) | |
| 77 | ||
| 78 | let value = queue.next(c) | |
| 79 | t.ok(value === a || value === b) | |
| 80 | ||
| 81 | const old = value | |
| 82 | value = queue.next(value) | |
| 83 | t.ok(old !== value) | |
| 84 | t.ok(value === a || value === b) | |
| 85 | ||
| 86 | t.same(queue.next(value), null) | |
| 87 | t.end() | |
| 88 | }) | |
| 89 | ||
| 90 | tape('prev', function (t) { | |
| 91 | const queue = spq() | |
| 92 | ||
| 93 | const a = queue.add({ | |
| 94 | hello: 'world', | |
| 95 | priority: 0 | |
| 96 | }) | |
| 97 | ||
| 98 | const b = queue.add({ | |
| 99 | hello: 'verden', | |
| 100 | priority: 0 | |
| 101 | }) | |
| 102 | ||
| 103 | const c = queue.add({ | |
| 104 | hej: 'verden', | |
| 105 | priority: 1 | |
| 106 | }) | |
| 107 | ||
| 108 | let tail = queue.prev() | |
| 109 | t.ok(tail === a || tail === b) | |
| 110 | ||
| 111 | const old = tail | |
| 112 | tail = queue.prev(tail) | |
| 113 | t.ok(old !== tail) | |
| 114 | t.ok(tail === a || tail === b) | |
| 115 | ||
| 116 | tail = queue.prev(tail) | |
| 117 | t.same(tail, c) | |
| 118 | ||
| 119 | t.same(queue.prev(tail), null) | |
| 120 | t.end() | |
| 121 | }) | |
| 122 | ||
| 123 | tape('equals', function (t) { | |
| 124 | const queue = spq({ | |
| 125 | equals: function (a, b) { | |
| 126 | return a.hello === b.hello | |
| 127 | } | |
| 128 | }) | |
| 129 | ||
| 130 | queue.add({ | |
| 131 | hello: 'world' | |
| 132 | }) | |
| 133 | ||
| 134 | t.same(queue.head().hello, 'world') | |
| 135 | ||
| 136 | queue.remove({ | |
| 137 | hello: 'world' | |
| 138 | }) | |
| 139 | ||
| 140 | t.same(queue.head(), null) | |
| 141 | t.end() | |
| 142 | }) | |
| 143 | ||
| 144 | tape('iterator', function (t) { | |
| 145 | t.plan(5) | |
| 146 | ||
| 147 | const queue = spq() | |
| 148 | const seen = {} | |
| 149 | ||
| 150 | queue.add({ priority: 0, hi: 'a' }) | |
| 151 | queue.add({ priority: 0, hi: 'b' }) | |
| 152 | queue.add({ priority: 1, hi: 'c' }) | |
| 153 | queue.add({ priority: 2, hi: 'd' }) | |
| 154 | ||
| 155 | let prev = 3 | |
| 156 | ||
| 157 | for (const value of queue) { | |
| 158 | t.ok(prev >= value.priority) | |
| 159 | prev = value.priority | |
| 160 | seen[value.hi] = true | |
| 161 | } | |
| 162 | ||
| 163 | t.same(seen, { a: true, b: true, c: true, d: true }) | |
| 164 | }) |
| … | … | @@ -0,0 +1,19 @@ |
| 1 | Copyright (c) 2017 Eric Wendelin and other contributors | |
| 2 | ||
| 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of | |
| 4 | this software and associated documentation files (the "Software"), to deal in | |
| 5 | the Software without restriction, including without limitation the rights to | |
| 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies | |
| 7 | of the Software, and to permit persons to whom the Software is furnished to do | |
| 8 | so, subject to the following conditions: | |
| 9 | ||
| 10 | The above copyright notice and this permission notice shall be included in all | |
| 11 | copies or substantial portions of the Software. | |
| 12 | ||
| 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
| 19 | SOFTWARE. |
| … | … | @@ -0,0 +1,79 @@ |
| 1 | stackframe | |
| 2 | ========== | |
| 3 | ## JS Object representation of a stack frame | |
| 4 | ||
| 5 | [](https://github.com/stacktracejs/stackframe/actions?query=workflow%3AContinuous+Integration+branch%3Amaster) | |
| 6 | [](https://coveralls.io/r/stacktracejs/stackframe?branch=master) | |
| 7 | [](https://opensource.org/licenses/MIT) | |
| 8 | [](https://github.com/stacktracejs/stackframe/releases) | |
| 9 | [](https://github.com/stacktracejs/stackframe/releases) | |
| 10 | [](https://github.com/stacktracejs/stackframe/releases) | |
| 11 | [](http://todogroup.org/opencodeofconduct/#stacktrace.js/me@eriwen.com) | |
| 12 | ||
| 13 | Underlies functionality of other modules within [stacktrace.js](https://www.stacktracejs.com). | |
| 14 | ||
| 15 | Written to closely resemble StackFrame representations in [Gecko](http://mxr.mozilla.org/mozilla-central/source/xpcom/base/nsIException.idl#14) and [V8](https://github.com/v8/v8/wiki/Stack%20Trace%20API) | |
| 16 | ||
| 17 | ## Usage | |
| 18 | ```js | |
| 19 | // Create StackFrame and set properties | |
| 20 | var stackFrame = new StackFrame({ | |
| 21 | functionName: 'funName', | |
| 22 | args: ['args'], | |
| 23 | fileName: 'http://localhost:3000/file.js', | |
| 24 | lineNumber: 1, | |
| 25 | columnNumber: 3288, | |
| 26 | isEval: true, | |
| 27 | isNative: false, | |
| 28 | source: 'ORIGINAL_STACK_LINE' | |
| 29 | evalOrigin: new StackFrame({functionName: 'withinEval', lineNumber: 2, columnNumber: 43}) | |
| 30 | }); | |
| 31 | ||
| 32 | stackFrame.functionName // => "funName" | |
| 33 | stackFrame.setFunctionName('newName') | |
| 34 | stackFrame.getFunctionName() // => "newName" | |
| 35 | ||
| 36 | stackFrame.args // => ["args"] | |
| 37 | stackFrame.setArgs([]) | |
| 38 | stackFrame.getArgs() // => [] | |
| 39 | ||
| 40 | stackFrame.fileName // => 'http://localhost:3000/file.min.js' | |
| 41 | stackFrame.setFileName('http://localhost:3000/file.js') | |
| 42 | stackFrame.getFileName() // => 'http://localhost:3000/file.js' | |
| 43 | ||
| 44 | stackFrame.lineNumber // => 1 | |
| 45 | stackFrame.setLineNumber(325) | |
| 46 | stackFrame.getLineNumber() // => 325 | |
| 47 | ||
| 48 | stackFrame.columnNumber // => 3288 | |
| 49 | stackFrame.setColumnNumber(20) | |
| 50 | stackFrame.getColumnNumber() // => 20 | |
| 51 | ||
| 52 | stackFrame.source // => 'ORIGINAL_STACK_LINE' | |
| 53 | stackFrame.setSource('NEW_SOURCE') | |
| 54 | stackFrame.getSource() // => 'NEW_SOURCE' | |
| 55 | ||
| 56 | stackFrame.isEval // => true | |
| 57 | stackFrame.setIsEval(false) | |
| 58 | stackFrame.getIsEval() // => false | |
| 59 | ||
| 60 | stackFrame.isNative // => false | |
| 61 | stackFrame.setIsNative(true) | |
| 62 | stackFrame.getIsNative() // => true | |
| 63 | ||
| 64 | stackFrame.evalOrigin // => StackFrame({functionName: 'withinEval', lineNumber: ...}) | |
| 65 | stackFrame.setEvalOrigin({functionName: 'evalFn', fileName: 'anonymous'}) | |
| 66 | stackFrame.getEvalOrigin().getFunctionName() // => 'evalFn' | |
| 67 | ||
| 68 | stackFrame.toString() // => 'funName(args)@http://localhost:3000/file.js:325:20' | |
| 69 | ``` | |
| 70 | ||
| 71 | ## Browser Support | |
| 72 | [](https://saucelabs.com/u/stacktracejs) | |
| 73 | ||
| 74 | ## Installation | |
| 75 | ``` | |
| 76 | npm install stackframe | |
| 77 | bower install stackframe | |
| 78 | https://raw.githubusercontent.com/stacktracejs/stackframe/master/dist/stackframe.min.js | |
| 79 | ``` |
| … | … | @@ -0,0 +1,143 @@ |
| 1 | (function(root, factory) { | |
| 2 | 'use strict'; | |
| 3 | // Universal Module Definition (UMD) to support AMD, CommonJS/Node.js, Rhino, and browsers. | |
| 4 | ||
| 5 | /* istanbul ignore next */ | |
| 6 | if (typeof define === 'function' && define.amd) { | |
| 7 | define('stackframe', [], factory); | |
| 8 | } else if (typeof exports === 'object') { | |
| 9 | module.exports = factory(); | |
| 10 | } else { | |
| 11 | root.StackFrame = factory(); | |
| 12 | } | |
| 13 | }(this, function() { | |
| 14 | 'use strict'; | |
| 15 | function _isNumber(n) { | |
| 16 | return !isNaN(parseFloat(n)) && isFinite(n); | |
| 17 | } | |
| 18 | ||
| 19 | function _capitalize(str) { | |
| 20 | return str.charAt(0).toUpperCase() + str.substring(1); | |
| 21 | } | |
| 22 | ||
| 23 | function _getter(p) { | |
| 24 | return function() { | |
| 25 | return this[p]; | |
| 26 | }; | |
| 27 | } | |
| 28 | ||
| 29 | var booleanProps = ['isConstructor', 'isEval', 'isNative', 'isToplevel']; | |
| 30 | var numericProps = ['columnNumber', 'lineNumber']; | |
| 31 | var stringProps = ['fileName', 'functionName', 'source']; | |
| 32 | var arrayProps = ['args']; | |
| 33 | var objectProps = ['evalOrigin']; | |
| 34 | ||
| 35 | var props = booleanProps.concat(numericProps, stringProps, arrayProps, objectProps); | |
| 36 | ||
| 37 | function StackFrame(obj) { | |
| 38 | if (!obj) return; | |
| 39 | for (var i = 0; i < props.length; i++) { | |
| 40 | if (obj[props[i]] !== undefined) { | |
| 41 | this['set' + _capitalize(props[i])](obj[props[i]]); | |
| 42 | } | |
| 43 | } | |
| 44 | } | |
| 45 | ||
| 46 | StackFrame.prototype = { | |
| 47 | getArgs: function() { | |
| 48 | return this.args; | |
| 49 | }, | |
| 50 | setArgs: function(v) { | |
| 51 | if (Object.prototype.toString.call(v) !== '[object Array]') { | |
| 52 | throw new TypeError('Args must be an Array'); | |
| 53 | } | |
| 54 | this.args = v; | |
| 55 | }, | |
| 56 | ||
| 57 | getEvalOrigin: function() { | |
| 58 | return this.evalOrigin; | |
| 59 | }, | |
| 60 | setEvalOrigin: function(v) { | |
| 61 | if (v instanceof StackFrame) { | |
| 62 | this.evalOrigin = v; | |
| 63 | } else if (v instanceof Object) { | |
| 64 | this.evalOrigin = new StackFrame(v); | |
| 65 | } else { | |
| 66 | throw new TypeError('Eval Origin must be an Object or StackFrame'); | |
| 67 | } | |
| 68 | }, | |
| 69 | ||
| 70 | toString: function() { | |
| 71 | var fileName = this.getFileName() || ''; | |
| 72 | var lineNumber = this.getLineNumber() || ''; | |
| 73 | var columnNumber = this.getColumnNumber() || ''; | |
| 74 | var functionName = this.getFunctionName() || ''; | |
| 75 | if (this.getIsEval()) { | |
| 76 | if (fileName) { | |
| 77 | return '[eval] (' + fileName + ':' + lineNumber + ':' + columnNumber + ')'; | |
| 78 | } | |
| 79 | return '[eval]:' + lineNumber + ':' + columnNumber; | |
| 80 | } | |
| 81 | if (functionName) { | |
| 82 | return functionName + ' (' + fileName + ':' + lineNumber + ':' + columnNumber + ')'; | |
| 83 | } | |
| 84 | return fileName + ':' + lineNumber + ':' + columnNumber; | |
| 85 | } | |
| 86 | }; | |
| 87 | ||
| 88 | StackFrame.fromString = function StackFrame$$fromString(str) { | |
| 89 | var argsStartIndex = str.indexOf('('); | |
| 90 | var argsEndIndex = str.lastIndexOf(')'); | |
| 91 | ||
| 92 | var functionName = str.substring(0, argsStartIndex); | |
| 93 | var args = str.substring(argsStartIndex + 1, argsEndIndex).split(','); | |
| 94 | var locationString = str.substring(argsEndIndex + 1); | |
| 95 | ||
| 96 | if (locationString.indexOf('@') === 0) { | |
| 97 | var parts = /@(.+?)(?::(\d+))?(?::(\d+))?$/.exec(locationString, ''); | |
| 98 | var fileName = parts[1]; | |
| 99 | var lineNumber = parts[2]; | |
| 100 | var columnNumber = parts[3]; | |
| 101 | } | |
| 102 | ||
| 103 | return new StackFrame({ | |
| 104 | functionName: functionName, | |
| 105 | args: args || undefined, | |
| 106 | fileName: fileName, | |
| 107 | lineNumber: lineNumber || undefined, | |
| 108 | columnNumber: columnNumber || undefined | |
| 109 | }); | |
| 110 | }; | |
| 111 | ||
| 112 | for (var i = 0; i < booleanProps.length; i++) { | |
| 113 | StackFrame.prototype['get' + _capitalize(booleanProps[i])] = _getter(booleanProps[i]); | |
| 114 | StackFrame.prototype['set' + _capitalize(booleanProps[i])] = (function(p) { | |
| 115 | return function(v) { | |
| 116 | this[p] = Boolean(v); | |
| 117 | }; | |
| 118 | })(booleanProps[i]); | |
| 119 | } | |
| 120 | ||
| 121 | for (var j = 0; j < numericProps.length; j++) { | |
| 122 | StackFrame.prototype['get' + _capitalize(numericProps[j])] = _getter(numericProps[j]); | |
| 123 | StackFrame.prototype['set' + _capitalize(numericProps[j])] = (function(p) { | |
| 124 | return function(v) { | |
| 125 | if (!_isNumber(v)) { | |
| 126 | throw new TypeError(p + ' must be a Number'); | |
| 127 | } | |
| 128 | this[p] = Number(v); | |
| 129 | }; | |
| 130 | })(numericProps[j]); | |
| 131 | } | |
| 132 | ||
| 133 | for (var k = 0; k < stringProps.length; k++) { | |
| 134 | StackFrame.prototype['get' + _capitalize(stringProps[k])] = _getter(stringProps[k]); | |
| 135 | StackFrame.prototype['set' + _capitalize(stringProps[k])] = (function(p) { | |
| 136 | return function(v) { | |
| 137 | this[p] = String(v); | |
| 138 | }; | |
| 139 | })(stringProps[k]); | |
| 140 | } | |
| 141 | ||
| 142 | return StackFrame; | |
| 143 | })); |
| … | … | @@ -0,0 +1,2 @@ |
| 1 | !function(t,e){"use strict";"function"==typeof define&&define.amd?define("stackframe",[],e):"object"==typeof exports?module.exports=e():t.StackFrame=e()}(this,function(){"use strict";function t(t){return t.charAt(0).toUpperCase()+t.substring(1)}function e(t){return function(){return this[t]}}var r=["isConstructor","isEval","isNative","isToplevel"],n=["columnNumber","lineNumber"],i=["fileName","functionName","source"],o=r.concat(n,i,["args"],["evalOrigin"]);function s(e){if(e)for(var r=0;r<o.length;r++)void 0!==e[o[r]]&&this["set"+t(o[r])](e[o[r]])}s.prototype={getArgs:function(){return this.args},setArgs:function(t){if("[object Array]"!==Object.prototype.toString.call(t))throw new TypeError("Args must be an Array");this.args=t},getEvalOrigin:function(){return this.evalOrigin},setEvalOrigin:function(t){if(t instanceof s)this.evalOrigin=t;else{if(!(t instanceof Object))throw new TypeError("Eval Origin must be an Object or StackFrame");this.evalOrigin=new s(t)}},toString:function(){var t=this.getFileName()||"",e=this.getLineNumber()||"",r=this.getColumnNumber()||"",n=this.getFunctionName()||"";return this.getIsEval()?t?"[eval] ("+t+":"+e+":"+r+")":"[eval]:"+e+":"+r:n?n+" ("+t+":"+e+":"+r+")":t+":"+e+":"+r}},s.fromString=function(t){var e=t.indexOf("("),r=t.lastIndexOf(")"),n=t.substring(0,e),i=t.substring(e+1,r).split(","),o=t.substring(r+1);if(0===o.indexOf("@"))var u=/@(.+?)(?::(\d+))?(?::(\d+))?$/.exec(o,""),a=u[1],f=u[2],c=u[3];return new s({functionName:n,args:i||void 0,fileName:a,lineNumber:f||void 0,columnNumber:c||void 0})};for(var u=0;u<r.length;u++)s.prototype["get"+t(r[u])]=e(r[u]),s.prototype["set"+t(r[u])]=function(t){return function(e){this[t]=Boolean(e)}}(r[u]);for(var a=0;a<n.length;a++)s.prototype["get"+t(n[a])]=e(n[a]),s.prototype["set"+t(n[a])]=function(t){return function(e){if(r=e,isNaN(parseFloat(r))||!isFinite(r))throw new TypeError(t+" must be a Number");var r;this[t]=Number(e)}}(n[a]);for(var f=0;f<i.length;f++)s.prototype["get"+t(i[f])]=e(i[f]),s.prototype["set"+t(i[f])]=function(t){return function(e){this[t]=String(e)}}(i[f]);return s}); | |
| 2 | //# sourceMappingURL=stackframe.min.js.map |
| … | … | @@ -0,0 +1 @@ |
| 1 | {"version":3,"sources":["stackframe.js"],"names":["root","factory","define","amd","exports","module","StackFrame","this","_capitalize","str","charAt","toUpperCase","substring","_getter","p","booleanProps","numericProps","stringProps","props","concat","obj","i","length","undefined","prototype","getArgs","args","setArgs","v","Object","toString","call","TypeError","getEvalOrigin","evalOrigin","setEvalOrigin","fileName","getFileName","lineNumber","getLineNumber","columnNumber","getColumnNumber","functionName","getFunctionName","getIsEval","fromString","argsStartIndex","indexOf","argsEndIndex","lastIndexOf","split","locationString","parts","exec","Boolean","j","n","isNaN","parseFloat","isFinite","Number","k","String"],"mappings":"CAAC,SAASA,EAAMC,GACZ,aAIsB,mBAAXC,QAAyBA,OAAOC,IACvCD,OAAO,gBAAkBD,GACC,iBAAZG,QACdC,OAAOD,QAAUH,IAEjBD,EAAKM,WAAaL,IAV1B,CAYEM,KAAM,WACJ,aAKA,SAASC,EAAYC,GACjB,OAAOA,EAAIC,OAAO,GAAGC,cAAgBF,EAAIG,UAAU,GAGvD,SAASC,EAAQC,GACb,OAAO,WACH,OAAOP,KAAKO,IAIpB,IAAIC,GAAgB,gBAAiB,SAAU,WAAY,cACvDC,GAAgB,eAAgB,cAChCC,GAAe,WAAY,eAAgB,UAI3CC,EAAQH,EAAaI,OAAOH,EAAcC,GAH5B,SACC,eAInB,SAASX,EAAWc,GAChB,GAAKA,EACL,IAAK,IAAIC,EAAI,EAAGA,EAAIH,EAAMI,OAAQD,SACRE,IAAlBH,EAAIF,EAAMG,KACVd,KAAK,MAAQC,EAAYU,EAAMG,KAAKD,EAAIF,EAAMG,KAK1Df,EAAWkB,WACPC,QAAS,WACL,OAAOlB,KAAKmB,MAEhBC,QAAS,SAASC,GACd,GAA0C,mBAAtCC,OAAOL,UAAUM,SAASC,KAAKH,GAC/B,MAAM,IAAII,UAAU,yBAExBzB,KAAKmB,KAAOE,GAGhBK,cAAe,WACX,OAAO1B,KAAK2B,YAEhBC,cAAe,SAASP,GACpB,GAAIA,aAAatB,EACbC,KAAK2B,WAAaN,MACf,CAAA,KAAIA,aAAaC,QAGpB,MAAM,IAAIG,UAAU,+CAFpBzB,KAAK2B,WAAa,IAAI5B,EAAWsB,KAMzCE,SAAU,WACN,IAAIM,EAAW7B,KAAK8B,eAAiB,GACjCC,EAAa/B,KAAKgC,iBAAmB,GACrCC,EAAejC,KAAKkC,mBAAqB,GACzCC,EAAenC,KAAKoC,mBAAqB,GAC7C,OAAIpC,KAAKqC,YACDR,EACO,WAAaA,EAAW,IAAME,EAAa,IAAME,EAAe,IAEpE,UAAYF,EAAa,IAAME,EAEtCE,EACOA,EAAe,KAAON,EAAW,IAAME,EAAa,IAAME,EAAe,IAE7EJ,EAAW,IAAME,EAAa,IAAME,IAInDlC,EAAWuC,WAAa,SAAgCpC,GACpD,IAAIqC,EAAiBrC,EAAIsC,QAAQ,KAC7BC,EAAevC,EAAIwC,YAAY,KAE/BP,EAAejC,EAAIG,UAAU,EAAGkC,GAChCpB,EAAOjB,EAAIG,UAAUkC,EAAiB,EAAGE,GAAcE,MAAM,KAC7DC,EAAiB1C,EAAIG,UAAUoC,EAAe,GAElD,GAAoC,IAAhCG,EAAeJ,QAAQ,KACvB,IAAIK,EAAQ,gCAAgCC,KAAKF,EAAgB,IAC7Df,EAAWgB,EAAM,GACjBd,EAAac,EAAM,GACnBZ,EAAeY,EAAM,GAG7B,OAAO,IAAI9C,GACPoC,aAAcA,EACdhB,KAAMA,QAAQH,EACda,SAAUA,EACVE,WAAYA,QAAcf,EAC1BiB,aAAcA,QAAgBjB,KAItC,IAAK,IAAIF,EAAI,EAAGA,EAAIN,EAAaO,OAAQD,IACrCf,EAAWkB,UAAU,MAAQhB,EAAYO,EAAaM,KAAOR,EAAQE,EAAaM,IAClFf,EAAWkB,UAAU,MAAQhB,EAAYO,EAAaM,KAAO,SAAUP,GACnE,OAAO,SAASc,GACZrB,KAAKO,GAAKwC,QAAQ1B,IAFmC,CAI1Db,EAAaM,IAGpB,IAAK,IAAIkC,EAAI,EAAGA,EAAIvC,EAAaM,OAAQiC,IACrCjD,EAAWkB,UAAU,MAAQhB,EAAYQ,EAAauC,KAAO1C,EAAQG,EAAauC,IAClFjD,EAAWkB,UAAU,MAAQhB,EAAYQ,EAAauC,KAAO,SAAUzC,GACnE,OAAO,SAASc,GACZ,GA9GO4B,EA8GQ5B,EA7Gf6B,MAAMC,WAAWF,MAAOG,SAASH,GA8G7B,MAAM,IAAIxB,UAAUlB,EAAI,qBA/GxC,IAAmB0C,EAiHPjD,KAAKO,GAAK8C,OAAOhC,IALoC,CAO1DZ,EAAauC,IAGpB,IAAK,IAAIM,EAAI,EAAGA,EAAI5C,EAAYK,OAAQuC,IACpCvD,EAAWkB,UAAU,MAAQhB,EAAYS,EAAY4C,KAAOhD,EAAQI,EAAY4C,IAChFvD,EAAWkB,UAAU,MAAQhB,EAAYS,EAAY4C,KAAO,SAAU/C,GAClE,OAAO,SAASc,GACZrB,KAAKO,GAAKgD,OAAOlC,IAFmC,CAIzDX,EAAY4C,IAGnB,OAAOvD"} |
| … | … | @@ -0,0 +1,59 @@ |
| 1 | { | |
| 2 | "name": "stackframe", | |
| 3 | "description": "JS Object representation of a stack frame", | |
| 4 | "maintainers": [ | |
| 5 | "Eric Wendelin <me@eriwen.com> (https://www.eriwen.com)", | |
| 6 | "Victor Homyakov <vkhomyackov@gmail.com> (https://github.com/victor-homyakov)", | |
| 7 | "Oliver Salzburg (https://github.com/oliversalzburg)" | |
| 8 | ], | |
| 9 | "version": "1.3.4", | |
| 10 | "license": "MIT", | |
| 11 | "keywords": [ | |
| 12 | "stacktrace", | |
| 13 | "error", | |
| 14 | "debugger", | |
| 15 | "stack frame" | |
| 16 | ], | |
| 17 | "homepage": "https://www.stacktracejs.com", | |
| 18 | "repository": { | |
| 19 | "type": "git", | |
| 20 | "url": "git://github.com/stacktracejs/stackframe.git" | |
| 21 | }, | |
| 22 | "devDependencies": { | |
| 23 | "eslint": "^8.17.0", | |
| 24 | "jasmine": "^4.1.0", | |
| 25 | "jasmine-core": "^4.1.1", | |
| 26 | "karma": "^6.3.20", | |
| 27 | "karma-chrome-launcher": "^3.1.1", | |
| 28 | "karma-coverage": "^2.2.0", | |
| 29 | "karma-coveralls": "^2.1.0", | |
| 30 | "karma-firefox-launcher": "^2.1.2", | |
| 31 | "karma-ie-launcher": "^1.0.0", | |
| 32 | "karma-jasmine": "^4.0.2", | |
| 33 | "karma-opera-launcher": "^1.0.0", | |
| 34 | "karma-phantomjs-launcher": "^1.0.4", | |
| 35 | "karma-safari-launcher": "^1.0.0", | |
| 36 | "karma-sauce-launcher": "^4.3.6", | |
| 37 | "karma-spec-reporter": "^0.0.34", | |
| 38 | "uglify-es": "^3.3.9" | |
| 39 | }, | |
| 40 | "bugs": { | |
| 41 | "url": "https://github.com/stacktracejs/stackframe/issues" | |
| 42 | }, | |
| 43 | "main": "./stackframe.js", | |
| 44 | "typings": "./stackframe.d.ts", | |
| 45 | "files": [ | |
| 46 | "LICENSE", | |
| 47 | "README.md", | |
| 48 | "stackframe.js", | |
| 49 | "stackframe.d.ts", | |
| 50 | "dist/" | |
| 51 | ], | |
| 52 | "scripts": { | |
| 53 | "lint": "eslint", | |
| 54 | "test": "karma start karma.conf.js --single-run", | |
| 55 | "test-pr": "karma start karma.conf.js --single-run --browsers Firefox,Chrome_No_Sandbox", | |
| 56 | "test-ci": "karma start karma.conf.ci.js --single-run", | |
| 57 | "prepare": "cp stackframe.js dist/ && uglifyjs stackframe.js -o dist/stackframe.min.js --compress --mangle --source-map \"url=stackframe.min.js.map\"" | |
| 58 | } | |
| 59 | } |
| … | … | @@ -0,0 +1,74 @@ |
| 1 | // Type definitions for StackFrame v1.3 | |
| 2 | // Project: https://github.com/stacktracejs/stackframe | |
| 3 | // Definitions by: Eric Wendelin <https://www.eriwen.com> | |
| 4 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped | |
| 5 | ||
| 6 | export as namespace StackFrame; // global for non-module UMD users | |
| 7 | ||
| 8 | export = StackFrame; | |
| 9 | ||
| 10 | declare namespace StackFrame { | |
| 11 | export interface StackFrameOptions { | |
| 12 | isConstructor?: boolean; | |
| 13 | isEval?: boolean; | |
| 14 | isNative?: boolean; | |
| 15 | isToplevel?: boolean; | |
| 16 | columnNumber?: number; | |
| 17 | lineNumber?: number; | |
| 18 | fileName?: string; | |
| 19 | functionName?: string; | |
| 20 | source?: string; | |
| 21 | args?: any[]; | |
| 22 | evalOrigin?: StackFrame; | |
| 23 | } | |
| 24 | } | |
| 25 | ||
| 26 | declare class StackFrame { | |
| 27 | constructor(obj: StackFrame.StackFrameOptions); | |
| 28 | ||
| 29 | args?: any[]; | |
| 30 | getArgs(): any[] | undefined; | |
| 31 | setArgs(args: any[]): void; | |
| 32 | ||
| 33 | evalOrigin?: StackFrame; | |
| 34 | getEvalOrigin(): StackFrame | undefined; | |
| 35 | setEvalOrigin(stackframe: StackFrame): void; | |
| 36 | ||
| 37 | isConstructor?: boolean; | |
| 38 | getIsConstructor(): boolean | undefined; | |
| 39 | setIsConstructor(isConstructor: boolean): void; | |
| 40 | ||
| 41 | isEval?: boolean; | |
| 42 | getIsEval(): boolean | undefined; | |
| 43 | setIsEval(isEval: boolean): void; | |
| 44 | ||
| 45 | isNative?: boolean; | |
| 46 | getIsNative(): boolean | undefined; | |
| 47 | setIsNative(isNative: boolean): void; | |
| 48 | ||
| 49 | isToplevel?: boolean; | |
| 50 | getIsToplevel(): boolean | undefined; | |
| 51 | setIsToplevel(isToplevel: boolean): void; | |
| 52 | ||
| 53 | columnNumber?: number; | |
| 54 | getColumnNumber(): number | undefined; | |
| 55 | setColumnNumber(columnNumber: number): void; | |
| 56 | ||
| 57 | lineNumber?: number; | |
| 58 | getLineNumber(): number | undefined; | |
| 59 | setLineNumber(lineNumber: number): void; | |
| 60 | ||
| 61 | fileName?: string; | |
| 62 | getFileName(): string | undefined; | |
| 63 | setFileName(fileName: string): void; | |
| 64 | ||
| 65 | functionName?: string; | |
| 66 | getFunctionName(): string | undefined; | |
| 67 | setFunctionName(functionName: string): void; | |
| 68 | ||
| 69 | source?: string; | |
| 70 | getSource(): string | undefined; | |
| 71 | setSource(source: string): void; | |
| 72 | ||
| 73 | toString(): string; | |
| 74 | } |
| … | … | @@ -0,0 +1,143 @@ |
| 1 | (function(root, factory) { | |
| 2 | 'use strict'; | |
| 3 | // Universal Module Definition (UMD) to support AMD, CommonJS/Node.js, Rhino, and browsers. | |
| 4 | ||
| 5 | /* istanbul ignore next */ | |
| 6 | if (typeof define === 'function' && define.amd) { | |
| 7 | define('stackframe', [], factory); | |
| 8 | } else if (typeof exports === 'object') { | |
| 9 | module.exports = factory(); | |
| 10 | } else { | |
| 11 | root.StackFrame = factory(); | |
| 12 | } | |
| 13 | }(this, function() { | |
| 14 | 'use strict'; | |
| 15 | function _isNumber(n) { | |
| 16 | return !isNaN(parseFloat(n)) && isFinite(n); | |
| 17 | } | |
| 18 | ||
| 19 | function _capitalize(str) { | |
| 20 | return str.charAt(0).toUpperCase() + str.substring(1); | |
| 21 | } | |
| 22 | ||
| 23 | function _getter(p) { | |
| 24 | return function() { | |
| 25 | return this[p]; | |
| 26 | }; | |
| 27 | } | |
| 28 | ||
| 29 | var booleanProps = ['isConstructor', 'isEval', 'isNative', 'isToplevel']; | |
| 30 | var numericProps = ['columnNumber', 'lineNumber']; | |
| 31 | var stringProps = ['fileName', 'functionName', 'source']; | |
| 32 | var arrayProps = ['args']; | |
| 33 | var objectProps = ['evalOrigin']; | |
| 34 | ||
| 35 | var props = booleanProps.concat(numericProps, stringProps, arrayProps, objectProps); | |
| 36 | ||
| 37 | function StackFrame(obj) { | |
| 38 | if (!obj) return; | |
| 39 | for (var i = 0; i < props.length; i++) { | |
| 40 | if (obj[props[i]] !== undefined) { | |
| 41 | this['set' + _capitalize(props[i])](obj[props[i]]); | |
| 42 | } | |
| 43 | } | |
| 44 | } | |
| 45 | ||
| 46 | StackFrame.prototype = { | |
| 47 | getArgs: function() { | |
| 48 | return this.args; | |
| 49 | }, | |
| 50 | setArgs: function(v) { | |
| 51 | if (Object.prototype.toString.call(v) !== '[object Array]') { | |
| 52 | throw new TypeError('Args must be an Array'); | |
| 53 | } | |
| 54 | this.args = v; | |
| 55 | }, | |
| 56 | ||
| 57 | getEvalOrigin: function() { | |
| 58 | return this.evalOrigin; | |
| 59 | }, | |
| 60 | setEvalOrigin: function(v) { | |
| 61 | if (v instanceof StackFrame) { | |
| 62 | this.evalOrigin = v; | |
| 63 | } else if (v instanceof Object) { | |
| 64 | this.evalOrigin = new StackFrame(v); | |
| 65 | } else { | |
| 66 | throw new TypeError('Eval Origin must be an Object or StackFrame'); | |
| 67 | } | |
| 68 | }, | |
| 69 | ||
| 70 | toString: function() { | |
| 71 | var fileName = this.getFileName() || ''; | |
| 72 | var lineNumber = this.getLineNumber() || ''; | |
| 73 | var columnNumber = this.getColumnNumber() || ''; | |
| 74 | var functionName = this.getFunctionName() || ''; | |
| 75 | if (this.getIsEval()) { | |
| 76 | if (fileName) { | |
| 77 | return '[eval] (' + fileName + ':' + lineNumber + ':' + columnNumber + ')'; | |
| 78 | } | |
| 79 | return '[eval]:' + lineNumber + ':' + columnNumber; | |
| 80 | } | |
| 81 | if (functionName) { | |
| 82 | return functionName + ' (' + fileName + ':' + lineNumber + ':' + columnNumber + ')'; | |
| 83 | } | |
| 84 | return fileName + ':' + lineNumber + ':' + columnNumber; | |
| 85 | } | |
| 86 | }; | |
| 87 | ||
| 88 | StackFrame.fromString = function StackFrame$$fromString(str) { | |
| 89 | var argsStartIndex = str.indexOf('('); | |
| 90 | var argsEndIndex = str.lastIndexOf(')'); | |
| 91 | ||
| 92 | var functionName = str.substring(0, argsStartIndex); | |
| 93 | var args = str.substring(argsStartIndex + 1, argsEndIndex).split(','); | |
| 94 | var locationString = str.substring(argsEndIndex + 1); | |
| 95 | ||
| 96 | if (locationString.indexOf('@') === 0) { | |
| 97 | var parts = /@(.+?)(?::(\d+))?(?::(\d+))?$/.exec(locationString, ''); | |
| 98 | var fileName = parts[1]; | |
| 99 | var lineNumber = parts[2]; | |
| 100 | var columnNumber = parts[3]; | |
| 101 | } | |
| 102 | ||
| 103 | return new StackFrame({ | |
| 104 | functionName: functionName, | |
| 105 | args: args || undefined, | |
| 106 | fileName: fileName, | |
| 107 | lineNumber: lineNumber || undefined, | |
| 108 | columnNumber: columnNumber || undefined | |
| 109 | }); | |
| 110 | }; | |
| 111 | ||
| 112 | for (var i = 0; i < booleanProps.length; i++) { | |
| 113 | StackFrame.prototype['get' + _capitalize(booleanProps[i])] = _getter(booleanProps[i]); | |
| 114 | StackFrame.prototype['set' + _capitalize(booleanProps[i])] = (function(p) { | |
| 115 | return function(v) { | |
| 116 | this[p] = Boolean(v); | |
| 117 | }; | |
| 118 | })(booleanProps[i]); | |
| 119 | } | |
| 120 | ||
| 121 | for (var j = 0; j < numericProps.length; j++) { | |
| 122 | StackFrame.prototype['get' + _capitalize(numericProps[j])] = _getter(numericProps[j]); | |
| 123 | StackFrame.prototype['set' + _capitalize(numericProps[j])] = (function(p) { | |
| 124 | return function(v) { | |
| 125 | if (!_isNumber(v)) { | |
| 126 | throw new TypeError(p + ' must be a Number'); | |
| 127 | } | |
| 128 | this[p] = Number(v); | |
| 129 | }; | |
| 130 | })(numericProps[j]); | |
| 131 | } | |
| 132 | ||
| 133 | for (var k = 0; k < stringProps.length; k++) { | |
| 134 | StackFrame.prototype['get' + _capitalize(stringProps[k])] = _getter(stringProps[k]); | |
| 135 | StackFrame.prototype['set' + _capitalize(stringProps[k])] = (function(p) { | |
| 136 | return function(v) { | |
| 137 | this[p] = String(v); | |
| 138 | }; | |
| 139 | })(stringProps[k]); | |
| 140 | } | |
| 141 | ||
| 142 | return StackFrame; | |
| 143 | })); |
| … | … | @@ -0,0 +1,23 @@ |
| 1 | name: Build Status | |
| 2 | on: | |
| 3 | push: | |
| 4 | branches: | |
| 5 | - master | |
| 6 | pull_request: | |
| 7 | branches: | |
| 8 | - master | |
| 9 | jobs: | |
| 10 | build: | |
| 11 | strategy: | |
| 12 | matrix: | |
| 13 | node-version: [lts/*] | |
| 14 | os: [ubuntu-latest, macos-latest, windows-latest] | |
| 15 | runs-on: ${{ matrix.os }} | |
| 16 | steps: | |
| 17 | - uses: actions/checkout@v2 | |
| 18 | - name: Use Node.js ${{ matrix.node-version }} | |
| 19 | uses: actions/setup-node@v2 | |
| 20 | with: | |
| 21 | node-version: ${{ matrix.node-version }} | |
| 22 | - run: npm install | |
| 23 | - run: npm test |
| … | … | @@ -0,0 +1,21 @@ |
| 1 | The MIT License (MIT) | |
| 2 | ||
| 3 | Copyright (c) 2023 Mathias Buus | |
| 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 | |
| 13 | all 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 | |
| 21 | THE SOFTWARE. |
| … | … | @@ -0,0 +1,23 @@ |
| 1 | # test-tmp | |
| 2 | ||
| 3 | Get a fresh tmpdir for tests | |
| 4 | ||
| 5 | ``` | |
| 6 | npm install test-tmp | |
| 7 | ``` | |
| 8 | ||
| 9 | ## Usage | |
| 10 | ||
| 11 | ``` js | |
| 12 | const test = require('brittle') | |
| 13 | const tmp = require('test-tmp') | |
| 14 | ||
| 15 | test('my test', async function (t) { | |
| 16 | const dir = await tmp(t) | |
| 17 | console.log('fresh dir for this test', dir) | |
| 18 | }) | |
| 19 | ``` | |
| 20 | ||
| 21 | ## License | |
| 22 | ||
| 23 | MIT |
| … | … | @@ -0,0 +1,37 @@ |
| 1 | const os = require('os') | |
| 2 | const path = require('path') | |
| 3 | const fs = require('fs') | |
| 4 | ||
| 5 | module.exports = tmp | |
| 6 | ||
| 7 | async function tmp (t, { dir = null, name = null, order = Infinity, force = true } = {}) { | |
| 8 | if (!valid(name)) name = Math.random().toString(16).slice(2) | |
| 9 | ||
| 10 | if (dir) { | |
| 11 | await fs.promises.mkdir(dir, { recursive: true }) | |
| 12 | } | |
| 13 | ||
| 14 | const tmpdir = path.join(await fs.promises.realpath(dir || os.tmpdir()), 'tmp-test-' + name) | |
| 15 | ||
| 16 | try { | |
| 17 | await gc(tmpdir) | |
| 18 | } catch {} | |
| 19 | ||
| 20 | await fs.promises.mkdir(tmpdir, { recursive: true }) | |
| 21 | ||
| 22 | if (t) t.teardown(gc, { order, force }) | |
| 23 | return tmpdir | |
| 24 | ||
| 25 | async function gc () { | |
| 26 | await fs.promises.rm(tmpdir, { recursive: true }) | |
| 27 | } | |
| 28 | ||
| 29 | function valid (name) { | |
| 30 | if (typeof name !== 'string') return false | |
| 31 | ||
| 32 | const chars = /[<>:/\\|?*]/ | |
| 33 | const max = 64 | |
| 34 | ||
| 35 | return !chars.test(name) && name.length <= max | |
| 36 | } | |
| 37 | } |
| … | … | @@ -0,0 +1,42 @@ |
| 1 | { | |
| 2 | "name": "test-tmp", | |
| 3 | "version": "1.4.0", | |
| 4 | "description": "Get a fresh tmpdir for tests", | |
| 5 | "main": "index.js", | |
| 6 | "imports": { | |
| 7 | "os": { | |
| 8 | "bare": "bare-os", | |
| 9 | "default": "os" | |
| 10 | }, | |
| 11 | "path": { | |
| 12 | "bare": "bare-path", | |
| 13 | "default": "path" | |
| 14 | }, | |
| 15 | "fs": { | |
| 16 | "bare": "bare-fs", | |
| 17 | "default": "fs" | |
| 18 | } | |
| 19 | }, | |
| 20 | "scripts": { | |
| 21 | "test": "standard && brittle test.js" | |
| 22 | }, | |
| 23 | "devDependencies": { | |
| 24 | "brittle": "^3.3.2", | |
| 25 | "standard": "^17.1.0" | |
| 26 | }, | |
| 27 | "repository": { | |
| 28 | "type": "git", | |
| 29 | "url": "https://github.com/mafintosh/test-tmp.git" | |
| 30 | }, | |
| 31 | "author": "Mathias Buus (@mafintosh)", | |
| 32 | "license": "MIT", | |
| 33 | "bugs": { | |
| 34 | "url": "https://github.com/mafintosh/test-tmp/issues" | |
| 35 | }, | |
| 36 | "homepage": "https://github.com/mafintosh/test-tmp", | |
| 37 | "dependencies": { | |
| 38 | "bare-fs": "^4.0.1", | |
| 39 | "bare-os": "^3.3.0", | |
| 40 | "bare-path": "^3.0.0" | |
| 41 | } | |
| 42 | } |
| … | … | @@ -0,0 +1,47 @@ |
| 1 | const test = require('brittle') | |
| 2 | const tmp = require('./') | |
| 3 | const fs = require('fs') | |
| 4 | const os = require('os') | |
| 5 | const path = require('path') | |
| 6 | ||
| 7 | test('basic', async function (t) { | |
| 8 | const dir = await tmp(t) | |
| 9 | ||
| 10 | t.alike(await fs.promises.readdir(dir), []) | |
| 11 | }) | |
| 12 | ||
| 13 | test('specified name', async function (t) { | |
| 14 | const name = 'testdir' | |
| 15 | const dir = await tmp(t, { name }) | |
| 16 | t.ok(dir.includes(name), 'directory contains specified name') | |
| 17 | t.alike(await fs.promises.readdir(dir), []) | |
| 18 | }) | |
| 19 | ||
| 20 | test('invalid directory name', async function (t) { | |
| 21 | const name = '<>:/\\|?*' | |
| 22 | try { | |
| 23 | await tmp(t, { name }) | |
| 24 | t.pass('should default to a random name when an invalid input is provided') | |
| 25 | } catch (error) { | |
| 26 | t.fail('expected to handle an invalid directory name') | |
| 27 | } | |
| 28 | }) | |
| 29 | ||
| 30 | test('reuse directory', async function (t) { | |
| 31 | const name = 'existing-dir' | |
| 32 | const existing = path.join(await fs.promises.realpath(os.tmpdir()), 'tmp-test-' + name) | |
| 33 | ||
| 34 | await fs.promises.mkdir(existing, { recursive: true }) | |
| 35 | ||
| 36 | const dir = await tmp(t, { name }) | |
| 37 | t.is(dir, existing, 'uses the existing directory when it already exists') | |
| 38 | }) | |
| 39 | ||
| 40 | test('specify root directory', async function (t) { | |
| 41 | const dir = await tmp(t, { dir: './tmp-storage' }) | |
| 42 | ||
| 43 | const children = await fs.promises.readdir('./tmp-storage') | |
| 44 | t.is(children.length, 1) | |
| 45 | ||
| 46 | t.alike(await fs.promises.readdir(dir), []) | |
| 47 | }) |
| … | … | @@ -0,0 +1,21 @@ |
| 1 | The MIT License (MIT) | |
| 2 | ||
| 3 | Copyright (c) 2017 Mathias Buus | |
| 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 | |
| 13 | all 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 | |
| 21 | THE SOFTWARE. |
| … | … | @@ -0,0 +1,72 @@ |
| 1 | # time-ordered-set | |
| 2 | ||
| 3 | Efficiently maintain a set of nodes ordered by the time they were added to the set | |
| 4 | ||
| 5 | ``` | |
| 6 | npm install time-ordered-set | |
| 7 | ``` | |
| 8 | ||
| 9 | ## Usage | |
| 10 | ||
| 11 | ``` js | |
| 12 | const TOS = require('time-ordered-set') | |
| 13 | const s = new TOS() | |
| 14 | ||
| 15 | // add 3 nodes | |
| 16 | ||
| 17 | s.add({ | |
| 18 | hello: 'world' | |
| 19 | }) | |
| 20 | ||
| 21 | const node = s.add({ | |
| 22 | hello: 'welt' | |
| 23 | }) | |
| 24 | ||
| 25 | s.add({ | |
| 26 | hello: 'verden' | |
| 27 | }) | |
| 28 | ||
| 29 | // re-add the 2nd one | |
| 30 | ||
| 31 | s.add(node) | |
| 32 | console.log(s.toArray().map(node => node.hello)) // ['world', 'verden', 'welt'] | |
| 33 | ``` | |
| 34 | ||
| 35 | ## API | |
| 36 | ||
| 37 | #### `const s = new TOS()` | |
| 38 | ||
| 39 | Create a new set | |
| 40 | ||
| 41 | #### `node = s.add(node)` | |
| 42 | ||
| 43 | Add a new node to the set. Will add the properties `node.next` and `node.prev` to the node. | |
| 44 | Re-adding the same node will move to the latest node. | |
| 45 | ||
| 46 | #### `node = s.remove(node)` | |
| 47 | ||
| 48 | Remove a node. Will set `node.next` and `node.prev` to `null`. | |
| 49 | ||
| 50 | #### `bool = s.has(node)` | |
| 51 | ||
| 52 | Check if a node has been added. | |
| 53 | ||
| 54 | #### `const array = s.toArray([options])` | |
| 55 | ||
| 56 | Get an ordered array out of all the nodes, ordered from oldest to newest. Use `options.reverse: true` to get from newest to oldest. Set `options.limit: number` if you only want to get a subset. | |
| 57 | ||
| 58 | #### `s.oldest` | |
| 59 | ||
| 60 | Property containing the oldest node. | |
| 61 | ||
| 62 | #### `s.latest` | |
| 63 | ||
| 64 | Property containing the newest node. | |
| 65 | ||
| 66 | #### `s.length` | |
| 67 | ||
| 68 | Property containing how many nodes are in the set. | |
| 69 | ||
| 70 | ## License | |
| 71 | ||
| 72 | MIT |
| … | … | @@ -0,0 +1,72 @@ |
| 1 | module.exports = class TimeOrderedSet { | |
| 2 | constructor () { | |
| 3 | this.oldest = null | |
| 4 | this.latest = null | |
| 5 | this.length = 0 | |
| 6 | } | |
| 7 | ||
| 8 | has (node) { | |
| 9 | return !!(node.next || node.prev) || node === this.oldest | |
| 10 | } | |
| 11 | ||
| 12 | add (node) { | |
| 13 | if (this.has(node)) this.remove(node) | |
| 14 | ||
| 15 | if (!this.latest && !this.oldest) { | |
| 16 | this.latest = this.oldest = node | |
| 17 | node.prev = node.next = null | |
| 18 | } else { | |
| 19 | this.latest.next = node | |
| 20 | node.prev = this.latest | |
| 21 | node.next = null | |
| 22 | this.latest = node | |
| 23 | } | |
| 24 | ||
| 25 | this.length++ | |
| 26 | ||
| 27 | return node | |
| 28 | } | |
| 29 | ||
| 30 | remove (node) { | |
| 31 | if (!this.has(node)) return node | |
| 32 | ||
| 33 | if (this.oldest !== node && this.latest !== node) { | |
| 34 | node.prev.next = node.next | |
| 35 | node.next.prev = node.prev | |
| 36 | } else { | |
| 37 | if (this.oldest === node) { | |
| 38 | this.oldest = node.next | |
| 39 | if (this.oldest) this.oldest.prev = null | |
| 40 | } | |
| 41 | if (this.latest === node) { | |
| 42 | this.latest = node.prev | |
| 43 | if (this.latest) this.latest.next = null | |
| 44 | } | |
| 45 | } | |
| 46 | ||
| 47 | node.next = node.prev = null | |
| 48 | this.length-- | |
| 49 | ||
| 50 | return node | |
| 51 | } | |
| 52 | ||
| 53 | toArray ({ limit = Infinity, reverse = false } = {}) { | |
| 54 | const list = [] | |
| 55 | ||
| 56 | if (reverse) { | |
| 57 | let node = this.latest | |
| 58 | while (node && limit--) { | |
| 59 | list.push(node) | |
| 60 | node = node.prev | |
| 61 | } | |
| 62 | } else { | |
| 63 | let node = this.oldest | |
| 64 | while (node && limit--) { | |
| 65 | list.push(node) | |
| 66 | node = node.next | |
| 67 | } | |
| 68 | } | |
| 69 | ||
| 70 | return list | |
| 71 | } | |
| 72 | } |
| … | … | @@ -0,0 +1,26 @@ |
| 1 | { | |
| 2 | "name": "time-ordered-set", | |
| 3 | "version": "2.0.1", | |
| 4 | "description": "Efficiently maintain a set of nodes ordered by the time they were added to the set", | |
| 5 | "main": "index.js", | |
| 6 | "files": [ | |
| 7 | "index.js" | |
| 8 | ], | |
| 9 | "devDependencies": { | |
| 10 | "brittle": "^3.0.0", | |
| 11 | "standard": "^17.1.2" | |
| 12 | }, | |
| 13 | "scripts": { | |
| 14 | "test": "standard && brittle test.js" | |
| 15 | }, | |
| 16 | "repository": { | |
| 17 | "type": "git", | |
| 18 | "url": "https://github.com/mafintosh/time-ordered-set.git" | |
| 19 | }, | |
| 20 | "author": "Mathias Buus (@mafintosh)", | |
| 21 | "license": "MIT", | |
| 22 | "bugs": { | |
| 23 | "url": "https://github.com/mafintosh/time-ordered-set/issues" | |
| 24 | }, | |
| 25 | "homepage": "https://github.com/mafintosh/time-ordered-set" | |
| 26 | } |
| … | … | @@ -0,0 +1,15 @@ |
| 1 | The ISC License | |
| 2 | ||
| 3 | Copyright (c) Isaac Z. Schlueter and Contributors | |
| 4 | ||
| 5 | Permission to use, copy, modify, and/or distribute this software for any | |
| 6 | purpose with or without fee is hereby granted, provided that the above | |
| 7 | copyright notice and this permission notice appear in all copies. | |
| 8 | ||
| 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES | |
| 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF | |
| 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR | |
| 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES | |
| 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN | |
| 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR | |
| 15 | IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. |
| … | … | @@ -0,0 +1,96 @@ |
| 1 | # tmatch | |
| 2 | ||
| 3 | This module exists to facilitate the `t.match()` method in | |
| 4 | [`tap`](http://npm.im/tap). | |
| 5 | ||
| 6 | It checks whether a value matches a given "pattern". A pattern is an | |
| 7 | object with a set of fields that must be in the test object, or a | |
| 8 | regular expression that a test string must match, or any combination | |
| 9 | thereof. | |
| 10 | ||
| 11 | The algorithm is borrowed heavily from | |
| 12 | [`only-shallow`](http://npm.im/only-shallow), with some notable | |
| 13 | differences with respect to the handling of missing properties and the | |
| 14 | way that regular expressions are compared to strings. | |
| 15 | ||
| 16 | ## usage | |
| 17 | ||
| 18 | ```javascript | |
| 19 | var matches = require('tmatch') | |
| 20 | ||
| 21 | if (!matches(testObject, pattern)) console.log("yay! diversity!"); | |
| 22 | ||
| 23 | // somewhat more realistic example.. | |
| 24 | http.get(someUrl).on('response', function (res) { | |
| 25 | var expect = { | |
| 26 | statusCode: 200, | |
| 27 | headers: { | |
| 28 | server: /express/ | |
| 29 | } | |
| 30 | } | |
| 31 | ||
| 32 | if (!tmatch(res, expect)) { | |
| 33 | throw new Error('Expect 200 status code from express server') | |
| 34 | } | |
| 35 | }) | |
| 36 | ``` | |
| 37 | ||
| 38 | ## details | |
| 39 | ||
| 40 | Copied from the source, here are the details of `tmatch`'s algorithm: | |
| 41 | ||
| 42 | 1. If the object loosely equals the pattern, and either they're both | |
| 43 | objects or neither objects, then return true. Note that this | |
| 44 | covers object identity, some type coercion, and matching `null` | |
| 45 | against `undefined`, and avoids some stuff like `1 == [1]`. | |
| 46 | 2. If the object is a RegExp and the pattern is also a RegExp, return | |
| 47 | true if their source, global, multiline, lastIndex, and ignoreCase | |
| 48 | fields all match. | |
| 49 | 3. If the pattern is a RegExp, then return true if | |
| 50 | `pattern.test(object)`, casting the object to a string if it is not | |
| 51 | already a string. | |
| 52 | 4. If the pattern is a `Set`, then return true if all the keys in | |
| 53 | `pattern` appear in `object`. | |
| 54 | 5. If the pattern is a `Map`, then return true if all the keys in | |
| 55 | `pattern` are in `object`, and the values match as well. | |
| 56 | 6. If the object is a string and the pattern is a non-empty string, | |
| 57 | then return true if the string occurs within the object. | |
| 58 | 7. If the object and the pattern are both Date objects, then return | |
| 59 | true if they represent the same date. | |
| 60 | 8. If the object is a Date object, and the pattern is a string, then | |
| 61 | return true if the pattern is parseable as a date that is the same | |
| 62 | date as the object. | |
| 63 | 9. If the object is an `arguments` object, or the pattern is an | |
| 64 | `arguments` object, then cast them to arrays and compare their | |
| 65 | contents. | |
| 66 | 10. If the pattern is the `Buffer` constructor, then return true if | |
| 67 | the object is a Buffer. | |
| 68 | 11. If the pattern is the `Function` constructor, then return true if | |
| 69 | the object is a function. | |
| 70 | 12. If the pattern is the String constructor, then return true if the | |
| 71 | pattern is a string. | |
| 72 | 13. If the pattern is the Boolean constructor, then return true if the | |
| 73 | pattern is a boolean. | |
| 74 | 14. If the pattern is the Array constructor, then return true if the | |
| 75 | pattern is an array. | |
| 76 | 15. If the pattern is any function, and then object is an object, then | |
| 77 | return true if the object is an `instanceof` the pattern. | |
| 78 | 16. At this point, if the object or the pattern are not objects, then | |
| 79 | return false (because they would have matched earlier). | |
| 80 | 17. If the object is a buffer, and the pattern is also a buffer, then | |
| 81 | return true if they contain the same bytes. | |
| 82 | 18. At this point, both object and pattern are object type values, so | |
| 83 | compare their keys: | |
| 84 | 1. Get list of all iterable keys in pattern and object. If both | |
| 85 | are zero (two empty objects), return true. | |
| 86 | 2. Check to see if this pattern and this object have been tested | |
| 87 | already (to handle cycles). If so, return true, since the | |
| 88 | check higher up in the stack will catch any mismatch. | |
| 89 | 3. For each key in the pattern, match it against the corresponding | |
| 90 | key in object. Missing keys in object will be resolved to | |
| 91 | `undefined`, so it's possible to use `{foo:null}` as a pattern | |
| 92 | to ensure that the object *doesn't* have a `foo` property. | |
| 93 | ||
| 94 | ## license | |
| 95 | ||
| 96 | ISC. Go nuts. |
| … | … | @@ -0,0 +1,189 @@ |
| 1 | 'use strict' | |
| 2 | ||
| 3 | module.exports = match | |
| 4 | ||
| 5 | function isArguments (obj) { | |
| 6 | return Object.prototype.toString.call(obj) === '[object Arguments]' | |
| 7 | } | |
| 8 | ||
| 9 | function regexpSame (a, b) { | |
| 10 | return a.source === b.source && | |
| 11 | a.global === b.global && | |
| 12 | a.multiline === b.multiline && | |
| 13 | a.lastIndex === b.lastIndex && | |
| 14 | a.ignoreCase === b.ignoreCase | |
| 15 | } | |
| 16 | ||
| 17 | function arrayFrom (obj) { | |
| 18 | return Array.isArray(obj) ? obj | |
| 19 | : Array.from(obj) | |
| 20 | } | |
| 21 | ||
| 22 | function isSet (object) { | |
| 23 | return object instanceof Set | |
| 24 | } | |
| 25 | ||
| 26 | function isMap (object) { | |
| 27 | return object instanceof Map | |
| 28 | } | |
| 29 | ||
| 30 | function bufferSame (a, b) { | |
| 31 | var ret | |
| 32 | if (a.equals) { | |
| 33 | ret = a.equals(b) | |
| 34 | } else if (a.length !== b.length) { | |
| 35 | ret = false | |
| 36 | } else { | |
| 37 | ret = true | |
| 38 | for (var j = 0; j < a.length && ret; j++) { | |
| 39 | if (a[j] != b[j]) | |
| 40 | ret = false | |
| 41 | } | |
| 42 | } | |
| 43 | return ret | |
| 44 | } | |
| 45 | ||
| 46 | function match (obj, pattern) { | |
| 47 | return match_(obj, pattern, [], []) | |
| 48 | } | |
| 49 | ||
| 50 | function setMatch (obj, pattern, ca, cb) { | |
| 51 | var ret = true | |
| 52 | if (!isSet(obj)) | |
| 53 | ret = false | |
| 54 | else if (pattern.size === 0) | |
| 55 | ret = true | |
| 56 | else { | |
| 57 | var seen = new Set() | |
| 58 | pattern.forEach(function (entry) { | |
| 59 | if (!ret) | |
| 60 | return | |
| 61 | ||
| 62 | if (obj.has(entry) && !seen.has(entry)) { | |
| 63 | seen.add(entry) | |
| 64 | return | |
| 65 | } | |
| 66 | ||
| 67 | // see if obj anything that matches | |
| 68 | // but don't allow anything to match more than once. | |
| 69 | var done = false | |
| 70 | obj.forEach(function (objentry) { | |
| 71 | if (done || seen.has(objentry)) | |
| 72 | return | |
| 73 | ||
| 74 | if (match_(objentry, entry, ca, cb)) { | |
| 75 | seen.add(objentry) | |
| 76 | ret = true | |
| 77 | done = true | |
| 78 | } | |
| 79 | }) | |
| 80 | ||
| 81 | // didn't find the match by walking the object | |
| 82 | // not a match! | |
| 83 | if (!done) | |
| 84 | ret = false | |
| 85 | }) | |
| 86 | } | |
| 87 | return ret | |
| 88 | } | |
| 89 | ||
| 90 | function mapMatch (obj, pattern, ca, cb) { | |
| 91 | var ret = true | |
| 92 | if (!isMap(obj)) | |
| 93 | ret = false | |
| 94 | else if (pattern.size === 0) | |
| 95 | ret = true | |
| 96 | else { | |
| 97 | pattern.forEach(function (value, key) { | |
| 98 | if (ret) | |
| 99 | ret = obj.has(key) | |
| 100 | if (ret) | |
| 101 | ret = match_(value, obj.get(key), ca, cb) | |
| 102 | }) | |
| 103 | } | |
| 104 | return ret | |
| 105 | } | |
| 106 | ||
| 107 | ||
| 108 | function match_ (obj, pattern, ca, cb) { | |
| 109 | return obj == pattern ? ( | |
| 110 | obj === null || pattern === null ? true | |
| 111 | : typeof obj === 'object' && typeof pattern === 'object' ? true | |
| 112 | : typeof obj === 'object' && typeof pattern !== 'object' ? false | |
| 113 | : typeof obj !== 'object' && typeof pattern === 'object' ? false | |
| 114 | : true | |
| 115 | ) | |
| 116 | : obj === null || pattern === null ? false | |
| 117 | : typeof pattern === 'symbol' ? | |
| 118 | typeof obj === 'symbol' && obj.toString() === pattern.toString() | |
| 119 | : pattern instanceof RegExp ? ( | |
| 120 | typeof obj === 'string' ? pattern.test(obj) | |
| 121 | : obj instanceof RegExp ? regexpSame(obj, pattern) | |
| 122 | : pattern.test('' + obj) | |
| 123 | ) | |
| 124 | : isSet(pattern) ? setMatch(obj, pattern, ca, cb) | |
| 125 | : isMap(pattern) ? mapMatch(obj, pattern, ca, cb) | |
| 126 | : typeof obj === 'string' && typeof pattern === 'string' && pattern ? | |
| 127 | obj.indexOf(pattern) !== -1 | |
| 128 | : obj instanceof Date && pattern instanceof Date ? | |
| 129 | obj.getTime() === pattern.getTime() | |
| 130 | : obj instanceof Date && typeof pattern === 'string' ? | |
| 131 | obj.getTime() === new Date(pattern).getTime() | |
| 132 | : isArguments(obj) || isArguments(pattern) ? | |
| 133 | match_(arrayFrom(obj), arrayFrom(pattern), ca, cb) | |
| 134 | : pattern === Buffer ? Buffer.isBuffer(obj) | |
| 135 | : pattern === Function ? typeof obj === 'function' | |
| 136 | : pattern === Number ? | |
| 137 | typeof obj === 'number' && obj === obj && isFinite(obj) | |
| 138 | : pattern !== pattern ? obj !== obj | |
| 139 | : pattern === String ? typeof obj === 'string' | |
| 140 | : pattern === Symbol ? typeof obj === 'symbol' | |
| 141 | : pattern === Boolean ? typeof obj === 'boolean' | |
| 142 | : pattern === Array ? Array.isArray(obj) | |
| 143 | : typeof pattern === 'function' && typeof obj === 'object' ? | |
| 144 | obj instanceof pattern | |
| 145 | : typeof obj !== 'object' || typeof pattern !== 'object' ? false | |
| 146 | : Buffer.isBuffer(obj) && Buffer.isBuffer(pattern) ? | |
| 147 | bufferSame(obj, pattern) | |
| 148 | : matchObj(obj, pattern, Object.keys(obj), Object.keys(pattern), ca, cb) | |
| 149 | } | |
| 150 | ||
| 151 | function matchObj (obj, pattern, kobj, kpat, ca, cb) { | |
| 152 | var ret = true | |
| 153 | ||
| 154 | // don't bother with stack acrobatics if there's nothing there | |
| 155 | if (kobj.length === 0 && kpat.length === 0) | |
| 156 | ret = true | |
| 157 | else { | |
| 158 | // if we've seen this exact pattern and object already, then | |
| 159 | // it means that pattern and obj have matching cyclicalness | |
| 160 | // however, non-cyclical patterns can match cyclical objects | |
| 161 | var cal = ca.length | |
| 162 | var go = true | |
| 163 | while (cal-- && go) { | |
| 164 | if (ca[cal] === obj && cb[cal] === pattern) { | |
| 165 | ret = true | |
| 166 | go = false | |
| 167 | } | |
| 168 | } | |
| 169 | ||
| 170 | if (go) { | |
| 171 | ca.push(obj) | |
| 172 | cb.push(pattern) | |
| 173 | ||
| 174 | var key | |
| 175 | for (var l = kpat.length - 1; l >= 0 && ret; l--) { | |
| 176 | key = kpat[l] | |
| 177 | if (!match_(obj[key], pattern[key], ca, cb)) | |
| 178 | ret = false | |
| 179 | } | |
| 180 | ||
| 181 | if (ret) { | |
| 182 | ca.pop() | |
| 183 | cb.pop() | |
| 184 | } | |
| 185 | } | |
| 186 | } | |
| 187 | ||
| 188 | return ret | |
| 189 | } |
| … | … | @@ -0,0 +1,31 @@ |
| 1 | { | |
| 2 | "name": "tmatch", | |
| 3 | "version": "5.0.0", | |
| 4 | "description": "This module exists to facilitate the `t.match()` method in [`tap`](http://npm.im/tap).", | |
| 5 | "main": "index.js", | |
| 6 | "scripts": { | |
| 7 | "test": "tap --100 test/*.js -J", | |
| 8 | "preversion": "npm test", | |
| 9 | "postversion": "npm publish", | |
| 10 | "postpublish": "git push origin --all; git push origin --tags" | |
| 11 | }, | |
| 12 | "repository": { | |
| 13 | "type": "git", | |
| 14 | "url": "git+https://github.com/isaacs/tmatch.git" | |
| 15 | }, | |
| 16 | "author": "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me/)", | |
| 17 | "license": "ISC", | |
| 18 | "bugs": { | |
| 19 | "url": "https://github.com/isaacs/tmatch/issues" | |
| 20 | }, | |
| 21 | "homepage": "https://github.com/isaacs/tmatch#readme", | |
| 22 | "devDependencies": { | |
| 23 | "tap": "^12.5.1" | |
| 24 | }, | |
| 25 | "files": [ | |
| 26 | "index.js" | |
| 27 | ], | |
| 28 | "engines": { | |
| 29 | "node": ">=8" | |
| 30 | } | |
| 31 | } |
| … | … | @@ -0,0 +1,81 @@ |
| 1 | cmake_minimum_required(VERSION 3.25) | |
| 2 | ||
| 3 | find_package(cmake-bare REQUIRED PATHS node_modules/cmake-bare) | |
| 4 | find_package(cmake-fetch REQUIRED PATHS node_modules/cmake-fetch) | |
| 5 | find_package(cmake-napi REQUIRED PATHS node_modules/cmake-napi) | |
| 6 | find_package(cmake-npm REQUIRED PATHS node_modules/cmake-npm) | |
| 7 | ||
| 8 | project(udx_native C CXX) | |
| 9 | ||
| 10 | bare_target(target) | |
| 11 | ||
| 12 | if(target MATCHES "win32") | |
| 13 | add_compile_options(/MT$<$<CONFIG:Debug>:d>) | |
| 14 | endif() | |
| 15 | ||
| 16 | fetch_package("github:holepunchto/libudx#25b063b") | |
| 17 | fetch_package("github:holepunchto/libjstl#098664c") | |
| 18 | ||
| 19 | add_bare_module(udx_native_bare) | |
| 20 | ||
| 21 | target_sources( | |
| 22 | ${udx_native_bare} | |
| 23 | PRIVATE | |
| 24 | binding.cc | |
| 25 | ) | |
| 26 | ||
| 27 | target_link_libraries( | |
| 28 | ${udx_native_bare} | |
| 29 | PRIVATE | |
| 30 | $<TARGET_OBJECTS:udx> | |
| 31 | jstl | |
| 32 | PUBLIC | |
| 33 | udx | |
| 34 | ) | |
| 35 | ||
| 36 | if(target MATCHES "win32") | |
| 37 | target_link_libraries( | |
| 38 | ${udx_native_bare} | |
| 39 | PUBLIC | |
| 40 | ws2_32 | |
| 41 | ) | |
| 42 | endif() | |
| 43 | ||
| 44 | add_napi_module(udx_native_napi) | |
| 45 | ||
| 46 | target_sources( | |
| 47 | ${udx_native_napi} | |
| 48 | PRIVATE | |
| 49 | binding.cc | |
| 50 | ) | |
| 51 | ||
| 52 | target_compile_definitions( | |
| 53 | ${udx_native_napi} | |
| 54 | PRIVATE | |
| 55 | NAPI_VERSION=9 | |
| 56 | ) | |
| 57 | ||
| 58 | target_link_libraries( | |
| 59 | ${udx_native_napi} | |
| 60 | PRIVATE | |
| 61 | $<TARGET_OBJECTS:udx> | |
| 62 | jstl | |
| 63 | PUBLIC | |
| 64 | udx | |
| 65 | ) | |
| 66 | ||
| 67 | if(target MATCHES "win32") | |
| 68 | target_link_libraries( | |
| 69 | ${udx_native_napi} | |
| 70 | PUBLIC | |
| 71 | ws2_32 | |
| 72 | ) | |
| 73 | endif() | |
| 74 | ||
| 75 | resolve_node_module(bare-compat-napi compat) | |
| 76 | ||
| 77 | target_include_directories( | |
| 78 | ${udx_native_napi} | |
| 79 | PRIVATE | |
| 80 | "${compat}/include" | |
| 81 | ) |
| … | … | @@ -0,0 +1,201 @@ |
| 1 | Apache License | |
| 2 | Version 2.0, January 2004 | |
| 3 | http://www.apache.org/licenses/ | |
| 4 | ||
| 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | |
| 6 | ||
| 7 | 1. Definitions. | |
| 8 | ||
| 9 | "License" shall mean the terms and conditions for use, reproduction, | |
| 10 | and distribution as defined by Sections 1 through 9 of this document. | |
| 11 | ||
| 12 | "Licensor" shall mean the copyright owner or entity authorized by | |
| 13 | the copyright owner that is granting the License. | |
| 14 | ||
| 15 | "Legal Entity" shall mean the union of the acting entity and all | |
| 16 | other entities that control, are controlled by, or are under common | |
| 17 | control with that entity. For the purposes of this definition, | |
| 18 | "control" means (i) the power, direct or indirect, to cause the | |
| 19 | direction or management of such entity, whether by contract or | |
| 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the | |
| 21 | outstanding shares, or (iii) beneficial ownership of such entity. | |
| 22 | ||
| 23 | "You" (or "Your") shall mean an individual or Legal Entity | |
| 24 | exercising permissions granted by this License. | |
| 25 | ||
| 26 | "Source" form shall mean the preferred form for making modifications, | |
| 27 | including but not limited to software source code, documentation | |
| 28 | source, and configuration files. | |
| 29 | ||
| 30 | "Object" form shall mean any form resulting from mechanical | |
| 31 | transformation or translation of a Source form, including but | |
| 32 | not limited to compiled object code, generated documentation, | |
| 33 | and conversions to other media types. | |
| 34 | ||
| 35 | "Work" shall mean the work of authorship, whether in Source or | |
| 36 | Object form, made available under the License, as indicated by a | |
| 37 | copyright notice that is included in or attached to the work | |
| 38 | (an example is provided in the Appendix below). | |
| 39 | ||
| 40 | "Derivative Works" shall mean any work, whether in Source or Object | |
| 41 | form, that is based on (or derived from) the Work and for which the | |
| 42 | editorial revisions, annotations, elaborations, or other modifications | |
| 43 | represent, as a whole, an original work of authorship. For the purposes | |
| 44 | of this License, Derivative Works shall not include works that remain | |
| 45 | separable from, or merely link (or bind by name) to the interfaces of, | |
| 46 | the Work and Derivative Works thereof. | |
| 47 | ||
| 48 | "Contribution" shall mean any work of authorship, including | |
| 49 | the original version of the Work and any modifications or additions | |
| 50 | to that Work or Derivative Works thereof, that is intentionally | |
| 51 | submitted to Licensor for inclusion in the Work by the copyright owner | |
| 52 | or by an individual or Legal Entity authorized to submit on behalf of | |
| 53 | the copyright owner. For the purposes of this definition, "submitted" | |
| 54 | means any form of electronic, verbal, or written communication sent | |
| 55 | to the Licensor or its representatives, including but not limited to | |
| 56 | communication on electronic mailing lists, source code control systems, | |
| 57 | and issue tracking systems that are managed by, or on behalf of, the | |
| 58 | Licensor for the purpose of discussing and improving the Work, but | |
| 59 | excluding communication that is conspicuously marked or otherwise | |
| 60 | designated in writing by the copyright owner as "Not a Contribution." | |
| 61 | ||
| 62 | "Contributor" shall mean Licensor and any individual or Legal Entity | |
| 63 | on behalf of whom a Contribution has been received by Licensor and | |
| 64 | subsequently incorporated within the Work. | |
| 65 | ||
| 66 | 2. Grant of Copyright License. Subject to the terms and conditions of | |
| 67 | this License, each Contributor hereby grants to You a perpetual, | |
| 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 69 | copyright license to reproduce, prepare Derivative Works of, | |
| 70 | publicly display, publicly perform, sublicense, and distribute the | |
| 71 | Work and such Derivative Works in Source or Object form. | |
| 72 | ||
| 73 | 3. Grant of Patent License. Subject to the terms and conditions of | |
| 74 | this License, each Contributor hereby grants to You a perpetual, | |
| 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 76 | (except as stated in this section) patent license to make, have made, | |
| 77 | use, offer to sell, sell, import, and otherwise transfer the Work, | |
| 78 | where such license applies only to those patent claims licensable | |
| 79 | by such Contributor that are necessarily infringed by their | |
| 80 | Contribution(s) alone or by combination of their Contribution(s) | |
| 81 | with the Work to which such Contribution(s) was submitted. If You | |
| 82 | institute patent litigation against any entity (including a | |
| 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work | |
| 84 | or a Contribution incorporated within the Work constitutes direct | |
| 85 | or contributory patent infringement, then any patent licenses | |
| 86 | granted to You under this License for that Work shall terminate | |
| 87 | as of the date such litigation is filed. | |
| 88 | ||
| 89 | 4. Redistribution. You may reproduce and distribute copies of the | |
| 90 | Work or Derivative Works thereof in any medium, with or without | |
| 91 | modifications, and in Source or Object form, provided that You | |
| 92 | meet the following conditions: | |
| 93 | ||
| 94 | (a) You must give any other recipients of the Work or | |
| 95 | Derivative Works a copy of this License; and | |
| 96 | ||
| 97 | (b) You must cause any modified files to carry prominent notices | |
| 98 | stating that You changed the files; and | |
| 99 | ||
| 100 | (c) You must retain, in the Source form of any Derivative Works | |
| 101 | that You distribute, all copyright, patent, trademark, and | |
| 102 | attribution notices from the Source form of the Work, | |
| 103 | excluding those notices that do not pertain to any part of | |
| 104 | the Derivative Works; and | |
| 105 | ||
| 106 | (d) If the Work includes a "NOTICE" text file as part of its | |
| 107 | distribution, then any Derivative Works that You distribute must | |
| 108 | include a readable copy of the attribution notices contained | |
| 109 | within such NOTICE file, excluding those notices that do not | |
| 110 | pertain to any part of the Derivative Works, in at least one | |
| 111 | of the following places: within a NOTICE text file distributed | |
| 112 | as part of the Derivative Works; within the Source form or | |
| 113 | documentation, if provided along with the Derivative Works; or, | |
| 114 | within a display generated by the Derivative Works, if and | |
| 115 | wherever such third-party notices normally appear. The contents | |
| 116 | of the NOTICE file are for informational purposes only and | |
| 117 | do not modify the License. You may add Your own attribution | |
| 118 | notices within Derivative Works that You distribute, alongside | |
| 119 | or as an addendum to the NOTICE text from the Work, provided | |
| 120 | that such additional attribution notices cannot be construed | |
| 121 | as modifying the License. | |
| 122 | ||
| 123 | You may add Your own copyright statement to Your modifications and | |
| 124 | may provide additional or different license terms and conditions | |
| 125 | for use, reproduction, or distribution of Your modifications, or | |
| 126 | for any such Derivative Works as a whole, provided Your use, | |
| 127 | reproduction, and distribution of the Work otherwise complies with | |
| 128 | the conditions stated in this License. | |
| 129 | ||
| 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, | |
| 131 | any Contribution intentionally submitted for inclusion in the Work | |
| 132 | by You to the Licensor shall be under the terms and conditions of | |
| 133 | this License, without any additional terms or conditions. | |
| 134 | Notwithstanding the above, nothing herein shall supersede or modify | |
| 135 | the terms of any separate license agreement you may have executed | |
| 136 | with Licensor regarding such Contributions. | |
| 137 | ||
| 138 | 6. Trademarks. This License does not grant permission to use the trade | |
| 139 | names, trademarks, service marks, or product names of the Licensor, | |
| 140 | except as required for reasonable and customary use in describing the | |
| 141 | origin of the Work and reproducing the content of the NOTICE file. | |
| 142 | ||
| 143 | 7. Disclaimer of Warranty. Unless required by applicable law or | |
| 144 | agreed to in writing, Licensor provides the Work (and each | |
| 145 | Contributor provides its Contributions) on an "AS IS" BASIS, | |
| 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |
| 147 | implied, including, without limitation, any warranties or conditions | |
| 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | |
| 149 | PARTICULAR PURPOSE. You are solely responsible for determining the | |
| 150 | appropriateness of using or redistributing the Work and assume any | |
| 151 | risks associated with Your exercise of permissions under this License. | |
| 152 | ||
| 153 | 8. Limitation of Liability. In no event and under no legal theory, | |
| 154 | whether in tort (including negligence), contract, or otherwise, | |
| 155 | unless required by applicable law (such as deliberate and grossly | |
| 156 | negligent acts) or agreed to in writing, shall any Contributor be | |
| 157 | liable to You for damages, including any direct, indirect, special, | |
| 158 | incidental, or consequential damages of any character arising as a | |
| 159 | result of this License or out of the use or inability to use the | |
| 160 | Work (including but not limited to damages for loss of goodwill, | |
| 161 | work stoppage, computer failure or malfunction, or any and all | |
| 162 | other commercial damages or losses), even if such Contributor | |
| 163 | has been advised of the possibility of such damages. | |
| 164 | ||
| 165 | 9. Accepting Warranty or Additional Liability. While redistributing | |
| 166 | the Work or Derivative Works thereof, You may choose to offer, | |
| 167 | and charge a fee for, acceptance of support, warranty, indemnity, | |
| 168 | or other liability obligations and/or rights consistent with this | |
| 169 | License. However, in accepting such obligations, You may act only | |
| 170 | on Your own behalf and on Your sole responsibility, not on behalf | |
| 171 | of any other Contributor, and only if You agree to indemnify, | |
| 172 | defend, and hold each Contributor harmless for any liability | |
| 173 | incurred by, or claims asserted against, such Contributor by reason | |
| 174 | of your accepting any such warranty or additional liability. | |
| 175 | ||
| 176 | END OF TERMS AND CONDITIONS | |
| 177 | ||
| 178 | APPENDIX: How to apply the Apache License to your work. | |
| 179 | ||
| 180 | To apply the Apache License to your work, attach the following | |
| 181 | boilerplate notice, with the fields enclosed by brackets "[]" | |
| 182 | replaced with your own identifying information. (Don't include | |
| 183 | the brackets!) The text should be enclosed in the appropriate | |
| 184 | comment syntax for the file format. We also recommend that a | |
| 185 | file or class name and description of purpose be included on the | |
| 186 | same "printed page" as the copyright notice for easier | |
| 187 | identification within third-party archives. | |
| 188 | ||
| 189 | Copyright [yyyy] [name of copyright owner] | |
| 190 | ||
| 191 | Licensed under the Apache License, Version 2.0 (the "License"); | |
| 192 | you may not use this file except in compliance with the License. | |
| 193 | You may obtain a copy of the License at | |
| 194 | ||
| 195 | http://www.apache.org/licenses/LICENSE-2.0 | |
| 196 | ||
| 197 | Unless required by applicable law or agreed to in writing, software | |
| 198 | distributed under the License is distributed on an "AS IS" BASIS, | |
| 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 200 | See the License for the specific language governing permissions and | |
| 201 | limitations under the License. |
| … | … | @@ -0,0 +1,403 @@ |
| 1 | # udx-native | |
| 2 | ||
| 3 | udx is reliable, multiplexed, and congestion-controlled streams over udp. | |
| 4 | ||
| 5 | ``` | |
| 6 | npm i udx-native | |
| 7 | ``` | |
| 8 | ||
| 9 | It's a transport protocol, made only for peer-to-peer networking. | |
| 10 | ||
| 11 | No handshakes. No encryption. No features. This is good for P2P.\ | |
| 12 | Just fast streams and messages that are composable into powerful things. | |
| 13 | ||
| 14 | ## Usage | |
| 15 | ||
| 16 | ```js | |
| 17 | const UDX = require('udx-native') | |
| 18 | ||
| 19 | const u = new UDX() | |
| 20 | const a = u.createSocket() | |
| 21 | const b = u.createSocket() | |
| 22 | ||
| 23 | b.on('message', function (message) { | |
| 24 | console.log('received', message.toString()) | |
| 25 | a.close() | |
| 26 | b.close() | |
| 27 | }) | |
| 28 | ||
| 29 | b.bind(0) | |
| 30 | a.send(Buffer.from('hello'), b.address().port) | |
| 31 | ``` | |
| 32 | ||
| 33 | ```js | |
| 34 | const UDX = require('udx-native') | |
| 35 | ||
| 36 | const u = new UDX() | |
| 37 | ||
| 38 | const socket1 = u.createSocket() | |
| 39 | const socket2 = u.createSocket() | |
| 40 | ||
| 41 | socket1.bind() | |
| 42 | socket2.bind() | |
| 43 | ||
| 44 | const stream1 = u.createStream(1) | |
| 45 | const stream2 = u.createStream(2) | |
| 46 | ||
| 47 | stream1.connect(socket1, stream2.id, socket2.address().port, '127.0.0.1') | |
| 48 | stream2.connect(socket2, stream1.id, socket1.address().port, '127.0.0.1') | |
| 49 | ||
| 50 | stream1.write(Buffer.from('hello')) | |
| 51 | stream1.end() | |
| 52 | ||
| 53 | stream2.on('data', function (data) { | |
| 54 | console.log(data) | |
| 55 | }) | |
| 56 | ||
| 57 | stream2.on('end', function () { | |
| 58 | stream2.end() | |
| 59 | }) | |
| 60 | ||
| 61 | stream1.on('close', function () { | |
| 62 | console.log('stream1 closed') | |
| 63 | socket1.close() | |
| 64 | }) | |
| 65 | ||
| 66 | stream2.on('close', function () { | |
| 67 | console.log('stream2 closed') | |
| 68 | socket2.close() | |
| 69 | }) | |
| 70 | ``` | |
| 71 | ||
| 72 | ## API | |
| 73 | ||
| 74 | #### `const udx = new UDX()` | |
| 75 | ||
| 76 | Creates a new UDX instance. | |
| 77 | ||
| 78 | #### `const bool = UDX.isIPv4(host)` | |
| 79 | ||
| 80 | Returns `true` if host is an IPv4 address. | |
| 81 | ||
| 82 | #### `const bool = UDX.isIPv6(host)` | |
| 83 | ||
| 84 | Returns `true` if host is an IPv6 address. | |
| 85 | ||
| 86 | #### `const family = UDX.isIP(host)` | |
| 87 | ||
| 88 | Returns the address family (`4` or `6`). Returns `0` if invalid. | |
| 89 | ||
| 90 | ## Sockets | |
| 91 | ||
| 92 | #### `const socket = udx.createSocket([options])` | |
| 93 | ||
| 94 | Creates a new socket instance. | |
| 95 | ||
| 96 | Available `options`: | |
| 97 | ||
| 98 | ```js | |
| 99 | { | |
| 100 | ipv6Only: false, | |
| 101 | reuseAddress: false | |
| 102 | } | |
| 103 | ``` | |
| 104 | ||
| 105 | #### `socket.udx` | |
| 106 | ||
| 107 | It's the UDX instance from where the socket was created. | |
| 108 | ||
| 109 | #### `socket.streams` | |
| 110 | ||
| 111 | It's a `Set` that tracks active streams (connected to the socket but not closed). | |
| 112 | ||
| 113 | #### `socket.userData` | |
| 114 | ||
| 115 | Optional custom userData. Default is `null`. | |
| 116 | ||
| 117 | #### `socket.bound` | |
| 118 | ||
| 119 | Indicates if it's bound to any port. It will be `true` after a successful `bind()`. | |
| 120 | ||
| 121 | #### `socket.closing` | |
| 122 | ||
| 123 | It will be `true` after `close()` is called. | |
| 124 | ||
| 125 | #### `socket.idle` | |
| 126 | ||
| 127 | Indicates that the socket doesn't have any connected stream. | |
| 128 | ||
| 129 | #### `socket.busy` | |
| 130 | ||
| 131 | Indicates that the socket have at least one connected stream. | |
| 132 | ||
| 133 | #### `socket.address()` | |
| 134 | ||
| 135 | Returns an object like `{ host, family, port }`. Only available after `bind()`. | |
| 136 | ||
| 137 | #### `socket.bind([port], [host])` | |
| 138 | ||
| 139 | The default port is `0`.\ | |
| 140 | If no host specified: it binds to IPv6 `::`. If fails then IPv4 `0.0.0.0`. | |
| 141 | ||
| 142 | #### `await socket.close()` | |
| 143 | ||
| 144 | It unbinds the socket so it stops listening for messages. | |
| 145 | ||
| 146 | #### `socket.setTTL(ttl)` | |
| 147 | ||
| 148 | Sets the amount of times that a packet is allowed to be forwarded through each router or gateway before being discarded. | |
| 149 | ||
| 150 | #### `socket.getRecvBufferSize()` | |
| 151 | ||
| 152 | #### `socket.setRecvBufferSize()` | |
| 153 | ||
| 154 | #### `socket.getSendBufferSize()` | |
| 155 | ||
| 156 | #### `socket.setSendBufferSize()` | |
| 157 | ||
| 158 | #### `await socket.send(buffer, port, [host], [ttl])` | |
| 159 | ||
| 160 | Sends a message to port and host destination. Default host is `127.0.0.1`. | |
| 161 | ||
| 162 | #### `socket.trySend(buffer, port, [host], [ttl])` | |
| 163 | ||
| 164 | Same behaviour as `send()` but no promise. | |
| 165 | ||
| 166 | #### `socket.on('message', (msg, from) => {})` | |
| 167 | ||
| 168 | `msg` is a buffer that containts the message.\ | |
| 169 | `from` is an object like `{ host, family, port }`. | |
| 170 | ||
| 171 | #### `socket.on('close', onclose)` | |
| 172 | ||
| 173 | Emitted if the socket was ever bound and it got closed. | |
| 174 | ||
| 175 | #### `socket.on('idle', onidle)` | |
| 176 | ||
| 177 | Emitted if the socket becomes idle (no active streams). | |
| 178 | ||
| 179 | #### `socket.on('busy', onbusy)` | |
| 180 | ||
| 181 | Emitted if the socket becomes busy (at least one active stream). | |
| 182 | ||
| 183 | #### `socket.on('listening', onlistening)` | |
| 184 | ||
| 185 | Emitted after a succesfull `bind()` call. | |
| 186 | ||
| 187 | ## Streams | |
| 188 | ||
| 189 | #### `const stream = udx.createStream(id, [options])` | |
| 190 | ||
| 191 | Creates a new stream instance that is a Duplex stream. | |
| 192 | ||
| 193 | Available `options`: | |
| 194 | ||
| 195 | ```js | |
| 196 | { | |
| 197 | firewall: (socket, port, host) => true, | |
| 198 | framed: false, | |
| 199 | seq: 0 | |
| 200 | } | |
| 201 | ``` | |
| 202 | ||
| 203 | #### `stream.udx` | |
| 204 | ||
| 205 | It's the UDX instance from where the stream was created. | |
| 206 | ||
| 207 | #### `stream.socket` | |
| 208 | ||
| 209 | Refers to the socket that is connected to. Setted when you `connect()` the stream. | |
| 210 | ||
| 211 | #### `stream.id` | |
| 212 | ||
| 213 | Custom stream id. | |
| 214 | ||
| 215 | #### `stream.remoteId` | |
| 216 | ||
| 217 | Remote stream id. Setted when you `connect()` the stream. | |
| 218 | ||
| 219 | #### `stream.remoteId` | |
| 220 | ||
| 221 | Remote stream id. Setted when you `connect()` the stream. | |
| 222 | ||
| 223 | #### `stream.remoteHost` | |
| 224 | ||
| 225 | Remote host. Setted when you `connect()` the stream. | |
| 226 | ||
| 227 | #### `stream.remoteFamily` | |
| 228 | ||
| 229 | Remote family (`4` or `6`). Setted when you `connect()` the stream. | |
| 230 | ||
| 231 | #### `stream.remotePort` | |
| 232 | ||
| 233 | Remote port. Setted when you `connect()` the stream. | |
| 234 | ||
| 235 | #### `stream.userData` | |
| 236 | ||
| 237 | Optional custom userData. Default is `null`. | |
| 238 | ||
| 239 | #### `stream.connected` | |
| 240 | ||
| 241 | Indicates if the stream is connected to a socket. It becomes `false` if the stream is closed. | |
| 242 | ||
| 243 | #### `stream.mtu` | |
| 244 | ||
| 245 | Indicates the maximum size of each packet. | |
| 246 | ||
| 247 | #### `stream.rtt` | |
| 248 | ||
| 249 | #### `stream.cwnd` | |
| 250 | ||
| 251 | #### `stream.inflight` | |
| 252 | ||
| 253 | #### `stream.localHost` | |
| 254 | ||
| 255 | Indicates the connected socket host address. By default `null` if not connected. | |
| 256 | ||
| 257 | #### `stream.localFamily` | |
| 258 | ||
| 259 | Indicates the connected socket family address (`4` o `6`). By default `0` if not connected. | |
| 260 | ||
| 261 | #### `stream.localPort` | |
| 262 | ||
| 263 | Indicates the connected socket port. By default `0` if not connected. | |
| 264 | ||
| 265 | #### `stream.setInteractive(bool)` | |
| 266 | ||
| 267 | #### `stream.connect(socket, remoteId, port, [host], [options])` | |
| 268 | ||
| 269 | Connects the stream using a socket to a: remote stream id, and remote socket port/host. | |
| 270 | ||
| 271 | If no host specified it uses `127.0.0.1` by default. | |
| 272 | ||
| 273 | Available `options`: | |
| 274 | ||
| 275 | ```js | |
| 276 | { | |
| 277 | ack | |
| 278 | } | |
| 279 | ``` | |
| 280 | ||
| 281 | #### `await stream.changeRemote(remoteId, port, [host])` | |
| 282 | ||
| 283 | Change the remote end of the stream. | |
| 284 | ||
| 285 | If no host specified it uses `127.0.0.1` by default. | |
| 286 | ||
| 287 | #### `stream.relayTo(destination)` | |
| 288 | ||
| 289 | Relay stream to another stream. | |
| 290 | ||
| 291 | #### `await stream.send(buffer)` | |
| 292 | ||
| 293 | Send a message to another stream. Returns a promise. | |
| 294 | ||
| 295 | #### `stream.trySend(buffer)` | |
| 296 | ||
| 297 | Send a message to another stream. | |
| 298 | ||
| 299 | #### `const drained = await stream.flush()` | |
| 300 | ||
| 301 | Wait for pending stream writes to have been explicitly acknowledged by the other side of the connection. | |
| 302 | ||
| 303 | #### `stream.on('connect', onconnect)` | |
| 304 | ||
| 305 | Emitted after the stream is connected to a socket. | |
| 306 | ||
| 307 | #### `stream.on('message', onmessage)` | |
| 308 | ||
| 309 | Emitted if the stream receives a message. | |
| 310 | ||
| 311 | #### `stream.on('remote-changed', onremotechanged)` | |
| 312 | ||
| 313 | Emitted when the remote end of the stream changes. | |
| 314 | ||
| 315 | #### `stream.on('mtu-exceeded', onmtuexceeded)` | |
| 316 | ||
| 317 | Emitted only once if you write data that exceeds the MTU. | |
| 318 | ||
| 319 | ## Network interfaces | |
| 320 | ||
| 321 | #### `const interfaces = udx.networkInterfaces()` | |
| 322 | ||
| 323 | Returns an array of network interfaces, for example: | |
| 324 | ||
| 325 | ```js | |
| 326 | ;[ | |
| 327 | { name: 'lo', host: '127.0.0.1', family: 4, internal: true }, | |
| 328 | { name: 'enp4s0', host: '192.168.0.20', family: 4, internal: false }, | |
| 329 | { name: 'lo', host: '::1', family: 6, internal: true }, | |
| 330 | { | |
| 331 | name: 'enp4s0', | |
| 332 | host: 'df08::c8df:bf61:95c1:352b', | |
| 333 | family: 6, | |
| 334 | internal: false | |
| 335 | } | |
| 336 | ] | |
| 337 | ``` | |
| 338 | ||
| 339 | #### `const watcher = udx.watchNetworkInterfaces([onchange])` | |
| 340 | ||
| 341 | Listens to changes in the network interfaces. The `watcher` object is iterable. | |
| 342 | ||
| 343 | #### `watcher.interfaces` | |
| 344 | ||
| 345 | Array of network interfaces. | |
| 346 | ||
| 347 | #### `watcher.watch()` | |
| 348 | ||
| 349 | Starts watching for changes. By default it already does it. This is only useful after you `unwatch()`. | |
| 350 | ||
| 351 | #### `watcher.unwatch()` | |
| 352 | ||
| 353 | Stops watching for changes. | |
| 354 | ||
| 355 | #### `await watcher.destroy()` | |
| 356 | ||
| 357 | Closes the watcher. | |
| 358 | ||
| 359 | #### `watcher.on('change', onchange)` | |
| 360 | ||
| 361 | Emitted after a network interface change. | |
| 362 | ||
| 363 | #### `watcher.on('close', onclose)` | |
| 364 | ||
| 365 | Emitted after the watcher is closed. | |
| 366 | ||
| 367 | ## DNS | |
| 368 | ||
| 369 | #### `const address = await udx.lookup(host, [options])` | |
| 370 | ||
| 371 | It does a DNS lookup for the IP address. Returns `{ host, family }`. | |
| 372 | ||
| 373 | Available `options`: | |
| 374 | ||
| 375 | ```js | |
| 376 | { | |
| 377 | family: 0 // => 0, 4 or 6 | |
| 378 | } | |
| 379 | ``` | |
| 380 | ||
| 381 | ## Dev Setup | |
| 382 | ||
| 383 | To develop UDX locally, you need to create a libudx prebuild. [bare-make](https://github.com/holepunchto/bare-make) is used for this. | |
| 384 | ||
| 385 | Requirements: The Clang C-compiler should be installed. | |
| 386 | ||
| 387 | The other setup steps are: | |
| 388 | ||
| 389 | - `npm install -g bare-runtime bare-make` | |
| 390 | - `npm install` | |
| 391 | - `bare-make generate` | |
| 392 | - `bare-make build` | |
| 393 | - `bare-make install` | |
| 394 | ||
| 395 | When testing changes, rebuild the prebuilds: | |
| 396 | ||
| 397 | - `bare-make generate` | |
| 398 | - `bare-make build` | |
| 399 | - `bare-make install` | |
| 400 | ||
| 401 | ## License | |
| 402 | ||
| 403 | Apache-2.0 |
| … | … | @@ -0,0 +1,1781 @@ |
| 1 | #include <bare.h> | |
| 2 | #include <js.h> | |
| 3 | #include <jstl.h> | |
| 4 | #include <stdlib.h> | |
| 5 | #include <string.h> | |
| 6 | #include <udx.h> | |
| 7 | ||
| 8 | #define UDX_NAPI_INTERACTIVE 0 | |
| 9 | #define UDX_NAPI_NON_INTERACTIVE 1 | |
| 10 | #define UDX_NAPI_FRAMED 2 | |
| 11 | ||
| 12 | namespace { | |
| 13 | // socket | |
| 14 | using cb_socket_send_t = js_function_t<void, js_receiver_t, uint64_t, int>; | |
| 15 | using cb_socket_message_t = js_function_t<js_typedarray_span_t<>, js_receiver_t, int64_t, int, std::string, int>; | |
| 16 | using cb_socket_close_t = js_function_t<void, js_receiver_t>; | |
| 17 | using cb_socket_realloc_message_t = js_function_t<js_typedarray_span_t<>, js_receiver_t>; | |
| 18 | ||
| 19 | // stream | |
| 20 | using cb_stream_end_t = js_function_t<void, js_receiver_t, uint32_t>; | |
| 21 | using cb_stream_data_t = js_function_t<js_typedarray_span_t<>, js_receiver_t, uint32_t>; | |
| 22 | using cb_stream_drain_t = js_function_t<void, js_receiver_t>; | |
| 23 | using cb_stream_ack_t = js_function_t<void, js_receiver_t, uint32_t>; | |
| 24 | using cb_stream_send_t = js_function_t<void, js_receiver_t, int32_t, int32_t>; | |
| 25 | using cb_stream_message_t = js_function_t<js_typedarray_span_t<>, js_receiver_t, uint32_t>; | |
| 26 | using cb_stream_realloc_data_t = js_function_t<js_typedarray_span_t<>, js_receiver_t>; | |
| 27 | using cb_stream_realloc_message_t = js_function_t<js_typedarray_span_t<>, js_receiver_t>; | |
| 28 | using cb_stream_close_t = js_function_t<void, js_receiver_t, std::optional<js_object_t>>; | |
| 29 | using cb_stream_firewall_t = js_function_t<uint32_t, js_receiver_t, js_receiver_t, uint32_t, std::string, uint32_t>; | |
| 30 | using cb_stream_remote_changed_t = js_function_t<void, js_receiver_t>; | |
| 31 | ||
| 32 | // udx | |
| 33 | using cb_udx_lookup_t = js_function_t<void, js_receiver_t, std::optional<js_object_t>, std::optional<std::string>, int32_t>; | |
| 34 | ||
| 35 | // interface | |
| 36 | using cb_interface_event_t = js_function_t<void, js_receiver_t>; | |
| 37 | using cb_interface_close_t = js_function_t<void, js_receiver_t>; | |
| 38 | }; // namespace | |
| 39 | ||
| 40 | struct udx_napi_t { | |
| 41 | udx_t udx; | |
| 42 | ||
| 43 | uint8_t *read_buf; | |
| 44 | size_t read_buf_free; | |
| 45 | ||
| 46 | js_deferred_teardown_t *teardown; | |
| 47 | bool exiting; | |
| 48 | }; | |
| 49 | ||
| 50 | struct udx_napi_socket_t { | |
| 51 | udx_socket_t socket; | |
| 52 | udx_napi_t *udx; | |
| 53 | ||
| 54 | js_env_t *env; | |
| 55 | ||
| 56 | js_persistent_t<js_receiver_t> ctx; | |
| 57 | ||
| 58 | js_persistent_t<cb_socket_send_t> on_send; | |
| 59 | js_persistent_t<cb_socket_message_t> on_message; | |
| 60 | js_persistent_t<cb_socket_close_t> on_close; | |
| 61 | js_persistent_t<cb_socket_realloc_message_t> realloc_message; // TODO: deprecate | |
| 62 | }; | |
| 63 | ||
| 64 | struct udx_napi_stream_t { | |
| 65 | udx_stream_t stream; | |
| 66 | udx_napi_t *udx; | |
| 67 | ||
| 68 | int mode; | |
| 69 | ||
| 70 | uint8_t *read_buf; | |
| 71 | uint8_t *read_buf_head; | |
| 72 | size_t read_buf_free; | |
| 73 | ||
| 74 | ssize_t frame_len; | |
| 75 | ||
| 76 | js_env_t *env; | |
| 77 | js_persistent_t<js_receiver_t> ctx; | |
| 78 | js_persistent_t<cb_stream_data_t> on_data; | |
| 79 | js_persistent_t<cb_stream_end_t> on_end; | |
| 80 | js_persistent_t<cb_stream_drain_t> on_drain; | |
| 81 | js_persistent_t<cb_stream_ack_t> on_ack; | |
| 82 | js_persistent_t<cb_stream_send_t> on_send; | |
| 83 | js_persistent_t<cb_stream_message_t> on_message; | |
| 84 | js_persistent_t<cb_stream_close_t> on_close; | |
| 85 | js_persistent_t<cb_stream_firewall_t> on_firewall; | |
| 86 | js_persistent_t<cb_stream_remote_changed_t> on_remote_changed; | |
| 87 | js_persistent_t<cb_stream_realloc_data_t> realloc_data; | |
| 88 | js_persistent_t<cb_stream_realloc_message_t> realloc_message; // TODO: deprecate | |
| 89 | }; | |
| 90 | ||
| 91 | struct udx_napi_lookup_t { | |
| 92 | udx_lookup_t handle; | |
| 93 | udx_napi_t *udx; | |
| 94 | ||
| 95 | js_env_t *env; | |
| 96 | js_persistent_t<js_receiver_t> ctx; | |
| 97 | js_persistent_t<cb_udx_lookup_t> on_lookup; | |
| 98 | }; | |
| 99 | ||
| 100 | struct udx_napi_interface_event_t { | |
| 101 | udx_interface_event_t handle; | |
| 102 | udx_napi_t *udx; | |
| 103 | ||
| 104 | js_env_t *env; | |
| 105 | js_persistent_t<js_receiver_t> ctx; | |
| 106 | js_persistent_t<cb_interface_event_t> on_event; | |
| 107 | js_persistent_t<cb_interface_close_t> on_close; | |
| 108 | }; | |
| 109 | ||
| 110 | inline static void | |
| 111 | parse_address (const struct sockaddr *name, char *ip, size_t size, int *port, int *family) { | |
| 112 | if (name->sa_family == AF_INET) { | |
| 113 | auto name_v4 = reinterpret_cast<const struct sockaddr_in *>(name); | |
| 114 | ||
| 115 | if (port) *port = ntohs(name_v4->sin_port); | |
| 116 | if (family) *family = 4; | |
| 117 | ||
| 118 | uv_ip4_name(name_v4, ip, size); | |
| 119 | } else if (name->sa_family == AF_INET6) { | |
| 120 | auto name_v6 = reinterpret_cast<const struct sockaddr_in6 *>(name); | |
| 121 | ||
| 122 | if (port) *port = ntohs(name_v6->sin6_port); | |
| 123 | if (family) *family = 6; | |
| 124 | ||
| 125 | uv_ip6_name(name_v6, ip, size); | |
| 126 | } | |
| 127 | } | |
| 128 | ||
| 129 | inline static int | |
| 130 | load_address (js_env_t *env, char *addr, std::string str) { | |
| 131 | memset(addr, 0, INET6_ADDRSTRLEN); | |
| 132 | ||
| 133 | const char *val = str.c_str(); | |
| 134 | ||
| 135 | size_t len = INET6_ADDRSTRLEN; | |
| 136 | if (str.length() < INET6_ADDRSTRLEN) len = str.length(); | |
| 137 | ||
| 138 | strncpy(addr, val, len); | |
| 139 | ||
| 140 | return len; | |
| 141 | } | |
| 142 | ||
| 143 | static void | |
| 144 | on_udx_send (udx_socket_send_t *req, int status) { | |
| 145 | int err; | |
| 146 | ||
| 147 | auto *n = reinterpret_cast<udx_napi_socket_t *>(req->socket); | |
| 148 | ||
| 149 | if (n->udx->exiting) return; | |
| 150 | ||
| 151 | js_env_t *env = n->env; | |
| 152 | ||
| 153 | js_handle_scope_t *scope; | |
| 154 | err = js_open_handle_scope(env, &scope); | |
| 155 | assert(err == 0); | |
| 156 | ||
| 157 | js_receiver_t ctx; | |
| 158 | err = js_get_reference_value(env, n->ctx, ctx); | |
| 159 | assert(err == 0); | |
| 160 | ||
| 161 | cb_socket_send_t callback; | |
| 162 | err = js_get_reference_value(env, n->on_send, callback); | |
| 163 | assert(err == 0); | |
| 164 | ||
| 165 | auto id = reinterpret_cast<uintptr_t>(req->data); | |
| 166 | ||
| 167 | err = js_call_function_with_checkpoint(env, callback, ctx, uint64_t(id), status); | |
| 168 | assert(err == 0); | |
| 169 | ||
| 170 | err = js_close_handle_scope(env, scope); | |
| 171 | assert(err == 0); | |
| 172 | } | |
| 173 | ||
| 174 | static void | |
| 175 | on_udx_message (udx_socket_t *self, ssize_t read_len, const uv_buf_t *buf, const struct sockaddr *from) { | |
| 176 | auto *n = reinterpret_cast<udx_napi_socket_t *>(self); | |
| 177 | ||
| 178 | if (n->udx->exiting) return; | |
| 179 | ||
| 180 | int err; | |
| 181 | int port = 0; | |
| 182 | char ip[INET6_ADDRSTRLEN]; | |
| 183 | int family = 0; | |
| 184 | parse_address(from, ip, INET6_ADDRSTRLEN, &port, &family); | |
| 185 | ||
| 186 | if (buf->len > n->udx->read_buf_free) return; | |
| 187 | ||
| 188 | memcpy(n->udx->read_buf, buf->base, buf->len); | |
| 189 | ||
| 190 | js_env_t *env = n->env; | |
| 191 | ||
| 192 | js_handle_scope_t *scope; | |
| 193 | err = js_open_handle_scope(env, &scope); | |
| 194 | assert(err == 0); | |
| 195 | ||
| 196 | js_receiver_t ctx; | |
| 197 | err = js_get_reference_value(env, n->ctx, ctx); | |
| 198 | assert(err == 0); | |
| 199 | ||
| 200 | cb_socket_message_t callback; | |
| 201 | err = js_get_reference_value(env, n->on_message, callback); | |
| 202 | assert(err == 0); | |
| 203 | ||
| 204 | std::string ip_str; | |
| 205 | ip_str = ip; | |
| 206 | ||
| 207 | js_typedarray_span_t<> res; | |
| 208 | err = js_call_function_with_checkpoint<js_type_options_t{}, js_typedarray_span_t<>, js_receiver_t, int64_t, int, std::string, int>( | |
| 209 | env, | |
| 210 | callback, | |
| 211 | ctx, | |
| 212 | int64_t(read_len), | |
| 213 | port, | |
| 214 | ip_str, | |
| 215 | family, | |
| 216 | res | |
| 217 | ); | |
| 218 | ||
| 219 | if (err == 0) { | |
| 220 | n->udx->read_buf = res.data(); | |
| 221 | n->udx->read_buf_free = res.size(); | |
| 222 | } else { | |
| 223 | // avoid reentry | |
| 224 | if (!(n->udx->exiting)) { | |
| 225 | int err; | |
| 226 | js_env_t *env = n->env; | |
| 227 | ||
| 228 | js_handle_scope_t *scope; | |
| 229 | err = js_open_handle_scope(env, &scope); | |
| 230 | assert(err == 0); | |
| 231 | ||
| 232 | js_receiver_t ctx; | |
| 233 | err = js_get_reference_value(env, n->ctx, ctx); | |
| 234 | assert(err == 0); | |
| 235 | ||
| 236 | cb_socket_realloc_message_t callback; | |
| 237 | err = js_get_reference_value(env, n->realloc_message, callback); | |
| 238 | assert(err == 0); | |
| 239 | ||
| 240 | err = js_call_function_with_checkpoint<js_type_options_t{}, js_typedarray_span_t<>, js_receiver_t>(env, callback, ctx, res); | |
| 241 | assert(err == 0); | |
| 242 | ||
| 243 | n->udx->read_buf = res.data(); | |
| 244 | n->udx->read_buf_free = res.size(); | |
| 245 | ||
| 246 | err = js_close_handle_scope(env, scope); | |
| 247 | assert(err == 0); | |
| 248 | } | |
| 249 | } | |
| 250 | ||
| 251 | err = js_close_handle_scope(env, scope); | |
| 252 | assert(err == 0); | |
| 253 | } | |
| 254 | ||
| 255 | static void | |
| 256 | on_udx_close (udx_socket_t *self) { | |
| 257 | auto n = reinterpret_cast<udx_napi_socket_t *>(self); | |
| 258 | int err; | |
| 259 | js_env_t *env = n->env; | |
| 260 | ||
| 261 | if (!(n->udx->exiting)) { | |
| 262 | js_handle_scope_t *scope; | |
| 263 | err = js_open_handle_scope(env, &scope); | |
| 264 | assert(err == 0); | |
| 265 | ||
| 266 | js_receiver_t ctx; | |
| 267 | err = js_get_reference_value(env, n->ctx, ctx); | |
| 268 | assert(err == 0); | |
| 269 | ||
| 270 | cb_socket_close_t callback; | |
| 271 | err = js_get_reference_value(env, n->on_close, callback); | |
| 272 | assert(err == 0); | |
| 273 | ||
| 274 | err = js_call_function_with_checkpoint(env, callback, ctx); | |
| 275 | assert(err == 0); | |
| 276 | ||
| 277 | err = js_close_handle_scope(env, scope); | |
| 278 | assert(err == 0); | |
| 279 | } | |
| 280 | ||
| 281 | n->on_send.reset(); | |
| 282 | n->on_message.reset(); | |
| 283 | n->on_close.reset(); | |
| 284 | n->realloc_message.reset(); | |
| 285 | n->ctx.reset(); | |
| 286 | } | |
| 287 | ||
| 288 | static void | |
| 289 | on_udx_teardown (js_deferred_teardown_t *handle, void *data) { | |
| 290 | auto self = reinterpret_cast<udx_napi_t *>(data); | |
| 291 | ||
| 292 | self->exiting = true; | |
| 293 | ||
| 294 | udx_teardown(&self->udx); | |
| 295 | } | |
| 296 | ||
| 297 | static void | |
| 298 | ensure_teardown (js_env_t *env, udx_napi_t *udx) { | |
| 299 | if (udx->teardown) return; | |
| 300 | ||
| 301 | int err = js_add_deferred_teardown_callback( | |
| 302 | env, | |
| 303 | on_udx_teardown, | |
| 304 | reinterpret_cast<void *>(udx), | |
| 305 | &udx->teardown | |
| 306 | ); | |
| 307 | ||
| 308 | if (err != 0) abort(); | |
| 309 | } | |
| 310 | ||
| 311 | static void | |
| 312 | on_udx_stream_end (udx_stream_t *stream) { | |
| 313 | int err; | |
| 314 | auto n = reinterpret_cast<udx_napi_stream_t *>(stream); | |
| 315 | if (n->udx->exiting) return; | |
| 316 | ||
| 317 | size_t read = n->read_buf_head - n->read_buf; | |
| 318 | ||
| 319 | js_env_t *env = n->env; | |
| 320 | ||
| 321 | js_handle_scope_t *scope; | |
| 322 | err = js_open_handle_scope(env, &scope); | |
| 323 | assert(err == 0); | |
| 324 | ||
| 325 | js_receiver_t ctx; | |
| 326 | err = js_get_reference_value(env, n->ctx, ctx); | |
| 327 | assert(err == 0); | |
| 328 | ||
| 329 | cb_stream_end_t callback; | |
| 330 | err = js_get_reference_value(env, n->on_end, callback); | |
| 331 | assert(err == 0); | |
| 332 | ||
| 333 | err = js_call_function_with_checkpoint(env, callback, ctx, uint32_t(read)); | |
| 334 | assert(err == 0); | |
| 335 | ||
| 336 | err = js_close_handle_scope(env, scope); | |
| 337 | assert(err == 0); | |
| 338 | } | |
| 339 | ||
| 340 | static void | |
| 341 | on_udx_stream_read (udx_stream_t *stream, ssize_t read_len, const uv_buf_t *buf) { | |
| 342 | if (read_len == UV_EOF) return on_udx_stream_end(stream); | |
| 343 | ||
| 344 | auto n = reinterpret_cast<udx_napi_stream_t *>(stream); | |
| 345 | if (n->udx->exiting) return; | |
| 346 | ||
| 347 | // ignore the message if it doesn't fit in the read buffer | |
| 348 | if (buf->len > n->read_buf_free) return; | |
| 349 | ||
| 350 | if (n->mode == UDX_NAPI_FRAMED && n->frame_len == -1) { | |
| 351 | if (buf->len < 3) { | |
| 352 | n->mode = UDX_NAPI_INTERACTIVE; | |
| 353 | } else { | |
| 354 | auto b = reinterpret_cast<uint8_t *>(buf->base); | |
| 355 | n->frame_len = 3 + (b[0] | (b[1] << 8) | (b[2] << 16)); | |
| 356 | } | |
| 357 | } | |
| 358 | ||
| 359 | memcpy(n->read_buf_head, buf->base, buf->len); | |
| 360 | ||
| 361 | n->read_buf_head += buf->len; | |
| 362 | n->read_buf_free -= buf->len; | |
| 363 | ||
| 364 | if (n->mode == UDX_NAPI_NON_INTERACTIVE && n->read_buf_free >= 2 * stream->mtu) { | |
| 365 | return; | |
| 366 | } | |
| 367 | ||
| 368 | ssize_t read = n->read_buf_head - n->read_buf; | |
| 369 | ||
| 370 | if (n->mode == UDX_NAPI_FRAMED) { | |
| 371 | if (n->frame_len < read) { | |
| 372 | n->mode = UDX_NAPI_INTERACTIVE; | |
| 373 | } else if (n->frame_len == read) { | |
| 374 | n->frame_len = -1; | |
| 375 | } else if (n->read_buf_free < 2 * stream->mtu) { | |
| 376 | n->frame_len -= read; | |
| 377 | } else { | |
| 378 | return; // wait for more data | |
| 379 | } | |
| 380 | } | |
| 381 | ||
| 382 | js_env_t *env = n->env; | |
| 383 | ||
| 384 | js_handle_scope_t *scope; | |
| 385 | int err = js_open_handle_scope(env, &scope); | |
| 386 | assert(err == 0); | |
| 387 | ||
| 388 | js_receiver_t ctx; | |
| 389 | err = js_get_reference_value(env, n->ctx, ctx); | |
| 390 | assert(err == 0); | |
| 391 | ||
| 392 | cb_stream_data_t callback; | |
| 393 | err = js_get_reference_value(env, n->on_data, callback); | |
| 394 | assert(err == 0); | |
| 395 | ||
| 396 | js_typedarray_span_t<> res; | |
| 397 | err = js_call_function_with_checkpoint<js_type_options_t{}, js_typedarray_span_t<>, js_receiver_t, uint32_t>(env, callback, ctx, uint32_t(read), res); | |
| 398 | ||
| 399 | if (err == 0) { | |
| 400 | n->read_buf = res.data(); | |
| 401 | n->read_buf_free = res.size(); | |
| 402 | n->read_buf_head = n->read_buf; | |
| 403 | } else { | |
| 404 | // avoid re-entry | |
| 405 | if (!(n->udx->exiting)) { | |
| 406 | js_handle_scope_t *inner_scope; | |
| 407 | err = js_open_handle_scope(env, &inner_scope); | |
| 408 | assert(err == 0); | |
| 409 | ||
| 410 | js_receiver_t ctx; | |
| 411 | err = js_get_reference_value(env, n->ctx, ctx); | |
| 412 | assert(err == 0); | |
| 413 | ||
| 414 | cb_stream_realloc_data_t callback; | |
| 415 | err = js_get_reference_value(env, n->realloc_data, callback); | |
| 416 | assert(err == 0); | |
| 417 | ||
| 418 | err = js_call_function_with_checkpoint<js_type_options_t{}, js_typedarray_span_t<>, js_receiver_t>(env, callback, ctx, res); | |
| 419 | assert(err == 0); | |
| 420 | ||
| 421 | n->read_buf = res.data(); | |
| 422 | n->read_buf_free = res.size(); | |
| 423 | n->read_buf_head = n->read_buf; | |
| 424 | ||
| 425 | err = js_close_handle_scope(env, inner_scope); | |
| 426 | assert(err == 0); | |
| 427 | } | |
| 428 | } | |
| 429 | ||
| 430 | err = js_close_handle_scope(env, scope); | |
| 431 | assert(err == 0); | |
| 432 | } | |
| 433 | ||
| 434 | static void | |
| 435 | on_udx_stream_drain (udx_stream_t *stream) { | |
| 436 | auto n = reinterpret_cast<udx_napi_stream_t *>(stream); | |
| 437 | if (n->udx->exiting) return; | |
| 438 | ||
| 439 | js_env_t *env = n->env; | |
| 440 | ||
| 441 | js_handle_scope_t *scope; | |
| 442 | int err = js_open_handle_scope(env, &scope); | |
| 443 | assert(err == 0); | |
| 444 | ||
| 445 | js_receiver_t ctx; | |
| 446 | err = js_get_reference_value(env, n->ctx, ctx); | |
| 447 | assert(err == 0); | |
| 448 | ||
| 449 | cb_stream_drain_t callback; | |
| 450 | err = js_get_reference_value(env, n->on_drain, callback); | |
| 451 | assert(err == 0); | |
| 452 | ||
| 453 | err = js_call_function_with_checkpoint(env, callback, ctx); | |
| 454 | assert(err == 0); | |
| 455 | ||
| 456 | err = js_close_handle_scope(env, scope); | |
| 457 | assert(err == 0); | |
| 458 | } | |
| 459 | ||
| 460 | static void | |
| 461 | on_udx_stream_ack (udx_stream_write_t *req, int status, int unordered) { | |
| 462 | int err = 0; | |
| 463 | ||
| 464 | auto n = reinterpret_cast<udx_napi_stream_t *>(req->stream); | |
| 465 | if (n->udx->exiting) return; | |
| 466 | ||
| 467 | js_env_t *env = n->env; | |
| 468 | ||
| 469 | js_handle_scope_t *scope; | |
| 470 | err = js_open_handle_scope(env, &scope); | |
| 471 | assert(err == 0); | |
| 472 | ||
| 473 | js_receiver_t ctx; | |
| 474 | err = js_get_reference_value(env, n->ctx, ctx); | |
| 475 | assert(err == 0); | |
| 476 | ||
| 477 | cb_stream_ack_t callback; | |
| 478 | err = js_get_reference_value(env, n->on_ack, callback); | |
| 479 | assert(err == 0); | |
| 480 | ||
| 481 | auto offset = reinterpret_cast<uintptr_t>(req->data); | |
| 482 | ||
| 483 | err = js_call_function_with_checkpoint(env, callback, ctx, uint32_t(offset)); | |
| 484 | assert(err == 0); | |
| 485 | ||
| 486 | err = js_close_handle_scope(env, scope); | |
| 487 | assert(err == 0); | |
| 488 | } | |
| 489 | ||
| 490 | static void | |
| 491 | on_udx_stream_send (udx_stream_send_t *req, int status) { | |
| 492 | int err; | |
| 493 | ||
| 494 | auto n = reinterpret_cast<udx_napi_stream_t *>(req->stream); | |
| 495 | ||
| 496 | if (n->udx->exiting) return; | |
| 497 | ||
| 498 | js_env_t *env = n->env; | |
| 499 | ||
| 500 | js_handle_scope_t *scope; | |
| 501 | err = js_open_handle_scope(env, &scope); | |
| 502 | assert(err == 0); | |
| 503 | ||
| 504 | js_receiver_t ctx; | |
| 505 | err = js_get_reference_value(env, n->ctx, ctx); | |
| 506 | assert(err == 0); | |
| 507 | ||
| 508 | cb_stream_send_t callback; | |
| 509 | err = js_get_reference_value(env, n->on_send, callback); | |
| 510 | assert(err == 0); | |
| 511 | ||
| 512 | auto id = reinterpret_cast<uintptr_t>(req->data); | |
| 513 | ||
| 514 | err = js_call_function_with_checkpoint(env, callback, ctx, int32_t(id), status); | |
| 515 | assert(err == 0); | |
| 516 | ||
| 517 | err = js_close_handle_scope(env, scope); | |
| 518 | assert(err == 0); | |
| 519 | } | |
| 520 | ||
| 521 | static void | |
| 522 | on_udx_stream_recv (udx_stream_t *stream, ssize_t read_len, const uv_buf_t *buf) { | |
| 523 | int err; | |
| 524 | ||
| 525 | auto n = reinterpret_cast<udx_napi_stream_t *>(stream); | |
| 526 | ||
| 527 | if (n->udx->exiting) return; | |
| 528 | ||
| 529 | if (buf->len > n->udx->read_buf_free) return; | |
| 530 | ||
| 531 | memcpy(n->udx->read_buf, buf->base, buf->len); | |
| 532 | ||
| 533 | js_env_t *env = n->env; | |
| 534 | ||
| 535 | js_handle_scope_t *scope; | |
| 536 | err = js_open_handle_scope(env, &scope); | |
| 537 | assert(err == 0); | |
| 538 | ||
| 539 | js_receiver_t ctx; | |
| 540 | err = js_get_reference_value(env, n->ctx, ctx); | |
| 541 | assert(err == 0); | |
| 542 | ||
| 543 | cb_stream_message_t callback; | |
| 544 | err = js_get_reference_value(env, n->on_message, callback); | |
| 545 | assert(err == 0); | |
| 546 | ||
| 547 | js_typedarray_span_t<> res; | |
| 548 | err = js_call_function_with_checkpoint<js_type_options_t{}, js_typedarray_span_t<>, js_receiver_t, uint32_t>(env, callback, ctx, uint32_t(read_len), res); | |
| 549 | ||
| 550 | if (err == 0) { | |
| 551 | n->udx->read_buf = res.data(); | |
| 552 | n->udx->read_buf_free = res.size(); | |
| 553 | } else { | |
| 554 | // avoid re-entry | |
| 555 | if (!(n->udx->exiting)) { | |
| 556 | js_handle_scope_t *inner_scope; | |
| 557 | err = js_open_handle_scope(env, &inner_scope); | |
| 558 | assert(err == 0); | |
| 559 | ||
| 560 | js_receiver_t ctx; | |
| 561 | err = js_get_reference_value(env, n->ctx, ctx); | |
| 562 | assert(err == 0); | |
| 563 | ||
| 564 | cb_stream_realloc_message_t callback; | |
| 565 | err = js_get_reference_value(env, n->realloc_message, callback); | |
| 566 | assert(err == 0); | |
| 567 | ||
| 568 | err = js_call_function_with_checkpoint<js_type_options_t{}, js_typedarray_span_t<>, js_receiver_t>(env, callback, ctx, res); | |
| 569 | assert(err == 0); | |
| 570 | ||
| 571 | n->udx->read_buf = res.data(); | |
| 572 | n->udx->read_buf_free = res.size(); | |
| 573 | ||
| 574 | err = js_close_handle_scope(env, inner_scope); | |
| 575 | assert(err == 0); | |
| 576 | } | |
| 577 | } | |
| 578 | ||
| 579 | err = js_close_handle_scope(env, scope); | |
| 580 | assert(err == 0); | |
| 581 | } | |
| 582 | ||
| 583 | static void | |
| 584 | on_udx_stream_finalize (udx_stream_t *stream) { | |
| 585 | auto n = reinterpret_cast<udx_napi_stream_t *>(stream); | |
| 586 | ||
| 587 | n->on_data.reset(); | |
| 588 | n->on_end.reset(); | |
| 589 | n->on_drain.reset(); | |
| 590 | n->on_ack.reset(); | |
| 591 | n->on_send.reset(); | |
| 592 | n->on_message.reset(); | |
| 593 | n->on_close.reset(); | |
| 594 | n->on_firewall.reset(); | |
| 595 | n->on_remote_changed.reset(); | |
| 596 | n->realloc_data.reset(); | |
| 597 | n->realloc_message.reset(); | |
| 598 | n->ctx.reset(); | |
| 599 | } | |
| 600 | ||
| 601 | static void | |
| 602 | on_udx_stream_close (udx_stream_t *stream, int status) { | |
| 603 | int err; | |
| 604 | ||
| 605 | auto n = reinterpret_cast<udx_napi_stream_t *>(stream); | |
| 606 | ||
| 607 | if (n->udx->exiting) return; | |
| 608 | ||
| 609 | js_env_t *env = n->env; | |
| 610 | ||
| 611 | js_handle_scope_t *scope; | |
| 612 | err = js_open_handle_scope(env, &scope); | |
| 613 | assert(err == 0); | |
| 614 | ||
| 615 | js_receiver_t ctx; | |
| 616 | err = js_get_reference_value(env, n->ctx, ctx); | |
| 617 | assert(err == 0); | |
| 618 | ||
| 619 | cb_stream_close_t callback; | |
| 620 | err = js_get_reference_value(env, n->on_close, callback); | |
| 621 | assert(err == 0); | |
| 622 | ||
| 623 | std::optional<js_object_t> res; | |
| 624 | ||
| 625 | if (status < 0) { | |
| 626 | js_string_t code; | |
| 627 | err = js_create_string(env, uv_err_name(status), code); | |
| 628 | assert(err == 0); | |
| 629 | ||
| 630 | res.emplace(); // init | |
| 631 | ||
| 632 | err = js_create_error(env, code, uv_strerror(status), *res); | |
| 633 | assert(err == 0); | |
| 634 | } | |
| 635 | ||
| 636 | err = js_call_function_with_checkpoint(env, callback, ctx, res); | |
| 637 | assert(err == 0); | |
| 638 | ||
| 639 | err = js_close_handle_scope(env, scope); | |
| 640 | assert(err == 0); | |
| 641 | } | |
| 642 | ||
| 643 | static int | |
| 644 | on_udx_stream_firewall (udx_stream_t *stream, udx_socket_t *socket, const struct sockaddr *from) { | |
| 645 | int err; | |
| 646 | auto *n = reinterpret_cast<udx_napi_stream_t *>(stream); | |
| 647 | auto *s = reinterpret_cast<udx_napi_socket_t *>(socket); | |
| 648 | ||
| 649 | uint32_t fw = 1; // assume error means firewall it, whilst reporting the uncaught | |
| 650 | if (n->udx->exiting) return fw; | |
| 651 | ||
| 652 | int port = 0; | |
| 653 | char ip[INET6_ADDRSTRLEN]; | |
| 654 | int family = 0; | |
| 655 | parse_address(from, ip, INET6_ADDRSTRLEN, &port, &family); | |
| 656 | ||
| 657 | js_env_t *env = n->env; | |
| 658 | ||
| 659 | js_handle_scope_t *scope; | |
| 660 | err = js_open_handle_scope(env, &scope); | |
| 661 | assert(err == 0); | |
| 662 | ||
| 663 | js_receiver_t ctx; | |
| 664 | err = js_get_reference_value(env, n->ctx, ctx); | |
| 665 | assert(err == 0); | |
| 666 | ||
| 667 | cb_stream_firewall_t callback; | |
| 668 | err = js_get_reference_value(env, n->on_firewall, callback); | |
| 669 | assert(err == 0); | |
| 670 | ||
| 671 | js_receiver_t socket_ctx; | |
| 672 | err = js_get_reference_value(env, s->ctx, socket_ctx); | |
| 673 | assert(err == 0); | |
| 674 | ||
| 675 | err = js_call_function_with_checkpoint<js_type_options_t{}, uint32_t, js_receiver_t, js_receiver_t, uint32_t, std::string, uint32_t>( | |
| 676 | env, | |
| 677 | callback, | |
| 678 | ctx, | |
| 679 | socket_ctx, | |
| 680 | uint32_t(port), | |
| 681 | std::string(ip), | |
| 682 | uint32_t(family), | |
| 683 | fw | |
| 684 | ); | |
| 685 | assert(err == 0); | |
| 686 | ||
| 687 | err = js_close_handle_scope(env, scope); | |
| 688 | assert(err == 0); | |
| 689 | ||
| 690 | return fw; | |
| 691 | } | |
| 692 | ||
| 693 | static void | |
| 694 | on_udx_stream_remote_changed (udx_stream_t *stream) { | |
| 695 | int err; | |
| 696 | ||
| 697 | auto n = reinterpret_cast<udx_napi_stream_t *>(stream); | |
| 698 | ||
| 699 | if (n->udx->exiting) return; | |
| 700 | ||
| 701 | js_env_t *env = n->env; | |
| 702 | ||
| 703 | js_handle_scope_t *scope; | |
| 704 | err = js_open_handle_scope(env, &scope); | |
| 705 | assert(err == 0); | |
| 706 | ||
| 707 | js_receiver_t ctx; | |
| 708 | err = js_get_reference_value(env, n->ctx, ctx); | |
| 709 | assert(err == 0); | |
| 710 | ||
| 711 | cb_stream_remote_changed_t callback; | |
| 712 | err = js_get_reference_value(env, n->on_remote_changed, callback); | |
| 713 | assert(err == 0); | |
| 714 | ||
| 715 | err = js_call_function_with_checkpoint(env, callback, ctx); | |
| 716 | assert(err == 0); | |
| 717 | ||
| 718 | err = js_close_handle_scope(env, scope); | |
| 719 | assert(err == 0); | |
| 720 | } | |
| 721 | ||
| 722 | static void | |
| 723 | on_udx_lookup (udx_lookup_t *lookup, int status, const struct sockaddr *addr, int addr_len) { | |
| 724 | int err; | |
| 725 | ||
| 726 | auto n = reinterpret_cast<udx_napi_lookup_t *>(lookup); | |
| 727 | if (n->udx->exiting) return; | |
| 728 | ||
| 729 | js_env_t *env = n->env; | |
| 730 | ||
| 731 | char ip[INET6_ADDRSTRLEN] = ""; | |
| 732 | int family = 0; | |
| 733 | ||
| 734 | js_handle_scope_t *scope; | |
| 735 | err = js_open_handle_scope(env, &scope); | |
| 736 | assert(err == 0); | |
| 737 | ||
| 738 | js_receiver_t ctx; | |
| 739 | err = js_get_reference_value(env, n->ctx, ctx); | |
| 740 | assert(err == 0); | |
| 741 | ||
| 742 | cb_udx_lookup_t callback; | |
| 743 | err = js_get_reference_value(env, n->on_lookup, callback); | |
| 744 | assert(err == 0); | |
| 745 | ||
| 746 | std::optional<js_object_t> error; | |
| 747 | std::optional<std::string> ip_str; | |
| 748 | ||
| 749 | if (status >= 0) { | |
| 750 | parse_address(addr, ip, addr_len, nullptr, &family); | |
| 751 | ||
| 752 | ip_str = ip; | |
| 753 | } else { | |
| 754 | js_string_t code; | |
| 755 | err = js_create_string(env, uv_err_name(status), code); | |
| 756 | assert(err == 0); | |
| 757 | ||
| 758 | error.emplace(); // init | |
| 759 | ||
| 760 | err = js_create_error(env, code, uv_strerror(status), *error); | |
| 761 | assert(err == 0); | |
| 762 | } | |
| 763 | ||
| 764 | err = js_call_function_with_checkpoint(env, callback, ctx, error, ip_str, family); | |
| 765 | assert(err == 0); | |
| 766 | ||
| 767 | err = js_close_handle_scope(env, scope); | |
| 768 | assert(err == 0); | |
| 769 | ||
| 770 | n->on_lookup.reset(); | |
| 771 | n->ctx.reset(); | |
| 772 | } | |
| 773 | ||
| 774 | static void | |
| 775 | on_udx_interface_event (udx_interface_event_t *handle, int status) { | |
| 776 | auto e = reinterpret_cast<udx_napi_interface_event_t *>(handle); | |
| 777 | if (e->udx->exiting) return; | |
| 778 | ||
| 779 | js_env_t *env = e->env; | |
| 780 | ||
| 781 | js_handle_scope_t *scope; | |
| 782 | int err = js_open_handle_scope(env, &scope); | |
| 783 | assert(err == 0); | |
| 784 | ||
| 785 | js_receiver_t ctx; | |
| 786 | err = js_get_reference_value(env, e->ctx, ctx); | |
| 787 | assert(err == 0); | |
| 788 | ||
| 789 | cb_interface_event_t callback; | |
| 790 | err = js_get_reference_value(env, e->on_event, callback); | |
| 791 | assert(err == 0); | |
| 792 | ||
| 793 | err = js_call_function_with_checkpoint(env, callback, ctx); | |
| 794 | assert(err == 0); | |
| 795 | ||
| 796 | err = js_close_handle_scope(env, scope); | |
| 797 | assert(err == 0); | |
| 798 | } | |
| 799 | ||
| 800 | static void | |
| 801 | on_udx_interface_event_close (udx_interface_event_t *handle) { | |
| 802 | auto e = reinterpret_cast<udx_napi_interface_event_t *>(handle); | |
| 803 | ||
| 804 | if (e->udx->exiting) return; | |
| 805 | ||
| 806 | js_env_t *env = e->env; | |
| 807 | ||
| 808 | js_handle_scope_t *scope; | |
| 809 | int err = js_open_handle_scope(env, &scope); | |
| 810 | assert(err == 0); | |
| 811 | ||
| 812 | js_receiver_t ctx; | |
| 813 | err = js_get_reference_value(env, e->ctx, ctx); | |
| 814 | assert(err == 0); | |
| 815 | ||
| 816 | cb_interface_close_t callback; | |
| 817 | err = js_get_reference_value(env, e->on_close, callback); | |
| 818 | assert(err == 0); | |
| 819 | ||
| 820 | err = js_call_function_with_checkpoint(env, callback, ctx); | |
| 821 | assert(err == 0); | |
| 822 | ||
| 823 | err = js_close_handle_scope(env, scope); | |
| 824 | assert(err == 0); | |
| 825 | ||
| 826 | e->on_event.reset(); | |
| 827 | e->on_close.reset(); | |
| 828 | e->ctx.reset(); | |
| 829 | } | |
| 830 | ||
| 831 | static void | |
| 832 | on_udx_idle (udx_t *u) { | |
| 833 | auto self = reinterpret_cast<udx_napi_t *>(u); | |
| 834 | ||
| 835 | if (!self->teardown) return; | |
| 836 | ||
| 837 | int err = js_finish_deferred_teardown_callback(self->teardown); | |
| 838 | ||
| 839 | self->teardown = nullptr; | |
| 840 | ||
| 841 | if (err != 0) abort(); | |
| 842 | } | |
| 843 | ||
| 844 | static inline void | |
| 845 | udx_napi_init ( | |
| 846 | js_env_t *env, | |
| 847 | js_typedarray_span_of_t<udx_napi_t, 1> self, | |
| 848 | js_typedarray_span_t<> read_buf | |
| 849 | ) { | |
| 850 | int err; | |
| 851 | ||
| 852 | *self = {}; | |
| 853 | ||
| 854 | uv_loop_t *loop; | |
| 855 | err = js_get_env_loop(env, &loop); | |
| 856 | assert(err == 0); | |
| 857 | ||
| 858 | err = udx_init(loop, &self->udx, on_udx_idle); | |
| 859 | assert(err == 0); | |
| 860 | ||
| 861 | self->read_buf = read_buf.data(); | |
| 862 | self->read_buf_free = read_buf.size(); | |
| 863 | ||
| 864 | self->exiting = false; | |
| 865 | self->teardown = nullptr; | |
| 866 | } | |
| 867 | ||
| 868 | static inline void | |
| 869 | udx_napi_socket_init ( | |
| 870 | js_env_t *env, | |
| 871 | js_typedarray_span_of_t<udx_napi_t, 1> udx, | |
| 872 | js_typedarray_span_of_t<udx_napi_socket_t, 1> self, | |
| 873 | ||
| 874 | js_receiver_t ctx, | |
| 875 | ||
| 876 | cb_socket_send_t on_send, | |
| 877 | cb_socket_message_t on_message, | |
| 878 | cb_socket_close_t on_close, | |
| 879 | cb_socket_realloc_message_t realloc_message | |
| 880 | ) { | |
| 881 | int err; | |
| 882 | ||
| 883 | *self = {}; | |
| 884 | ||
| 885 | udx_socket_t *socket = &self->socket; | |
| 886 | self->udx = &*udx; | |
| 887 | self->env = env; | |
| 888 | ||
| 889 | err = udx_socket_init(&udx->udx, socket, on_udx_close); | |
| 890 | ||
| 891 | if (err < 0) { | |
| 892 | err = js_throw_error(env, uv_err_name(err), uv_strerror(err)); | |
| 893 | assert(err == 0); | |
| 894 | ||
| 895 | return; | |
| 896 | } | |
| 897 | ||
| 898 | err = js_create_reference(env, ctx, self->ctx); | |
| 899 | assert(err == 0); | |
| 900 | err = js_create_reference(env, on_send, self->on_send); | |
| 901 | assert(err == 0); | |
| 902 | err = js_create_reference(env, on_message, self->on_message); | |
| 903 | assert(err == 0); | |
| 904 | err = js_create_reference(env, on_close, self->on_close); | |
| 905 | assert(err == 0); | |
| 906 | err = js_create_reference(env, realloc_message, self->realloc_message); | |
| 907 | assert(err == 0); | |
| 908 | ||
| 909 | ensure_teardown(env, udx); | |
| 910 | } | |
| 911 | ||
| 912 | static inline int32_t | |
| 913 | udx_napi_socket_bind ( | |
| 914 | js_env_t *env, | |
| 915 | js_typedarray_span_of_t<udx_napi_socket_t, 1> self, | |
| 916 | uint32_t port, | |
| 917 | std::string ip_str, | |
| 918 | uint32_t family, | |
| 919 | uint32_t flags | |
| 920 | ) { | |
| 921 | int err; | |
| 922 | ||
| 923 | auto socket = &self->socket; | |
| 924 | ||
| 925 | char ip[INET6_ADDRSTRLEN]; | |
| 926 | load_address(env, ip, ip_str); | |
| 927 | ||
| 928 | struct sockaddr_storage addr; | |
| 929 | int addr_len; | |
| 930 | ||
| 931 | if (family == 4) { | |
| 932 | addr_len = sizeof(struct sockaddr_in); | |
| 933 | err = uv_ip4_addr(ip, port, (struct sockaddr_in *) &addr); | |
| 934 | } else { | |
| 935 | addr_len = sizeof(struct sockaddr_in6); | |
| 936 | err = uv_ip6_addr(ip, port, (struct sockaddr_in6 *) &addr); | |
| 937 | } | |
| 938 | ||
| 939 | if (err < 0) { | |
| 940 | err = js_throw_error(env, uv_err_name(err), uv_strerror(err)); | |
| 941 | assert(err == 0); | |
| 942 | ||
| 943 | return -1; | |
| 944 | } | |
| 945 | ||
| 946 | err = udx_socket_bind(socket, (struct sockaddr *) &addr, flags); | |
| 947 | if (err < 0) { | |
| 948 | err = js_throw_error(env, uv_err_name(err), uv_strerror(err)); | |
| 949 | assert(err == 0); | |
| 950 | return -1; | |
| 951 | } | |
| 952 | ||
| 953 | // TODO: move the bottom stuff into another function, start, so error handling is easier | |
| 954 | ||
| 955 | struct sockaddr_storage name; | |
| 956 | ||
| 957 | // wont error in practice | |
| 958 | err = udx_socket_getsockname(socket, (struct sockaddr *) &name, &addr_len); | |
| 959 | if (err < 0) { | |
| 960 | err = js_throw_error(env, uv_err_name(err), uv_strerror(err)); | |
| 961 | assert(err == 0); | |
| 962 | return -1; | |
| 963 | } | |
| 964 | ||
| 965 | int local_port; | |
| 966 | ||
| 967 | if (family == 4) { | |
| 968 | local_port = ntohs(((struct sockaddr_in *) &name)->sin_port); | |
| 969 | } else { | |
| 970 | local_port = ntohs(((struct sockaddr_in6 *) &name)->sin6_port); | |
| 971 | } | |
| 972 | ||
| 973 | // wont error in practice | |
| 974 | err = udx_socket_recv_start(socket, on_udx_message); | |
| 975 | if (err < 0) { | |
| 976 | err = js_throw_error(env, uv_err_name(err), uv_strerror(err)); | |
| 977 | assert(err == 0); | |
| 978 | return -1; | |
| 979 | } | |
| 980 | ||
| 981 | return local_port; | |
| 982 | } | |
| 983 | ||
| 984 | static inline void | |
| 985 | udx_napi_socket_set_ttl ( | |
| 986 | js_env_t *env, | |
| 987 | js_typedarray_span_of_t<udx_napi_socket_t, 1> self, | |
| 988 | uint32_t ttl | |
| 989 | ) { | |
| 990 | int err; | |
| 991 | ||
| 992 | err = udx_socket_set_ttl(&self->socket, ttl); | |
| 993 | ||
| 994 | if (err < 0) { | |
| 995 | err = js_throw_error(env, uv_err_name(err), uv_strerror(err)); | |
| 996 | assert(err == 0); | |
| 997 | } | |
| 998 | } | |
| 999 | ||
| 1000 | static inline void | |
| 1001 | udx_napi_socket_set_membership ( | |
| 1002 | js_env_t *env, | |
| 1003 | js_typedarray_span_of_t<udx_napi_socket_t, 1> socket, | |
| 1004 | std::string mcast_addr_str, | |
| 1005 | std::string iface_addr_str, | |
| 1006 | bool join | |
| 1007 | ) { | |
| 1008 | int err; | |
| 1009 | ||
| 1010 | char mcast_addr[INET6_ADDRSTRLEN]; | |
| 1011 | load_address(env, mcast_addr, mcast_addr_str); | |
| 1012 | ||
| 1013 | char iface_addr[INET6_ADDRSTRLEN]; | |
| 1014 | size_t iface_addr_len = load_address(env, iface_addr, iface_addr_str); | |
| 1015 | ||
| 1016 | char *iface_param = iface_addr_len > 0 ? iface_addr : NULL; | |
| 1017 | ||
| 1018 | err = udx_socket_set_membership(&socket->socket, mcast_addr, iface_param, join ? UV_JOIN_GROUP : UV_LEAVE_GROUP); | |
| 1019 | ||
| 1020 | if (err < 0) { | |
| 1021 | err = js_throw_error(env, uv_err_name(err), uv_strerror(err)); | |
| 1022 | assert(err == 0); | |
| 1023 | } | |
| 1024 | } | |
| 1025 | ||
| 1026 | static inline uint32_t | |
| 1027 | udx_napi_socket_get_recv_buffer_size ( | |
| 1028 | js_env_t *env, | |
| 1029 | js_typedarray_span_of_t<udx_napi_socket_t, 1> self | |
| 1030 | ) { | |
| 1031 | int err; | |
| 1032 | ||
| 1033 | int size = 0; | |
| 1034 | ||
| 1035 | err = udx_socket_get_recv_buffer_size(&self->socket, &size); | |
| 1036 | ||
| 1037 | if (err < 0) { | |
| 1038 | err = js_throw_error(env, uv_err_name(err), uv_strerror(err)); | |
| 1039 | assert(err == 0); | |
| 1040 | } | |
| 1041 | ||
| 1042 | return size; | |
| 1043 | } | |
| 1044 | ||
| 1045 | static inline void | |
| 1046 | udx_napi_socket_set_recv_buffer_size ( | |
| 1047 | js_env_t *env, | |
| 1048 | js_typedarray_span_of_t<udx_napi_socket_t, 1> self, | |
| 1049 | uint32_t size | |
| 1050 | ) { | |
| 1051 | int err = udx_socket_set_recv_buffer_size(&self->socket, size); | |
| 1052 | ||
| 1053 | if (err < 0) { | |
| 1054 | err = js_throw_error(env, uv_err_name(err), uv_strerror(err)); | |
| 1055 | assert(err == 0); | |
| 1056 | } | |
| 1057 | } | |
| 1058 | ||
| 1059 | static inline uint32_t | |
| 1060 | udx_napi_socket_get_send_buffer_size ( | |
| 1061 | js_env_t *env, | |
| 1062 | js_typedarray_span_of_t<udx_napi_socket_t, 1> self | |
| 1063 | ) { | |
| 1064 | int size = 0; | |
| 1065 | ||
| 1066 | int err = udx_socket_get_send_buffer_size(&self->socket, &size); | |
| 1067 | ||
| 1068 | if (err < 0) { | |
| 1069 | err = js_throw_error(env, uv_err_name(err), uv_strerror(err)); | |
| 1070 | assert(err == 0); | |
| 1071 | } | |
| 1072 | ||
| 1073 | return size; | |
| 1074 | } | |
| 1075 | ||
| 1076 | static inline uint32_t | |
| 1077 | udx_napi_socket_set_send_buffer_size ( | |
| 1078 | js_env_t *env, | |
| 1079 | js_typedarray_span_of_t<udx_napi_socket_t, 1> self, | |
| 1080 | uint32_t size | |
| 1081 | ) { | |
| 1082 | int err = udx_socket_set_send_buffer_size(&self->socket, size); | |
| 1083 | ||
| 1084 | if (err < 0) { | |
| 1085 | err = js_throw_error(env, uv_err_name(err), uv_strerror(err)); | |
| 1086 | assert(err == 0); | |
| 1087 | } | |
| 1088 | ||
| 1089 | return size; | |
| 1090 | } | |
| 1091 | ||
| 1092 | static inline void | |
| 1093 | udx_napi_socket_send_ttl ( | |
| 1094 | js_env_t *env, | |
| 1095 | js_typedarray_span_of_t<udx_napi_socket_t, 1> self, | |
| 1096 | js_typedarray_span_of_t<udx_socket_send_t, 1> req, | |
| 1097 | uint32_t rid, | |
| 1098 | js_typedarray_span_of_t<char> buf, | |
| 1099 | uint32_t port, | |
| 1100 | std::string ip_str, | |
| 1101 | uint32_t family, | |
| 1102 | uint32_t ttl | |
| 1103 | ) { | |
| 1104 | int err; | |
| 1105 | ||
| 1106 | char ip[INET6_ADDRSTRLEN]; | |
| 1107 | load_address(env, ip, ip_str); | |
| 1108 | ||
| 1109 | req->data = reinterpret_cast<void *>(static_cast<uintptr_t>(rid)); | |
| 1110 | ||
| 1111 | struct sockaddr_storage addr; | |
| 1112 | ||
| 1113 | if (family == 4) { | |
| 1114 | err = uv_ip4_addr(ip, port, (struct sockaddr_in *) &addr); | |
| 1115 | } else { | |
| 1116 | err = uv_ip6_addr(ip, port, (struct sockaddr_in6 *) &addr); | |
| 1117 | } | |
| 1118 | ||
| 1119 | if (err < 0) { | |
| 1120 | err = js_throw_error(env, uv_err_name(err), uv_strerror(err)); | |
| 1121 | assert(err == 0); | |
| 1122 | return; | |
| 1123 | } | |
| 1124 | ||
| 1125 | uv_buf_t b = uv_buf_init(buf.data(), buf.size()); | |
| 1126 | ||
| 1127 | err = udx_socket_send_ttl(req, &self->socket, &b, 1, (const struct sockaddr *) &addr, ttl, on_udx_send); | |
| 1128 | ||
| 1129 | if (err < 0) { | |
| 1130 | err = js_throw_error(env, uv_err_name(err), uv_strerror(err)); | |
| 1131 | assert(err == 0); | |
| 1132 | } | |
| 1133 | } | |
| 1134 | ||
| 1135 | static inline void | |
| 1136 | udx_napi_socket_close ( | |
| 1137 | js_env_t *env, | |
| 1138 | js_typedarray_span_of_t<udx_napi_socket_t, 1> self | |
| 1139 | ) { | |
| 1140 | int err; | |
| 1141 | ||
| 1142 | err = udx_socket_close(&self->socket); | |
| 1143 | ||
| 1144 | if (err < 0) { | |
| 1145 | err = js_throw_error(env, uv_err_name(err), uv_strerror(err)); | |
| 1146 | assert(err == 0); | |
| 1147 | } | |
| 1148 | } | |
| 1149 | ||
| 1150 | static inline void | |
| 1151 | udx_napi_stream_init ( | |
| 1152 | js_env_t *env, | |
| 1153 | js_typedarray_span_of_t<udx_napi_t, 1> udx, | |
| 1154 | js_typedarray_span_of_t<udx_napi_stream_t, 1> self, | |
| 1155 | ||
| 1156 | uint32_t id, | |
| 1157 | uint32_t framed, | |
| 1158 | js_receiver_t ctx, | |
| 1159 | ||
| 1160 | cb_stream_data_t on_data, | |
| 1161 | cb_stream_end_t on_end, | |
| 1162 | cb_stream_drain_t on_drain, | |
| 1163 | cb_stream_ack_t on_ack, | |
| 1164 | cb_stream_send_t on_send, | |
| 1165 | cb_stream_message_t on_message, | |
| 1166 | cb_stream_close_t on_close, | |
| 1167 | cb_stream_firewall_t on_firewall, | |
| 1168 | cb_stream_remote_changed_t on_remote_changed, | |
| 1169 | cb_stream_realloc_data_t realloc_data, | |
| 1170 | cb_stream_realloc_message_t realloc_message | |
| 1171 | ) { | |
| 1172 | int err; | |
| 1173 | ||
| 1174 | *self = {}; | |
| 1175 | ||
| 1176 | self->mode = framed ? UDX_NAPI_FRAMED : UDX_NAPI_INTERACTIVE; | |
| 1177 | ||
| 1178 | self->frame_len = -1; | |
| 1179 | ||
| 1180 | self->read_buf = NULL; | |
| 1181 | self->read_buf_head = NULL; | |
| 1182 | self->read_buf_free = 0; | |
| 1183 | ||
| 1184 | self->udx = udx; | |
| 1185 | self->env = env; | |
| 1186 | ||
| 1187 | auto stream = &self->stream; | |
| 1188 | ||
| 1189 | err = udx_stream_init(&udx->udx, stream, id, on_udx_stream_close, on_udx_stream_finalize); | |
| 1190 | ||
| 1191 | if (err < 0) { | |
| 1192 | err = js_throw_error(env, uv_err_name(err), uv_strerror(err)); | |
| 1193 | assert(err == 0); | |
| 1194 | ||
| 1195 | return; | |
| 1196 | } | |
| 1197 | ||
| 1198 | err = js_create_reference(env, ctx, self->ctx); | |
| 1199 | assert(err == 0); | |
| 1200 | err = js_create_reference(env, on_data, self->on_data); | |
| 1201 | assert(err == 0); | |
| 1202 | err = js_create_reference(env, on_end, self->on_end); | |
| 1203 | assert(err == 0); | |
| 1204 | err = js_create_reference(env, on_drain, self->on_drain); | |
| 1205 | assert(err == 0); | |
| 1206 | err = js_create_reference(env, on_ack, self->on_ack); | |
| 1207 | assert(err == 0); | |
| 1208 | err = js_create_reference(env, on_send, self->on_send); | |
| 1209 | assert(err == 0); | |
| 1210 | err = js_create_reference(env, on_message, self->on_message); | |
| 1211 | assert(err == 0); | |
| 1212 | err = js_create_reference(env, on_close, self->on_close); | |
| 1213 | assert(err == 0); | |
| 1214 | err = js_create_reference(env, on_firewall, self->on_firewall); | |
| 1215 | assert(err == 0); | |
| 1216 | err = js_create_reference(env, on_remote_changed, self->on_remote_changed); | |
| 1217 | assert(err == 0); | |
| 1218 | err = js_create_reference(env, realloc_data, self->realloc_data); | |
| 1219 | assert(err == 0); | |
| 1220 | err = js_create_reference(env, realloc_message, self->realloc_message); | |
| 1221 | assert(err == 0); | |
| 1222 | ||
| 1223 | udx_stream_firewall(stream, on_udx_stream_firewall); | |
| 1224 | udx_stream_write_resume(stream, on_udx_stream_drain); | |
| 1225 | ||
| 1226 | ensure_teardown(env, udx); | |
| 1227 | } | |
| 1228 | ||
| 1229 | static inline void | |
| 1230 | udx_napi_stream_set_seq ( | |
| 1231 | js_env_t *env, | |
| 1232 | js_typedarray_span_of_t<udx_napi_stream_t, 1> stream, | |
| 1233 | uint32_t seq | |
| 1234 | ) { | |
| 1235 | int err = udx_stream_set_seq(&stream->stream, seq); | |
| 1236 | ||
| 1237 | if (err < 0) { | |
| 1238 | err = js_throw_error(env, uv_err_name(err), uv_strerror(err)); | |
| 1239 | assert(err == 0); | |
| 1240 | } | |
| 1241 | } | |
| 1242 | ||
| 1243 | static inline void | |
| 1244 | udx_napi_stream_set_ack ( | |
| 1245 | js_env_t *env, | |
| 1246 | js_typedarray_span_of_t<udx_napi_stream_t, 1> stream, | |
| 1247 | uint32_t ack | |
| 1248 | ) { | |
| 1249 | int err = udx_stream_set_ack(&stream->stream, ack); | |
| 1250 | ||
| 1251 | if (err < 0) { | |
| 1252 | err = js_throw_error(env, uv_err_name(err), uv_strerror(err)); | |
| 1253 | assert(err == 0); | |
| 1254 | } | |
| 1255 | } | |
| 1256 | ||
| 1257 | static inline void | |
| 1258 | udx_napi_stream_set_mode ( | |
| 1259 | js_env_t *env, | |
| 1260 | js_typedarray_span_of_t<udx_napi_stream_t, 1> stream, | |
| 1261 | uint32_t mode | |
| 1262 | ) { | |
| 1263 | stream->mode = mode; | |
| 1264 | } | |
| 1265 | ||
| 1266 | static inline void | |
| 1267 | udx_napi_stream_recv_start ( | |
| 1268 | js_env_t *env, | |
| 1269 | js_typedarray_span_of_t<udx_napi_stream_t, 1> stream, | |
| 1270 | js_typedarray_span_t<> read_buf | |
| 1271 | ) { | |
| 1272 | int err; | |
| 1273 | ||
| 1274 | stream->read_buf = read_buf.data(); | |
| 1275 | stream->read_buf_head = read_buf.data(); | |
| 1276 | stream->read_buf_free = read_buf.size(); | |
| 1277 | ||
| 1278 | err = udx_stream_read_start(&stream->stream, on_udx_stream_read); | |
| 1279 | assert(err == 0); | |
| 1280 | err = udx_stream_recv_start(&stream->stream, on_udx_stream_recv); | |
| 1281 | assert(err == 0); | |
| 1282 | } | |
| 1283 | ||
| 1284 | static inline void | |
| 1285 | udx_napi_stream_connect ( | |
| 1286 | js_env_t *env, | |
| 1287 | js_typedarray_span_of_t<udx_napi_stream_t, 1> stream, | |
| 1288 | js_typedarray_span_of_t<udx_napi_socket_t, 1> socket, | |
| 1289 | uint32_t remote_id, | |
| 1290 | uint32_t port, | |
| 1291 | std::string ip_str, | |
| 1292 | uint32_t family | |
| 1293 | ) { | |
| 1294 | int err; | |
| 1295 | ||
| 1296 | char ip[INET6_ADDRSTRLEN]; | |
| 1297 | load_address(env, ip, ip_str); | |
| 1298 | ||
| 1299 | struct sockaddr_storage addr; | |
| 1300 | ||
| 1301 | if (family == 4) { | |
| 1302 | err = uv_ip4_addr(ip, port, (struct sockaddr_in *) &addr); | |
| 1303 | } else { | |
| 1304 | err = uv_ip6_addr(ip, port, (struct sockaddr_in6 *) &addr); | |
| 1305 | } | |
| 1306 | ||
| 1307 | if (err < 0) { | |
| 1308 | err = js_throw_error(env, uv_err_name(err), uv_strerror(err)); | |
| 1309 | assert(err == 0); | |
| 1310 | return; | |
| 1311 | } | |
| 1312 | ||
| 1313 | err = udx_stream_connect(&stream->stream, &socket->socket, remote_id, (const struct sockaddr *) &addr); | |
| 1314 | ||
| 1315 | if (err < 0) { | |
| 1316 | err = js_throw_error(env, uv_err_name(err), uv_strerror(err)); | |
| 1317 | assert(err == 0); | |
| 1318 | } | |
| 1319 | } | |
| 1320 | ||
| 1321 | static inline void | |
| 1322 | udx_napi_stream_change_remote ( | |
| 1323 | js_env_t *env, | |
| 1324 | js_typedarray_span_of_t<udx_napi_stream_t, 1> stream, | |
| 1325 | js_typedarray_span_of_t<udx_napi_socket_t, 1> socket, | |
| 1326 | uint32_t remote_id, | |
| 1327 | uint32_t port, | |
| 1328 | std::string ip_str, | |
| 1329 | uint32_t family | |
| 1330 | ) { | |
| 1331 | int err; | |
| 1332 | ||
| 1333 | char ip[INET6_ADDRSTRLEN]; | |
| 1334 | load_address(env, ip, ip_str); | |
| 1335 | ||
| 1336 | struct sockaddr_storage addr; | |
| 1337 | ||
| 1338 | if (family == 4) { | |
| 1339 | err = uv_ip4_addr(ip, port, (struct sockaddr_in *) &addr); | |
| 1340 | } else { | |
| 1341 | err = uv_ip6_addr(ip, port, (struct sockaddr_in6 *) &addr); | |
| 1342 | } | |
| 1343 | ||
| 1344 | if (err < 0) { | |
| 1345 | err = js_throw_error(env, uv_err_name(err), uv_strerror(err)); | |
| 1346 | assert(err == 0); | |
| 1347 | return; | |
| 1348 | } | |
| 1349 | ||
| 1350 | int immediate = err = udx_stream_change_remote(&stream->stream, &socket->socket, remote_id, (const struct sockaddr *) &addr, on_udx_stream_remote_changed); | |
| 1351 | ||
| 1352 | if (err < 0) { | |
| 1353 | err = js_throw_error(env, uv_err_name(err), uv_strerror(err)); | |
| 1354 | assert(err == 0); | |
| 1355 | return; | |
| 1356 | } | |
| 1357 | ||
| 1358 | if (immediate == 1) { | |
| 1359 | js_receiver_t ctx; | |
| 1360 | err = js_get_reference_value(env, stream->ctx, ctx); | |
| 1361 | assert(err == 0); | |
| 1362 | ||
| 1363 | cb_stream_remote_changed_t callback; | |
| 1364 | err = js_get_reference_value(env, stream->on_remote_changed, callback); | |
| 1365 | assert(err == 0); | |
| 1366 | ||
| 1367 | err = js_call_function(env, callback, ctx); | |
| 1368 | assert(err == 0); | |
| 1369 | } | |
| 1370 | } | |
| 1371 | ||
| 1372 | static inline void | |
| 1373 | udx_napi_stream_relay_to ( | |
| 1374 | js_env_t *env, | |
| 1375 | js_typedarray_span_of_t<udx_napi_stream_t, 1> stream, | |
| 1376 | js_typedarray_span_of_t<udx_napi_stream_t, 1> destination | |
| 1377 | ) { | |
| 1378 | int err = udx_stream_relay_to(&stream->stream, &destination->stream); | |
| 1379 | ||
| 1380 | if (err < 0) { | |
| 1381 | err = js_throw_error(env, uv_err_name(err), uv_strerror(err)); | |
| 1382 | assert(err == 0); | |
| 1383 | } | |
| 1384 | } | |
| 1385 | ||
| 1386 | static inline uint32_t | |
| 1387 | udx_napi_stream_send ( | |
| 1388 | js_env_t *env, | |
| 1389 | js_typedarray_span_of_t<udx_napi_stream_t, 1> stream, | |
| 1390 | js_typedarray_span_of_t<udx_stream_send_t, 1> req, | |
| 1391 | uint32_t rid, | |
| 1392 | js_typedarray_span_of_t<char> buf | |
| 1393 | ) { | |
| 1394 | req->data = reinterpret_cast<void *>(static_cast<uintptr_t>(rid)); | |
| 1395 | ||
| 1396 | uv_buf_t b = uv_buf_init(buf.data(), buf.size()); | |
| 1397 | ||
| 1398 | int res = udx_stream_send(req, &stream->stream, &b, 1, on_udx_stream_send); | |
| 1399 | ||
| 1400 | if (res < 0) { | |
| 1401 | int err = js_throw_error(env, uv_err_name(res), uv_strerror(res)); | |
| 1402 | assert(err == 0); | |
| 1403 | } | |
| 1404 | ||
| 1405 | return res; | |
| 1406 | } | |
| 1407 | ||
| 1408 | static inline int64_t | |
| 1409 | udx_napi_stream_write ( | |
| 1410 | js_env_t *env, | |
| 1411 | js_receiver_t, | |
| 1412 | js_typedarray_span_of_t<udx_napi_stream_t, 1> stream, | |
| 1413 | js_typedarray_span_t<> req_handle, | |
| 1414 | uint32_t rid, | |
| 1415 | js_typedarray_span_of_t<char> buf | |
| 1416 | ) { | |
| 1417 | int err; | |
| 1418 | ||
| 1419 | auto req = reinterpret_cast<udx_stream_write_t *>(req_handle.data()); | |
| 1420 | ||
| 1421 | req->data = reinterpret_cast<void *>(static_cast<uintptr_t>(rid)); | |
| 1422 | ||
| 1423 | auto b = uv_buf_init(buf.data(), buf.size()); | |
| 1424 | ||
| 1425 | int res = udx_stream_write(&*req, &stream->stream, &b, 1, on_udx_stream_ack); | |
| 1426 | ||
| 1427 | if (res < 0) { | |
| 1428 | err = js_throw_error(env, uv_err_name(res), uv_strerror(res)); | |
| 1429 | assert(err == 0); | |
| 1430 | } | |
| 1431 | ||
| 1432 | return res; | |
| 1433 | } | |
| 1434 | ||
| 1435 | // TODO: use only on platforms without fastcall support | |
| 1436 | static inline int64_t | |
| 1437 | udx_napi_stream_writev ( | |
| 1438 | js_env_t *env, | |
| 1439 | js_typedarray_span_of_t<udx_napi_stream_t, 1> stream, | |
| 1440 | js_typedarray_span_t<> req_handle, | |
| 1441 | uint32_t rid, | |
| 1442 | std::vector<js_typedarray_span_of_t<char>> buffers | |
| 1443 | ) { | |
| 1444 | int err; | |
| 1445 | ||
| 1446 | auto req = reinterpret_cast<udx_stream_write_t *>(req_handle.data()); | |
| 1447 | ||
| 1448 | req->data = reinterpret_cast<void *>(static_cast<uintptr_t>(rid)); | |
| 1449 | ||
| 1450 | const auto len = buffers.size(); | |
| 1451 | ||
| 1452 | std::vector<uv_buf_t> batch(len); | |
| 1453 | ||
| 1454 | for (int i = 0; i < len; i++) { | |
| 1455 | auto buf = buffers[i]; | |
| 1456 | batch[i] = uv_buf_init(buf.data(), buf.size()); | |
| 1457 | } | |
| 1458 | ||
| 1459 | int res = udx_stream_write(req, &stream->stream, batch.data(), len, on_udx_stream_ack); | |
| 1460 | ||
| 1461 | if (res < 0) { | |
| 1462 | err = js_throw_error(env, uv_err_name(res), uv_strerror(res)); | |
| 1463 | assert(err == 0); | |
| 1464 | } | |
| 1465 | ||
| 1466 | return res; | |
| 1467 | } | |
| 1468 | ||
| 1469 | static inline uint32_t | |
| 1470 | udx_napi_stream_write_sizeof (uint32_t bufs) { | |
| 1471 | return udx_stream_write_sizeof(bufs); | |
| 1472 | } | |
| 1473 | ||
| 1474 | static inline uint32_t | |
| 1475 | udx_napi_stream_write_end ( | |
| 1476 | js_env_t *env, | |
| 1477 | js_typedarray_span_of_t<udx_napi_stream_t, 1> stream, | |
| 1478 | js_typedarray_span_t<> req_handle, | |
| 1479 | uint32_t rid, | |
| 1480 | js_typedarray_span_of_t<char> buf | |
| 1481 | ) { | |
| 1482 | ||
| 1483 | auto req = reinterpret_cast<udx_stream_write_t *>(req_handle.data()); | |
| 1484 | ||
| 1485 | req->data = reinterpret_cast<void *>(static_cast<uintptr_t>(rid)); | |
| 1486 | ||
| 1487 | uv_buf_t b = uv_buf_init(buf.data(), buf.size()); | |
| 1488 | ||
| 1489 | int res = udx_stream_write_end(req, &stream->stream, &b, 1, on_udx_stream_ack); | |
| 1490 | ||
| 1491 | if (res < 0) { | |
| 1492 | int err = js_throw_error(env, uv_err_name(res), uv_strerror(res)); | |
| 1493 | assert(err == 0); | |
| 1494 | } | |
| 1495 | ||
| 1496 | return res; | |
| 1497 | } | |
| 1498 | ||
| 1499 | static inline uint32_t | |
| 1500 | udx_napi_stream_destroy ( | |
| 1501 | js_env_t *env, | |
| 1502 | js_typedarray_span_of_t<udx_napi_stream_t, 1> stream | |
| 1503 | ) { | |
| 1504 | int res = udx_stream_destroy(&stream->stream); | |
| 1505 | ||
| 1506 | if (res < 0) { | |
| 1507 | int err = js_throw_error(env, uv_err_name(res), uv_strerror(res)); | |
| 1508 | assert(err == 0); | |
| 1509 | } | |
| 1510 | ||
| 1511 | return res; | |
| 1512 | } | |
| 1513 | ||
| 1514 | static inline void | |
| 1515 | udx_napi_lookup ( | |
| 1516 | js_env_t *env, | |
| 1517 | js_typedarray_span_of_t<udx_napi_t, 1> udx, | |
| 1518 | js_typedarray_span_of_t<udx_napi_lookup_t, 1> self, | |
| 1519 | js_string_t host_str, | |
| 1520 | uint32_t family, | |
| 1521 | js_receiver_t ctx, // TODO: use auto receiver | |
| 1522 | cb_udx_lookup_t on_lookup | |
| 1523 | ) { | |
| 1524 | int err; | |
| 1525 | ||
| 1526 | *self = {}; | |
| 1527 | ||
| 1528 | self->udx = udx; | |
| 1529 | ||
| 1530 | std::string host; | |
| 1531 | err = js_get_value_string(env, host_str, host); | |
| 1532 | assert(err == 0); | |
| 1533 | ||
| 1534 | udx_lookup_t *lookup = &self->handle; | |
| 1535 | ||
| 1536 | self->env = env; | |
| 1537 | ||
| 1538 | int flags = 0; | |
| 1539 | ||
| 1540 | if (family == 4) flags |= UDX_LOOKUP_FAMILY_IPV4; | |
| 1541 | if (family == 6) flags |= UDX_LOOKUP_FAMILY_IPV6; | |
| 1542 | ||
| 1543 | err = udx_lookup(&udx->udx, lookup, host.c_str(), flags, on_udx_lookup); | |
| 1544 | ||
| 1545 | if (err < 0) { | |
| 1546 | err = js_throw_error(env, uv_err_name(err), uv_strerror(err)); | |
| 1547 | assert(err == 0); | |
| 1548 | ||
| 1549 | return; | |
| 1550 | } | |
| 1551 | ||
| 1552 | err = js_create_reference(env, ctx, self->ctx); | |
| 1553 | assert(err == 0); | |
| 1554 | err = js_create_reference(env, on_lookup, self->on_lookup); | |
| 1555 | assert(err == 0); | |
| 1556 | ||
| 1557 | ensure_teardown(env, udx); | |
| 1558 | } | |
| 1559 | ||
| 1560 | static inline void | |
| 1561 | udx_napi_interface_event_init ( | |
| 1562 | js_env_t *env, | |
| 1563 | js_typedarray_span_of_t<udx_napi_t, 1> udx, | |
| 1564 | js_typedarray_span_of_t<udx_napi_interface_event_t, 1> self, | |
| 1565 | js_receiver_t ctx, | |
| 1566 | cb_interface_event_t on_event, | |
| 1567 | cb_interface_close_t on_close | |
| 1568 | ) { | |
| 1569 | int err; | |
| 1570 | ||
| 1571 | *self = {}; | |
| 1572 | ||
| 1573 | self->udx = udx; | |
| 1574 | self->env = env; | |
| 1575 | ||
| 1576 | err = udx_interface_event_init(&udx->udx, &self->handle, on_udx_interface_event_close); | |
| 1577 | ||
| 1578 | if (err < 0) { | |
| 1579 | err = js_throw_error(env, uv_err_name(err), uv_strerror(err)); | |
| 1580 | assert(err == 0); | |
| 1581 | ||
| 1582 | return; | |
| 1583 | } | |
| 1584 | ||
| 1585 | err = udx_interface_event_start(&self->handle, on_udx_interface_event, 5000); | |
| 1586 | ||
| 1587 | if (err < 0) { | |
| 1588 | err = js_throw_error(env, uv_err_name(err), uv_strerror(err)); | |
| 1589 | assert(err == 0); | |
| 1590 | ||
| 1591 | return; | |
| 1592 | } | |
| 1593 | ||
| 1594 | err = js_create_reference(env, ctx, self->ctx); | |
| 1595 | assert(err == 0); | |
| 1596 | err = js_create_reference(env, on_event, self->on_event); | |
| 1597 | assert(err == 0); | |
| 1598 | err = js_create_reference(env, on_close, self->on_close); | |
| 1599 | assert(err == 0); | |
| 1600 | ||
| 1601 | ensure_teardown(env, udx); | |
| 1602 | } | |
| 1603 | ||
| 1604 | static inline void | |
| 1605 | udx_napi_interface_event_start ( | |
| 1606 | js_env_t *env, | |
| 1607 | js_typedarray_span_of_t<udx_napi_interface_event_t, 1> self | |
| 1608 | ) { | |
| 1609 | int err = udx_interface_event_start(&self->handle, on_udx_interface_event, 5000); | |
| 1610 | ||
| 1611 | if (err < 0) { | |
| 1612 | err = js_throw_error(env, uv_err_name(err), uv_strerror(err)); | |
| 1613 | assert(err == 0); | |
| 1614 | } | |
| 1615 | } | |
| 1616 | ||
| 1617 | static inline void | |
| 1618 | udx_napi_interface_event_stop ( | |
| 1619 | js_env_t *env, | |
| 1620 | js_typedarray_span_of_t<udx_napi_interface_event_t, 1> self | |
| 1621 | ) { | |
| 1622 | int err; | |
| 1623 | ||
| 1624 | err = udx_interface_event_stop(&self->handle); | |
| 1625 | ||
| 1626 | if (err < 0) { | |
| 1627 | err = js_throw_error(env, uv_err_name(err), uv_strerror(err)); | |
| 1628 | assert(err == 0); | |
| 1629 | } | |
| 1630 | } | |
| 1631 | ||
| 1632 | static inline void | |
| 1633 | udx_napi_interface_event_close ( | |
| 1634 | js_env_t *env, | |
| 1635 | js_typedarray_span_of_t<udx_napi_interface_event_t, 1> self | |
| 1636 | ) { | |
| 1637 | int err; | |
| 1638 | ||
| 1639 | err = udx_interface_event_close(&self->handle); | |
| 1640 | ||
| 1641 | if (err < 0) { | |
| 1642 | err = js_throw_error(env, uv_err_name(err), uv_strerror(err)); | |
| 1643 | assert(err == 0); | |
| 1644 | } | |
| 1645 | } | |
| 1646 | ||
| 1647 | static inline std::vector<js_object_t> | |
| 1648 | udx_napi_interface_event_get_addrs ( | |
| 1649 | js_env_t *env, | |
| 1650 | js_typedarray_span_of_t<udx_napi_interface_event_t, 1> self | |
| 1651 | ) { | |
| 1652 | int err; | |
| 1653 | ||
| 1654 | auto event = &self->handle; | |
| 1655 | ||
| 1656 | char ip[INET6_ADDRSTRLEN]; | |
| 1657 | int family = 0; | |
| 1658 | ||
| 1659 | std::vector<js_object_t> result; | |
| 1660 | ||
| 1661 | for (int i = 0; i < event->addrs_len; i++) { | |
| 1662 | uv_interface_address_t addr = event->addrs[i]; | |
| 1663 | ||
| 1664 | if (addr.address.address4.sin_family == AF_INET) { | |
| 1665 | uv_ip4_name(&addr.address.address4, ip, sizeof(ip)); | |
| 1666 | family = 4; | |
| 1667 | } else if (addr.address.address4.sin_family == AF_INET6) { | |
| 1668 | uv_ip6_name(&addr.address.address6, ip, sizeof(ip)); | |
| 1669 | family = 6; | |
| 1670 | } else { | |
| 1671 | continue; | |
| 1672 | } | |
| 1673 | ||
| 1674 | js_object_t item; | |
| 1675 | err = js_create_object(env, item); | |
| 1676 | assert(err == 0); | |
| 1677 | ||
| 1678 | err = js_set_property(env, item, "name", addr.name); | |
| 1679 | assert(err == 0); | |
| 1680 | ||
| 1681 | err = js_set_property(env, item, "host", ip); | |
| 1682 | assert(err == 0); | |
| 1683 | ||
| 1684 | err = js_set_property(env, item, "family", family); | |
| 1685 | assert(err == 0); | |
| 1686 | ||
| 1687 | err = js_set_property(env, item, "internal", bool(addr.is_internal)); | |
| 1688 | assert(err == 0); | |
| 1689 | ||
| 1690 | result.push_back(item); | |
| 1691 | } | |
| 1692 | ||
| 1693 | return result; | |
| 1694 | } | |
| 1695 | ||
| 1696 | static js_value_t * | |
| 1697 | udx_native_exports (js_env_t *env, js_value_t *exports) { | |
| 1698 | int err; | |
| 1699 | ||
| 1700 | // uint32 | |
| 1701 | #define V(name, value) \ | |
| 1702 | err = js_set_property(env, exports, name, uint32_t(value)); \ | |
| 1703 | assert(err == 0); | |
| 1704 | ||
| 1705 | V("UV_UDP_IPV6ONLY", UV_UDP_IPV6ONLY); | |
| 1706 | V("UV_UDP_REUSEADDR", UV_UDP_REUSEADDR); | |
| 1707 | ||
| 1708 | V("offsetof_udx_stream_t_inflight", offsetof(udx_stream_t, inflight)); | |
| 1709 | V("offsetof_udx_stream_t_mtu", offsetof(udx_stream_t, mtu)); | |
| 1710 | V("offsetof_udx_stream_t_cwnd", offsetof(udx_stream_t, cwnd)); | |
| 1711 | V("offsetof_udx_stream_t_srtt", offsetof(udx_stream_t, srtt)); | |
| 1712 | V("offsetof_udx_stream_t_bytes_rx", offsetof(udx_stream_t, bytes_rx)); | |
| 1713 | V("offsetof_udx_stream_t_packets_rx", offsetof(udx_stream_t, packets_rx)); | |
| 1714 | V("offsetof_udx_stream_t_bytes_tx", offsetof(udx_stream_t, bytes_tx)); | |
| 1715 | V("offsetof_udx_stream_t_packets_tx", offsetof(udx_stream_t, packets_tx)); | |
| 1716 | V("offsetof_udx_stream_t_rto_count", offsetof(udx_stream_t, rto_count)); | |
| 1717 | V("offsetof_udx_stream_t_retransmit_count", offsetof(udx_stream_t, retransmit_count)); | |
| 1718 | V("offsetof_udx_stream_t_fast_recovery_count", offsetof(udx_stream_t, fast_recovery_count)); | |
| 1719 | V("offsetof_udx_socket_t_bytes_rx", offsetof(udx_socket_t, bytes_rx)); | |
| 1720 | V("offsetof_udx_socket_t_packets_rx", offsetof(udx_socket_t, packets_rx)); | |
| 1721 | V("offsetof_udx_socket_t_bytes_tx", offsetof(udx_socket_t, bytes_tx)); | |
| 1722 | V("offsetof_udx_socket_t_packets_tx", offsetof(udx_socket_t, packets_tx)); | |
| 1723 | V("offsetof_udx_socket_t_packets_dropped_by_kernel", offsetof(udx_socket_t, packets_dropped_by_kernel)); | |
| 1724 | ||
| 1725 | V("offsetof_udx_t_bytes_rx", offsetof(udx_t, bytes_rx)); | |
| 1726 | V("offsetof_udx_t_packets_rx", offsetof(udx_t, packets_rx)); | |
| 1727 | V("offsetof_udx_t_bytes_tx", offsetof(udx_t, bytes_tx)); | |
| 1728 | V("offsetof_udx_t_packets_tx", offsetof(udx_t, packets_tx)); | |
| 1729 | V("offsetof_udx_t_packets_dropped_by_kernel", offsetof(udx_t, packets_dropped_by_kernel)); | |
| 1730 | ||
| 1731 | V("sizeof_udx_napi_t", sizeof(udx_napi_t)); | |
| 1732 | V("sizeof_udx_napi_socket_t", sizeof(udx_napi_socket_t)); | |
| 1733 | V("sizeof_udx_napi_stream_t", sizeof(udx_napi_stream_t)); | |
| 1734 | V("sizeof_udx_napi_lookup_t", sizeof(udx_napi_lookup_t)); | |
| 1735 | V("sizeof_udx_napi_interface_event_t", sizeof(udx_napi_interface_event_t)); | |
| 1736 | V("sizeof_udx_socket_send_t", sizeof(udx_socket_send_t)); | |
| 1737 | V("sizeof_udx_stream_send_t", sizeof(udx_stream_send_t)); | |
| 1738 | #undef V | |
| 1739 | ||
| 1740 | // functions | |
| 1741 | #define V(name, fn) \ | |
| 1742 | err = js_set_property<fn>(env, exports, name); \ | |
| 1743 | assert(err == 0); | |
| 1744 | ||
| 1745 | V("udx_napi_init", udx_napi_init); | |
| 1746 | V("udx_napi_socket_init", udx_napi_socket_init); | |
| 1747 | V("udx_napi_socket_bind", udx_napi_socket_bind); | |
| 1748 | V("udx_napi_socket_set_ttl", udx_napi_socket_set_ttl); | |
| 1749 | V("udx_napi_socket_get_recv_buffer_size", udx_napi_socket_get_recv_buffer_size); | |
| 1750 | V("udx_napi_socket_set_recv_buffer_size", udx_napi_socket_set_recv_buffer_size); | |
| 1751 | V("udx_napi_socket_get_send_buffer_size", udx_napi_socket_get_send_buffer_size); | |
| 1752 | V("udx_napi_socket_set_send_buffer_size", udx_napi_socket_set_send_buffer_size); | |
| 1753 | V("udx_napi_socket_set_membership", udx_napi_socket_set_membership); | |
| 1754 | V("udx_napi_socket_send_ttl", udx_napi_socket_send_ttl); | |
| 1755 | V("udx_napi_socket_close", udx_napi_socket_close); | |
| 1756 | V("udx_napi_stream_init", udx_napi_stream_init); | |
| 1757 | V("udx_napi_stream_set_seq", udx_napi_stream_set_seq); | |
| 1758 | V("udx_napi_stream_set_ack", udx_napi_stream_set_ack); | |
| 1759 | V("udx_napi_stream_set_mode", udx_napi_stream_set_mode); | |
| 1760 | V("udx_napi_stream_connect", udx_napi_stream_connect); | |
| 1761 | V("udx_napi_stream_change_remote", udx_napi_stream_change_remote); | |
| 1762 | V("udx_napi_stream_relay_to", udx_napi_stream_relay_to); | |
| 1763 | V("udx_napi_stream_send", udx_napi_stream_send); | |
| 1764 | V("udx_napi_stream_recv_start", udx_napi_stream_recv_start); | |
| 1765 | V("udx_napi_stream_write", udx_napi_stream_write); | |
| 1766 | V("udx_napi_stream_writev", udx_napi_stream_writev); | |
| 1767 | V("udx_napi_stream_write_sizeof", udx_napi_stream_write_sizeof); | |
| 1768 | V("udx_napi_stream_write_end", udx_napi_stream_write_end); | |
| 1769 | V("udx_napi_stream_destroy", udx_napi_stream_destroy); | |
| 1770 | V("udx_napi_lookup", udx_napi_lookup); | |
| 1771 | V("udx_napi_interface_event_init", udx_napi_interface_event_init); | |
| 1772 | V("udx_napi_interface_event_start", udx_napi_interface_event_start); | |
| 1773 | V("udx_napi_interface_event_stop", udx_napi_interface_event_stop); | |
| 1774 | V("udx_napi_interface_event_close", udx_napi_interface_event_close); | |
| 1775 | V("udx_napi_interface_event_get_addrs", udx_napi_interface_event_get_addrs); | |
| 1776 | #undef V | |
| 1777 | ||
| 1778 | return exports; | |
| 1779 | } | |
| 1780 | ||
| 1781 | BARE_MODULE(udx_native, udx_native_exports) |
| … | … | @@ -0,0 +1,3 @@ |
| 1 | require.addon = require('require-addon') | |
| 2 | ||
| 3 | module.exports = require.addon('.', __filename) |
| … | … | @@ -0,0 +1,52 @@ |
| 1 | // Copyright Joyent, Inc. and other Node contributors. | |
| 2 | // | |
| 3 | // Permission is hereby granted, free of charge, to any person obtaining a | |
| 4 | // copy of this software and associated documentation files (the | |
| 5 | // "Software"), to deal in the Software without restriction, including | |
| 6 | // without limitation the rights to use, copy, modify, merge, publish, | |
| 7 | // distribute, sublicense, and/or sell copies of the Software, and to permit | |
| 8 | // persons to whom the Software is furnished to do so, subject to the | |
| 9 | // following conditions: | |
| 10 | // | |
| 11 | // The above copyright notice and this permission notice shall be included | |
| 12 | // in all copies or substantial portions of the Software. | |
| 13 | // | |
| 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS | |
| 15 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | |
| 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN | |
| 17 | // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, | |
| 18 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR | |
| 19 | // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE | |
| 20 | // USE OR OTHER DEALINGS IN THE SOFTWARE. | |
| 21 | ||
| 22 | const v4Seg = '(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])' | |
| 23 | const v4Str = `(${v4Seg}[.]){3}${v4Seg}` | |
| 24 | const IPv4Pattern = new RegExp(`^${v4Str}$`) | |
| 25 | ||
| 26 | const v6Seg = '(?:[0-9a-fA-F]{1,4})' | |
| 27 | const IPv6Pattern = new RegExp( | |
| 28 | '^(' + | |
| 29 | `(?:${v6Seg}:){7}(?:${v6Seg}|:)|` + | |
| 30 | `(?:${v6Seg}:){6}(?:${v4Str}|:${v6Seg}|:)|` + | |
| 31 | `(?:${v6Seg}:){5}(?::${v4Str}|(:${v6Seg}){1,2}|:)|` + | |
| 32 | `(?:${v6Seg}:){4}(?:(:${v6Seg}){0,1}:${v4Str}|(:${v6Seg}){1,3}|:)|` + | |
| 33 | `(?:${v6Seg}:){3}(?:(:${v6Seg}){0,2}:${v4Str}|(:${v6Seg}){1,4}|:)|` + | |
| 34 | `(?:${v6Seg}:){2}(?:(:${v6Seg}){0,3}:${v4Str}|(:${v6Seg}){1,5}|:)|` + | |
| 35 | `(?:${v6Seg}:){1}(?:(:${v6Seg}){0,4}:${v4Str}|(:${v6Seg}){1,6}|:)|` + | |
| 36 | `(?::((?::${v6Seg}){0,5}:${v4Str}|(?::${v6Seg}){1,7}|:))` + | |
| 37 | ')(%[0-9a-zA-Z-.:]{1,})?$' | |
| 38 | ) | |
| 39 | ||
| 40 | const isIPv4 = (exports.isIPv4 = function isIPv4(host) { | |
| 41 | return IPv4Pattern.test(host) | |
| 42 | }) | |
| 43 | ||
| 44 | const isIPv6 = (exports.isIPv6 = function isIPv6(host) { | |
| 45 | return IPv6Pattern.test(host) | |
| 46 | }) | |
| 47 | ||
| 48 | exports.isIP = function isIP(host) { | |
| 49 | if (isIPv4(host)) return 4 | |
| 50 | if (isIPv6(host)) return 6 | |
| 51 | return 0 | |
| 52 | } |
| … | … | @@ -0,0 +1,64 @@ |
| 1 | const events = require('events') | |
| 2 | const b4a = require('b4a') | |
| 3 | const binding = require('../binding') | |
| 4 | ||
| 5 | module.exports = class NetworkInterfaces extends events.EventEmitter { | |
| 6 | constructor(udx) { | |
| 7 | super() | |
| 8 | ||
| 9 | this._handle = b4a.alloc(binding.sizeof_udx_napi_interface_event_t) | |
| 10 | this._watching = false | |
| 11 | this._destroying = null | |
| 12 | ||
| 13 | binding.udx_napi_interface_event_init( | |
| 14 | udx._handle, | |
| 15 | this._handle, | |
| 16 | this, | |
| 17 | this._onevent, | |
| 18 | this._onclose | |
| 19 | ) | |
| 20 | ||
| 21 | this.interfaces = binding.udx_napi_interface_event_get_addrs(this._handle) | |
| 22 | } | |
| 23 | ||
| 24 | _onclose() { | |
| 25 | this.emit('close') | |
| 26 | } | |
| 27 | ||
| 28 | _onevent() { | |
| 29 | this.interfaces = binding.udx_napi_interface_event_get_addrs(this._handle) | |
| 30 | ||
| 31 | this.emit('change', this.interfaces) | |
| 32 | } | |
| 33 | ||
| 34 | watch() { | |
| 35 | if (this._watching) return this | |
| 36 | this._watching = true | |
| 37 | ||
| 38 | binding.udx_napi_interface_event_start(this._handle) | |
| 39 | ||
| 40 | return this | |
| 41 | } | |
| 42 | ||
| 43 | unwatch() { | |
| 44 | if (!this._watching) return this | |
| 45 | this._watching = false | |
| 46 | ||
| 47 | binding.udx_napi_interface_event_stop(this._handle) | |
| 48 | ||
| 49 | return this | |
| 50 | } | |
| 51 | ||
| 52 | async destroy() { | |
| 53 | if (this._destroying) return this._destroying | |
| 54 | this._destroying = events.once(this, 'close') | |
| 55 | ||
| 56 | binding.udx_napi_interface_event_close(this._handle) | |
| 57 | ||
| 58 | return this._destroying | |
| 59 | } | |
| 60 | ||
| 61 | [Symbol.iterator]() { | |
| 62 | return this.interfaces[Symbol.iterator]() | |
| 63 | } | |
| 64 | } |
| … | … | @@ -0,0 +1,331 @@ |
| 1 | const events = require('events') | |
| 2 | const b4a = require('b4a') | |
| 3 | const binding = require('../binding') | |
| 4 | const ip = require('./ip') | |
| 5 | ||
| 6 | module.exports = class UDXSocket extends events.EventEmitter { | |
| 7 | constructor(udx, opts = {}) { | |
| 8 | super() | |
| 9 | ||
| 10 | this.udx = udx | |
| 11 | ||
| 12 | this._handle = b4a.alloc(binding.sizeof_udx_napi_socket_t) | |
| 13 | this._inited = false | |
| 14 | this._host = null | |
| 15 | this._family = 0 | |
| 16 | this._ipv6Only = opts.ipv6Only === true | |
| 17 | this._reuseAddress = opts.reuseAddress === true | |
| 18 | this._port = 0 | |
| 19 | this._reqs = [] | |
| 20 | this._free = [] | |
| 21 | this._closing = null | |
| 22 | this._closed = false | |
| 23 | ||
| 24 | this._view64 = new BigUint64Array( | |
| 25 | this._handle.buffer, | |
| 26 | this._handle.byteOffset, | |
| 27 | this._handle.byteLength >> 3 | |
| 28 | ) | |
| 29 | ||
| 30 | this.streams = new Set() | |
| 31 | ||
| 32 | this.userData = null | |
| 33 | } | |
| 34 | ||
| 35 | get bound() { | |
| 36 | return this._port !== 0 | |
| 37 | } | |
| 38 | ||
| 39 | get closing() { | |
| 40 | return this._closing !== null | |
| 41 | } | |
| 42 | ||
| 43 | get idle() { | |
| 44 | return this.streams.size === 0 | |
| 45 | } | |
| 46 | ||
| 47 | get busy() { | |
| 48 | return this.streams.size > 0 | |
| 49 | } | |
| 50 | ||
| 51 | get bytesTransmitted() { | |
| 52 | if (this._inited !== true) return 0 | |
| 53 | return Number(this._view64[binding.offsetof_udx_socket_t_bytes_tx >> 3]) | |
| 54 | } | |
| 55 | ||
| 56 | get packetsTransmitted() { | |
| 57 | if (this._inited !== true) return 0 | |
| 58 | return Number(this._view64[binding.offsetof_udx_socket_t_packets_tx >> 3]) | |
| 59 | } | |
| 60 | ||
| 61 | get bytesReceived() { | |
| 62 | if (this._inited !== true) return 0 | |
| 63 | return Number(this._view64[binding.offsetof_udx_socket_t_bytes_rx >> 3]) | |
| 64 | } | |
| 65 | ||
| 66 | get packetsReceived() { | |
| 67 | if (this._inited !== true) return 0 | |
| 68 | return Number(this._view64[binding.offsetof_udx_socket_t_packets_rx >> 3]) | |
| 69 | } | |
| 70 | ||
| 71 | get packetsDroppedByKernel() { | |
| 72 | if (this._inited !== true) return 0 | |
| 73 | return Number(this._view64[binding.offsetof_udx_socket_t_packets_dropped_by_kernel >> 3]) | |
| 74 | } | |
| 75 | ||
| 76 | toJSON() { | |
| 77 | return { | |
| 78 | bound: this.bound, | |
| 79 | closing: this.closing, | |
| 80 | streams: this.streams.size, | |
| 81 | address: this.address(), | |
| 82 | ipv6Only: this._ipv6Only, | |
| 83 | reuseAddress: this._reuseAddress, | |
| 84 | idle: this.idle, | |
| 85 | busy: this.busy | |
| 86 | } | |
| 87 | } | |
| 88 | ||
| 89 | _init() { | |
| 90 | if (this._inited) return | |
| 91 | ||
| 92 | binding.udx_napi_socket_init( | |
| 93 | this.udx._handle, | |
| 94 | this._handle, | |
| 95 | this, | |
| 96 | this._onsend, | |
| 97 | this._onmessage, | |
| 98 | this._onclose, | |
| 99 | this._reallocMessage | |
| 100 | ) | |
| 101 | ||
| 102 | this._inited = true | |
| 103 | } | |
| 104 | ||
| 105 | _onsend(id, err) { | |
| 106 | const req = this._reqs[id] | |
| 107 | ||
| 108 | const onflush = req.onflush | |
| 109 | ||
| 110 | req.buffer = null | |
| 111 | req.onflush = null | |
| 112 | ||
| 113 | this._free.push(id) | |
| 114 | ||
| 115 | onflush(err >= 0) | |
| 116 | ||
| 117 | // gc the free list | |
| 118 | if (this._free.length >= 16 && this._free.length === this._reqs.length) { | |
| 119 | this._free = [] | |
| 120 | this._reqs = [] | |
| 121 | } | |
| 122 | } | |
| 123 | ||
| 124 | _onmessage(len, port, host, family) { | |
| 125 | this.emit('message', this.udx._consumeMessage(len), { host, family, port }) | |
| 126 | return this.udx._buffer | |
| 127 | } | |
| 128 | ||
| 129 | _onclose() { | |
| 130 | this.emit('close') | |
| 131 | } | |
| 132 | ||
| 133 | _reallocMessage() { | |
| 134 | return this.udx._reallocMessage() | |
| 135 | } | |
| 136 | ||
| 137 | _onidle() { | |
| 138 | this.emit('idle') | |
| 139 | } | |
| 140 | ||
| 141 | _onbusy() { | |
| 142 | this.emit('busy') | |
| 143 | } | |
| 144 | ||
| 145 | _addStream(stream) { | |
| 146 | if (this.streams.has(stream)) return false | |
| 147 | this.streams.add(stream) | |
| 148 | if (this.streams.size === 1) this._onbusy() | |
| 149 | return true | |
| 150 | } | |
| 151 | ||
| 152 | _removeStream(stream) { | |
| 153 | if (!this.streams.has(stream)) return false | |
| 154 | this.streams.delete(stream) | |
| 155 | const closed = this._closeMaybe() | |
| 156 | if (this.idle && !closed) this._onidle() | |
| 157 | return true | |
| 158 | } | |
| 159 | ||
| 160 | address() { | |
| 161 | if (!this.bound) return null | |
| 162 | return { host: this._host, family: this._family, port: this._port } | |
| 163 | } | |
| 164 | ||
| 165 | bind(port, host) { | |
| 166 | if (this.bound) throw new Error('Already bound') | |
| 167 | if (this.closing) throw new Error('Socket is closed') | |
| 168 | ||
| 169 | if (!port) port = 0 | |
| 170 | ||
| 171 | let flags = 0 | |
| 172 | if (this._ipv6Only) flags |= binding.UV_UDP_IPV6ONLY | |
| 173 | if (this._reuseAddress) flags |= binding.UV_UDP_REUSEADDR | |
| 174 | ||
| 175 | let family | |
| 176 | ||
| 177 | if (host) { | |
| 178 | family = ip.isIP(host) | |
| 179 | if (!family) throw new Error(`${host} is not a valid IP address`) | |
| 180 | ||
| 181 | if (!this._inited) this._init() | |
| 182 | ||
| 183 | this._port = binding.udx_napi_socket_bind(this._handle, port, host, family, flags) | |
| 184 | } else { | |
| 185 | if (!this._inited) this._init() | |
| 186 | ||
| 187 | try { | |
| 188 | host = '::' | |
| 189 | family = 6 | |
| 190 | this._port = binding.udx_napi_socket_bind(this._handle, port, host, family, flags) | |
| 191 | } catch { | |
| 192 | host = '0.0.0.0' | |
| 193 | family = 4 | |
| 194 | this._port = binding.udx_napi_socket_bind(this._handle, port, host, family, flags) | |
| 195 | } | |
| 196 | } | |
| 197 | ||
| 198 | this._host = host | |
| 199 | this._family = family | |
| 200 | ||
| 201 | this.emit('listening') | |
| 202 | } | |
| 203 | ||
| 204 | async close() { | |
| 205 | if (this._closing) return this._closing | |
| 206 | this._closing = new Promise((resolve) => this.once('close', resolve)) | |
| 207 | this._closeMaybe() | |
| 208 | return this._closing | |
| 209 | } | |
| 210 | ||
| 211 | _closeMaybe() { | |
| 212 | if (this._closed || this._closing === null) return this._closed | |
| 213 | ||
| 214 | if (!this._inited) { | |
| 215 | this._closed = true | |
| 216 | this.emit('close') | |
| 217 | return true | |
| 218 | } | |
| 219 | ||
| 220 | if (this.idle) { | |
| 221 | binding.udx_napi_socket_close(this._handle) | |
| 222 | this._closed = true | |
| 223 | } | |
| 224 | ||
| 225 | return this._closed | |
| 226 | } | |
| 227 | ||
| 228 | setTTL(ttl) { | |
| 229 | if (!this._inited) throw new Error('Socket not active') | |
| 230 | binding.udx_napi_socket_set_ttl(this._handle, ttl) | |
| 231 | } | |
| 232 | ||
| 233 | getRecvBufferSize() { | |
| 234 | if (!this._inited) throw new Error('Socket not active') | |
| 235 | return binding.udx_napi_socket_get_recv_buffer_size(this._handle) | |
| 236 | } | |
| 237 | ||
| 238 | setRecvBufferSize(size) { | |
| 239 | if (!this._inited) throw new Error('Socket not active') | |
| 240 | return binding.udx_napi_socket_set_recv_buffer_size(this._handle, size) | |
| 241 | } | |
| 242 | ||
| 243 | getSendBufferSize() { | |
| 244 | if (!this._inited) throw new Error('Socket not active') | |
| 245 | return binding.udx_napi_socket_get_send_buffer_size(this._handle) | |
| 246 | } | |
| 247 | ||
| 248 | setSendBufferSize(size) { | |
| 249 | if (!this._inited) throw new Error('Socket not active') | |
| 250 | return binding.udx_napi_socket_set_send_buffer_size(this._handle, size) | |
| 251 | } | |
| 252 | ||
| 253 | addMembership(group, ifaceAddress) { | |
| 254 | if (!this._inited) throw new Error('Socket not active') | |
| 255 | return binding.udx_napi_socket_set_membership(this._handle, group, ifaceAddress || '', true) | |
| 256 | } | |
| 257 | ||
| 258 | dropMembership(group, ifaceAddress) { | |
| 259 | if (!this._inited) throw new Error('Socket not active') | |
| 260 | return binding.udx_napi_socket_set_membership(this._handle, group, ifaceAddress || '', false) | |
| 261 | } | |
| 262 | ||
| 263 | async send(buffer, port, host, ttl) { | |
| 264 | if (this.closing) return false | |
| 265 | ||
| 266 | if (!host) host = '127.0.0.1' | |
| 267 | ||
| 268 | const family = ip.isIP(host) | |
| 269 | if (!family) throw new Error(`${host} is not a valid IP address`) | |
| 270 | ||
| 271 | if (!this.bound) this.bind(0) | |
| 272 | ||
| 273 | const id = this._allocSend() | |
| 274 | const req = this._reqs[id] | |
| 275 | ||
| 276 | req.buffer = buffer | |
| 277 | ||
| 278 | const promise = new Promise((resolve) => { | |
| 279 | req.onflush = resolve | |
| 280 | }) | |
| 281 | ||
| 282 | binding.udx_napi_socket_send_ttl( | |
| 283 | this._handle, | |
| 284 | req.handle, | |
| 285 | id, | |
| 286 | buffer, | |
| 287 | port, | |
| 288 | host, | |
| 289 | family, | |
| 290 | ttl || 0 | |
| 291 | ) | |
| 292 | ||
| 293 | return promise | |
| 294 | } | |
| 295 | ||
| 296 | trySend(buffer, port, host, ttl) { | |
| 297 | if (this.closing) return | |
| 298 | ||
| 299 | if (!host) host = '127.0.0.1' | |
| 300 | ||
| 301 | const family = ip.isIP(host) | |
| 302 | if (!family) throw new Error(`${host} is not a valid IP address`) | |
| 303 | ||
| 304 | if (!this.bound) this.bind(0) | |
| 305 | ||
| 306 | const id = this._allocSend() | |
| 307 | const req = this._reqs[id] | |
| 308 | ||
| 309 | req.buffer = buffer | |
| 310 | req.onflush = noop | |
| 311 | ||
| 312 | binding.udx_napi_socket_send_ttl( | |
| 313 | this._handle, | |
| 314 | req.handle, | |
| 315 | id, | |
| 316 | buffer, | |
| 317 | port, | |
| 318 | host, | |
| 319 | family, | |
| 320 | ttl || 0 | |
| 321 | ) | |
| 322 | } | |
| 323 | ||
| 324 | _allocSend() { | |
| 325 | if (this._free.length > 0) return this._free.pop() | |
| 326 | const handle = b4a.allocUnsafe(binding.sizeof_udx_socket_send_t) | |
| 327 | return this._reqs.push({ handle, buffer: null, onflush: null }) - 1 | |
| 328 | } | |
| 329 | } | |
| 330 | ||
| 331 | function noop() {} |
| … | … | @@ -0,0 +1,545 @@ |
| 1 | const streamx = require('streamx') | |
| 2 | const b4a = require('b4a') | |
| 3 | const binding = require('../binding') | |
| 4 | const ip = require('./ip') | |
| 5 | ||
| 6 | const MAX_PACKET = 2048 | |
| 7 | const BUFFER_SIZE = 65536 + MAX_PACKET | |
| 8 | ||
| 9 | module.exports = class UDXStream extends streamx.Duplex { | |
| 10 | constructor(udx, id, opts = {}) { | |
| 11 | super({ mapWritable: toBuffer, eagerOpen: true }) | |
| 12 | ||
| 13 | this.udx = udx | |
| 14 | this.socket = null | |
| 15 | ||
| 16 | this._handle = b4a.alloc(binding.sizeof_udx_napi_stream_t) | |
| 17 | this._view = new Uint32Array( | |
| 18 | this._handle.buffer, | |
| 19 | this._handle.byteOffset, | |
| 20 | this._handle.byteLength >> 2 | |
| 21 | ) | |
| 22 | this._view16 = new Uint16Array( | |
| 23 | this._handle.buffer, | |
| 24 | this._handle.byteOffset, | |
| 25 | this._handle.byteLength >> 1 | |
| 26 | ) | |
| 27 | this._view64 = new BigUint64Array( | |
| 28 | this._handle.buffer, | |
| 29 | this._handle.byteOffset, | |
| 30 | this._handle.byteLength >> 3 | |
| 31 | ) | |
| 32 | ||
| 33 | this._wreqs = [] | |
| 34 | this._wfree = [] | |
| 35 | ||
| 36 | this._sreqs = [] | |
| 37 | this._sfree = [] | |
| 38 | this._closed = false | |
| 39 | ||
| 40 | this._flushing = 0 | |
| 41 | this._flushes = [] | |
| 42 | ||
| 43 | this._buffer = null | |
| 44 | this._reallocData() | |
| 45 | ||
| 46 | this._onwrite = null | |
| 47 | this._ondestroy = null | |
| 48 | this._firewall = opts.firewall || firewallAll | |
| 49 | ||
| 50 | this._remoteChanging = null | |
| 51 | this._previousSocket = null | |
| 52 | ||
| 53 | this.id = id | |
| 54 | this.remoteId = 0 | |
| 55 | this.remoteHost = null | |
| 56 | this.remoteFamily = 0 | |
| 57 | this.remotePort = 0 | |
| 58 | ||
| 59 | this.userData = null | |
| 60 | ||
| 61 | binding.udx_napi_stream_init( | |
| 62 | this.udx._handle, | |
| 63 | this._handle, | |
| 64 | id, | |
| 65 | opts.framed ? 1 : 0, | |
| 66 | this, | |
| 67 | this._ondata, | |
| 68 | this._onend, | |
| 69 | this._ondrain, | |
| 70 | this._onack, | |
| 71 | this._onsend, | |
| 72 | this._onmessage, | |
| 73 | this._onclose, | |
| 74 | this._onfirewall, | |
| 75 | this._onremotechanged, | |
| 76 | this._reallocData, | |
| 77 | this._reallocMessage | |
| 78 | ) | |
| 79 | ||
| 80 | if (opts.seq) binding.udx_napi_stream_set_seq(this._handle, opts.seq) | |
| 81 | ||
| 82 | binding.udx_napi_stream_recv_start(this._handle, this._buffer) | |
| 83 | } | |
| 84 | ||
| 85 | get connected() { | |
| 86 | return this.socket !== null | |
| 87 | } | |
| 88 | ||
| 89 | get mtu() { | |
| 90 | return this._view16[binding.offsetof_udx_stream_t_mtu >> 1] | |
| 91 | } | |
| 92 | ||
| 93 | get rtt() { | |
| 94 | return this._view[binding.offsetof_udx_stream_t_srtt >> 2] | |
| 95 | } | |
| 96 | ||
| 97 | get cwnd() { | |
| 98 | return this._view[binding.offsetof_udx_stream_t_cwnd >> 2] | |
| 99 | } | |
| 100 | ||
| 101 | get rtoCount() { | |
| 102 | return this._view16[binding.offsetof_udx_stream_t_rto_count >> 1] | |
| 103 | } | |
| 104 | ||
| 105 | get retransmits() { | |
| 106 | return this._view16[binding.offsetof_udx_stream_t_retransmit_count >> 1] | |
| 107 | } | |
| 108 | ||
| 109 | get fastRecoveries() { | |
| 110 | return this._view16[binding.offsetof_udx_stream_t_fast_recovery_count >> 1] | |
| 111 | } | |
| 112 | ||
| 113 | get inflight() { | |
| 114 | return this._view[binding.offsetof_udx_stream_t_inflight >> 2] | |
| 115 | } | |
| 116 | ||
| 117 | get bytesTransmitted() { | |
| 118 | return Number(this._view64[binding.offsetof_udx_stream_t_bytes_tx >> 3]) | |
| 119 | } | |
| 120 | ||
| 121 | get packetsTransmitted() { | |
| 122 | return Number(this._view64[binding.offsetof_udx_stream_t_packets_tx >> 3]) | |
| 123 | } | |
| 124 | ||
| 125 | get bytesReceived() { | |
| 126 | return Number(this._view64[binding.offsetof_udx_stream_t_bytes_rx >> 3]) | |
| 127 | } | |
| 128 | ||
| 129 | get packetsReceived() { | |
| 130 | return Number(this._view64[binding.offsetof_udx_stream_t_packets_rx >> 3]) | |
| 131 | } | |
| 132 | ||
| 133 | get localHost() { | |
| 134 | return this.socket ? this.socket.address().host : null | |
| 135 | } | |
| 136 | ||
| 137 | get localFamily() { | |
| 138 | return this.socket ? this.socket.address().family : 0 | |
| 139 | } | |
| 140 | ||
| 141 | get localPort() { | |
| 142 | return this.socket ? this.socket.address().port : 0 | |
| 143 | } | |
| 144 | ||
| 145 | setInteractive(bool) { | |
| 146 | if (!this._closed) return | |
| 147 | binding.udx_napi_stream_set_mode(this._handle, bool ? 0 : 1) | |
| 148 | } | |
| 149 | ||
| 150 | connect(socket, remoteId, port, host, opts = {}) { | |
| 151 | if (this._closed) return | |
| 152 | ||
| 153 | if (this.connected) throw new Error('Already connected') | |
| 154 | if (socket.closing) throw new Error('Socket is closed') | |
| 155 | ||
| 156 | if (typeof host === 'object') { | |
| 157 | opts = host | |
| 158 | host = null | |
| 159 | } | |
| 160 | ||
| 161 | if (!host) host = '127.0.0.1' | |
| 162 | ||
| 163 | const family = ip.isIP(host) | |
| 164 | if (!family) throw new Error(`${host} is not a valid IP address`) | |
| 165 | if (!(port > 0 && port < 65536)) throw new Error(`${port} is not a valid port`) | |
| 166 | ||
| 167 | if (!socket.bound) socket.bind(0) | |
| 168 | ||
| 169 | this.remoteId = remoteId | |
| 170 | this.remotePort = port | |
| 171 | this.remoteHost = host | |
| 172 | this.remoteFamily = family | |
| 173 | this.socket = socket | |
| 174 | ||
| 175 | if (opts.ack) binding.udx_napi_stream_set_ack(this._handle, opts.ack) | |
| 176 | ||
| 177 | binding.udx_napi_stream_connect(this._handle, socket._handle, remoteId, port, host, family) | |
| 178 | ||
| 179 | this.socket._addStream(this) | |
| 180 | ||
| 181 | this.emit('connect') | |
| 182 | } | |
| 183 | ||
| 184 | changeRemote(socket, remoteId, port, host) { | |
| 185 | if (this._remoteChanging) throw new Error('Remote already changing') | |
| 186 | ||
| 187 | if (!this.connected) throw new Error('Not yet connected') | |
| 188 | if (socket.closing) throw new Error('Socket is closed') | |
| 189 | ||
| 190 | if (this.socket.udx !== socket.udx) { | |
| 191 | throw new Error('Cannot change to a socket on another UDX instance') | |
| 192 | } | |
| 193 | ||
| 194 | if (!host) host = '127.0.0.1' | |
| 195 | ||
| 196 | const family = ip.isIP(host) | |
| 197 | if (!family) throw new Error(`${host} is not a valid IP address`) | |
| 198 | if (!(port > 0 && port < 65536)) throw new Error(`${port} is not a valid port`) | |
| 199 | ||
| 200 | if (this.socket !== socket) this._previousSocket = this.socket | |
| 201 | ||
| 202 | this.remoteId = remoteId | |
| 203 | this.remotePort = port | |
| 204 | this.remoteHost = host | |
| 205 | this.remoteFamily = family | |
| 206 | this.socket = socket | |
| 207 | ||
| 208 | this._remoteChanging = new Promise((resolve, reject) => { | |
| 209 | const onchanged = () => { | |
| 210 | this.off('close', onclose) | |
| 211 | resolve() | |
| 212 | } | |
| 213 | ||
| 214 | const onclose = () => { | |
| 215 | this.off('remote-changed', onchanged) | |
| 216 | reject(new Error('Stream is closed')) | |
| 217 | } | |
| 218 | ||
| 219 | this.once('remote-changed', onchanged).once('close', onclose) | |
| 220 | }) | |
| 221 | ||
| 222 | binding.udx_napi_stream_change_remote( | |
| 223 | this._handle, | |
| 224 | socket._handle, | |
| 225 | remoteId, | |
| 226 | port, | |
| 227 | host, | |
| 228 | family | |
| 229 | ) | |
| 230 | ||
| 231 | this.socket._addStream(this) | |
| 232 | ||
| 233 | return this._remoteChanging | |
| 234 | } | |
| 235 | ||
| 236 | relayTo(destination) { | |
| 237 | if (this._closed) return | |
| 238 | ||
| 239 | binding.udx_napi_stream_relay_to(this._handle, destination._handle) | |
| 240 | } | |
| 241 | ||
| 242 | async send(buffer) { | |
| 243 | if (!this.connected || this._closed) return false | |
| 244 | ||
| 245 | const id = this._allocSend() | |
| 246 | const req = this._sreqs[id] | |
| 247 | ||
| 248 | req.buffer = buffer | |
| 249 | ||
| 250 | const promise = new Promise((resolve) => { | |
| 251 | req.onflush = resolve | |
| 252 | }) | |
| 253 | ||
| 254 | binding.udx_napi_stream_send(this._handle, req.handle, id, buffer) | |
| 255 | ||
| 256 | return promise | |
| 257 | } | |
| 258 | ||
| 259 | trySend(buffer) { | |
| 260 | if (!this.connected || this._closed) return | |
| 261 | ||
| 262 | const id = this._allocSend() | |
| 263 | const req = this._sreqs[id] | |
| 264 | ||
| 265 | req.buffer = buffer | |
| 266 | req.onflush = noop | |
| 267 | ||
| 268 | binding.udx_napi_stream_send(this._handle, req.handle, id, buffer) | |
| 269 | } | |
| 270 | ||
| 271 | async flush() { | |
| 272 | if ((await streamx.Writable.drained(this)) === false) return false | |
| 273 | if (this.destroying) return false | |
| 274 | ||
| 275 | const missing = this._wreqs.length - this._wfree.length | |
| 276 | if (missing === 0) return true | |
| 277 | ||
| 278 | return new Promise((resolve) => { | |
| 279 | this._flushes.push({ flush: this._flushing++, missing, resolve }) | |
| 280 | }) | |
| 281 | } | |
| 282 | ||
| 283 | toJSON() { | |
| 284 | return { | |
| 285 | id: this.id, | |
| 286 | connected: this.connected, | |
| 287 | destroying: this.destroying, | |
| 288 | destroyed: this.destroyed, | |
| 289 | remoteId: this.remoteId, | |
| 290 | remoteHost: this.remoteHost, | |
| 291 | remoteFamily: this.remoteFamily, | |
| 292 | remotePort: this.remotePort, | |
| 293 | mtu: this.mtu, | |
| 294 | rtt: this.rtt, | |
| 295 | cwnd: this.cwnd, | |
| 296 | inflight: this.inflight, | |
| 297 | socket: this.socket ? this.socket.toJSON() : null | |
| 298 | } | |
| 299 | } | |
| 300 | ||
| 301 | _read(cb) { | |
| 302 | cb(null) | |
| 303 | } | |
| 304 | ||
| 305 | _writeContinue(err) { | |
| 306 | if (this._onwrite === null) return | |
| 307 | const cb = this._onwrite | |
| 308 | this._onwrite = null | |
| 309 | cb(err) | |
| 310 | } | |
| 311 | ||
| 312 | _destroyContinue(err) { | |
| 313 | if (this._ondestroy === null) return | |
| 314 | const cb = this._ondestroy | |
| 315 | this._ondestroy = null | |
| 316 | cb(err) | |
| 317 | } | |
| 318 | ||
| 319 | _writev(buffers, cb) { | |
| 320 | if (!this.connected) | |
| 321 | throw customError('Writing while not connected not currently supported', 'ERR_ASSERTION') | |
| 322 | ||
| 323 | let drained = true | |
| 324 | ||
| 325 | if (buffers.length === 1) { | |
| 326 | const id = this._allocWrite(1) | |
| 327 | const req = this._wreqs[id] | |
| 328 | ||
| 329 | req.flush = this._flushing | |
| 330 | req.buffer = buffers[0] | |
| 331 | ||
| 332 | drained = binding.udx_napi_stream_write(this._handle, req.handle, id, req.buffer) !== 0 | |
| 333 | } else { | |
| 334 | const id = this._allocWrite(nextBatchSize(buffers.length)) | |
| 335 | const req = this._wreqs[id] | |
| 336 | ||
| 337 | req.flush = this._flushing | |
| 338 | req.buffers = buffers | |
| 339 | ||
| 340 | drained = binding.udx_napi_stream_writev(this._handle, req.handle, id, req.buffers) !== 0 | |
| 341 | } | |
| 342 | ||
| 343 | if (drained) cb(null) | |
| 344 | else this._onwrite = cb | |
| 345 | } | |
| 346 | ||
| 347 | _final(cb) { | |
| 348 | const id = this._allocWrite(1) | |
| 349 | const req = this._wreqs[id] | |
| 350 | ||
| 351 | req.flush = this._flushes | |
| 352 | req.buffer = b4a.allocUnsafe(0) | |
| 353 | ||
| 354 | const drained = | |
| 355 | binding.udx_napi_stream_write_end(this._handle, req.handle, id, req.buffer) !== 0 | |
| 356 | ||
| 357 | if (drained) cb(null) | |
| 358 | else this._onwrite = cb | |
| 359 | } | |
| 360 | ||
| 361 | _predestroy() { | |
| 362 | if (!this._closed) binding.udx_napi_stream_destroy(this._handle) | |
| 363 | this._closed = true | |
| 364 | this._writeContinue(null) | |
| 365 | } | |
| 366 | ||
| 367 | _destroy(cb) { | |
| 368 | if (this.connected) this._ondestroy = cb | |
| 369 | else cb(null) | |
| 370 | } | |
| 371 | ||
| 372 | _ondata(read) { | |
| 373 | this.push(this._consumeData(read)) | |
| 374 | return this._buffer | |
| 375 | } | |
| 376 | ||
| 377 | _onend(read) { | |
| 378 | if (read > 0) this.push(this._consumeData(read)) | |
| 379 | this.push(null) | |
| 380 | } | |
| 381 | ||
| 382 | _ondrain() { | |
| 383 | this._writeContinue(null) | |
| 384 | } | |
| 385 | ||
| 386 | _flushAck(flush) { | |
| 387 | for (let i = this._flushes.length - 1; i >= 0; i--) { | |
| 388 | const f = this._flushes[i] | |
| 389 | if (f.flush < flush) break | |
| 390 | f.missing-- | |
| 391 | } | |
| 392 | ||
| 393 | while (this._flushes.length > 0 && this._flushes[0].missing === 0) { | |
| 394 | this._flushes.shift().resolve(true) | |
| 395 | } | |
| 396 | } | |
| 397 | ||
| 398 | _onack(id) { | |
| 399 | const req = this._wreqs[id] | |
| 400 | ||
| 401 | req.buffers = req.buffer = null | |
| 402 | this._wfree.push(id) | |
| 403 | ||
| 404 | if (this._flushes.length > 0) this._flushAck(req.flush) | |
| 405 | ||
| 406 | // gc the free list | |
| 407 | if (this._wfree.length >= 64 && this._wfree.length === this._wreqs.length) { | |
| 408 | this._wfree = [] | |
| 409 | this._wreqs = [] | |
| 410 | } | |
| 411 | } | |
| 412 | ||
| 413 | _onsend(id, err) { | |
| 414 | const req = this._sreqs[id] | |
| 415 | ||
| 416 | const onflush = req.onflush | |
| 417 | ||
| 418 | req.buffer = null | |
| 419 | req.onflush = null | |
| 420 | ||
| 421 | this._sfree.push(id) | |
| 422 | ||
| 423 | onflush(err >= 0) | |
| 424 | ||
| 425 | // gc the free list | |
| 426 | if (this._sfree.length >= 16 && this._sfree.length === this._sreqs.length) { | |
| 427 | this._sfree = [] | |
| 428 | this._sreqs = [] | |
| 429 | } | |
| 430 | } | |
| 431 | ||
| 432 | _onmessage(len) { | |
| 433 | this.emit('message', this.udx._consumeMessage(len)) | |
| 434 | return this.udx._buffer | |
| 435 | } | |
| 436 | ||
| 437 | _onclose(err) { | |
| 438 | this._closed = true | |
| 439 | ||
| 440 | if (this.socket) { | |
| 441 | this.socket._removeStream(this) | |
| 442 | this.socket = null | |
| 443 | } | |
| 444 | ||
| 445 | if (this._previousSocket) { | |
| 446 | this._previousSocket._removeStream(this) | |
| 447 | this._previousSocket = null | |
| 448 | } | |
| 449 | ||
| 450 | // no error, we don't need to do anything | |
| 451 | if (!err) return this._destroyContinue(null) | |
| 452 | ||
| 453 | if (this._ondestroy === null) this.destroy(err) | |
| 454 | else this._destroyContinue(err) | |
| 455 | } | |
| 456 | ||
| 457 | _onfirewall(socket, port, host, family) { | |
| 458 | return this._firewall(socket, port, host, family) ? 1 : 0 | |
| 459 | } | |
| 460 | ||
| 461 | _onremotechanged() { | |
| 462 | if (this._previousSocket) { | |
| 463 | this._previousSocket._removeStream(this) | |
| 464 | this._previousSocket = null | |
| 465 | } | |
| 466 | ||
| 467 | this._remoteChanging = null | |
| 468 | this.emit('remote-changed') | |
| 469 | } | |
| 470 | ||
| 471 | _consumeData(len) { | |
| 472 | const next = this._buffer.subarray(0, len) | |
| 473 | this._buffer = this._buffer.subarray(len) | |
| 474 | if (this._buffer.byteLength < MAX_PACKET) this._reallocData() | |
| 475 | return next | |
| 476 | } | |
| 477 | ||
| 478 | _reallocData() { | |
| 479 | this._buffer = b4a.allocUnsafe(BUFFER_SIZE) | |
| 480 | return this._buffer | |
| 481 | } | |
| 482 | ||
| 483 | _reallocMessage() { | |
| 484 | return this.udx._reallocMessage() | |
| 485 | } | |
| 486 | ||
| 487 | _allocWrite(size) { | |
| 488 | if (this._wfree.length === 0) { | |
| 489 | const handle = b4a.allocUnsafe(binding.udx_napi_stream_write_sizeof(size)) | |
| 490 | return ( | |
| 491 | this._wreqs.push({ | |
| 492 | handle, | |
| 493 | size, | |
| 494 | buffers: null, | |
| 495 | buffer: null, | |
| 496 | flush: 0 | |
| 497 | }) - 1 | |
| 498 | ) | |
| 499 | } | |
| 500 | ||
| 501 | const free = this._wfree.pop() | |
| 502 | if (size === 1) return free | |
| 503 | ||
| 504 | const next = this._wreqs[free] | |
| 505 | if (next.size < size) { | |
| 506 | next.handle = b4a.allocUnsafe(binding.udx_napi_stream_write_sizeof(size)) | |
| 507 | next.size = size | |
| 508 | } | |
| 509 | ||
| 510 | return free | |
| 511 | } | |
| 512 | ||
| 513 | _allocSend() { | |
| 514 | if (this._sfree.length > 0) return this._sfree.pop() | |
| 515 | const handle = b4a.allocUnsafe(binding.sizeof_udx_stream_send_t) | |
| 516 | return this._sreqs.push({ handle, buffer: null, resolve: null, reject: null }) - 1 | |
| 517 | } | |
| 518 | } | |
| 519 | ||
| 520 | function noop() {} | |
| 521 | ||
| 522 | function toBuffer(data) { | |
| 523 | return typeof data === 'string' ? b4a.from(data) : data | |
| 524 | } | |
| 525 | ||
| 526 | function firewallAll(socket, port, host) { | |
| 527 | return true | |
| 528 | } | |
| 529 | ||
| 530 | function customError(message, code) { | |
| 531 | const error = new Error(message) | |
| 532 | error.code = code | |
| 533 | return error | |
| 534 | } | |
| 535 | ||
| 536 | function nextBatchSize(n) { | |
| 537 | // try to coerce the the writevs into sameish size | |
| 538 | if (n === 1) return 1 | |
| 539 | // group all < 8 to the same size, low mem overhead but save some small allocs | |
| 540 | if (n < 8) return 8 | |
| 541 | if (n < 16) return 16 | |
| 542 | if (n < 32) return 32 | |
| 543 | if (n < 64) return 64 | |
| 544 | return n | |
| 545 | } |
| … | … | @@ -0,0 +1,127 @@ |
| 1 | const b4a = require('b4a') | |
| 2 | const binding = require('../binding') | |
| 3 | const ip = require('./ip') | |
| 4 | const Socket = require('./socket') | |
| 5 | const Stream = require('./stream') | |
| 6 | const NetworkInterfaces = require('./network-interfaces') | |
| 7 | ||
| 8 | const MAX_MESSAGE = 4096 | |
| 9 | const BUFFER_SIZE = 65536 + MAX_MESSAGE | |
| 10 | ||
| 11 | module.exports = class UDX { | |
| 12 | constructor() { | |
| 13 | this._handle = b4a.alloc(binding.sizeof_udx_napi_t) | |
| 14 | this._watchers = new Set() | |
| 15 | this._view64 = new BigUint64Array( | |
| 16 | this._handle.buffer, | |
| 17 | this._handle.byteOffset, | |
| 18 | this._handle.byteLength >> 3 | |
| 19 | ) | |
| 20 | ||
| 21 | this._buffer = null | |
| 22 | this._reallocMessage() | |
| 23 | ||
| 24 | binding.udx_napi_init(this._handle, this._buffer) | |
| 25 | } | |
| 26 | ||
| 27 | static isIPv4(host) { | |
| 28 | return ip.isIPv4(host) | |
| 29 | } | |
| 30 | ||
| 31 | static isIPv6(host) { | |
| 32 | return ip.isIPv6(host) | |
| 33 | } | |
| 34 | ||
| 35 | static isIP(host) { | |
| 36 | return ip.isIP(host) | |
| 37 | } | |
| 38 | ||
| 39 | get bytesTransmitted() { | |
| 40 | return Number(this._view64[binding.offsetof_udx_t_bytes_tx >> 3]) | |
| 41 | } | |
| 42 | ||
| 43 | get packetsTransmitted() { | |
| 44 | return Number(this._view64[binding.offsetof_udx_t_packets_tx >> 3]) | |
| 45 | } | |
| 46 | ||
| 47 | get bytesReceived() { | |
| 48 | return Number(this._view64[binding.offsetof_udx_t_bytes_rx >> 3]) | |
| 49 | } | |
| 50 | ||
| 51 | get packetsReceived() { | |
| 52 | return Number(this._view64[binding.offsetof_udx_t_packets_rx >> 3]) | |
| 53 | } | |
| 54 | ||
| 55 | get packetsDroppedByKernel() { | |
| 56 | return Number(this._view64[binding.offsetof_udx_t_packets_dropped_by_kernel >> 3]) | |
| 57 | } | |
| 58 | ||
| 59 | _consumeMessage(len) { | |
| 60 | const next = this._buffer.subarray(0, len) | |
| 61 | this._buffer = this._buffer.subarray(len) | |
| 62 | if (this._buffer.byteLength < MAX_MESSAGE) this._reallocMessage() | |
| 63 | return next | |
| 64 | } | |
| 65 | ||
| 66 | _reallocMessage() { | |
| 67 | // TODO: move reallocation to native | |
| 68 | this._buffer = b4a.allocUnsafe(BUFFER_SIZE) | |
| 69 | return this._buffer | |
| 70 | } | |
| 71 | ||
| 72 | createSocket(opts) { | |
| 73 | return new Socket(this, opts) | |
| 74 | } | |
| 75 | ||
| 76 | createStream(id, opts) { | |
| 77 | return new Stream(this, id, opts) | |
| 78 | } | |
| 79 | ||
| 80 | networkInterfaces() { | |
| 81 | let [watcher = null] = this._watchers | |
| 82 | if (watcher) return watcher.interfaces | |
| 83 | ||
| 84 | watcher = new NetworkInterfaces(this) | |
| 85 | watcher.destroy() | |
| 86 | ||
| 87 | return watcher.interfaces | |
| 88 | } | |
| 89 | ||
| 90 | watchNetworkInterfaces(onchange) { | |
| 91 | const watcher = new NetworkInterfaces(this) | |
| 92 | ||
| 93 | this._watchers.add(watcher) | |
| 94 | watcher.on('close', () => { | |
| 95 | this._watchers.delete(watcher) | |
| 96 | }) | |
| 97 | ||
| 98 | if (onchange) watcher.on('change', onchange) | |
| 99 | ||
| 100 | return watcher.watch() | |
| 101 | } | |
| 102 | ||
| 103 | async lookup(host, opts = {}) { | |
| 104 | const { family = 0 } = opts | |
| 105 | ||
| 106 | const req = b4a.alloc(binding.sizeof_udx_napi_lookup_t) | |
| 107 | const ctx = { | |
| 108 | req, | |
| 109 | resolve: null, | |
| 110 | reject: null | |
| 111 | } | |
| 112 | ||
| 113 | const promise = new Promise((resolve, reject) => { | |
| 114 | ctx.resolve = resolve | |
| 115 | ctx.reject = reject | |
| 116 | }) | |
| 117 | ||
| 118 | binding.udx_napi_lookup(this._handle, req, host, family, ctx, onlookup) | |
| 119 | ||
| 120 | return promise | |
| 121 | } | |
| 122 | } | |
| 123 | ||
| 124 | function onlookup(err, host, family) { | |
| 125 | if (err) this.reject(err) | |
| 126 | else this.resolve({ host, family }) | |
| 127 | } |
| … | … | @@ -0,0 +1,66 @@ |
| 1 | { | |
| 2 | "name": "udx-native", | |
| 3 | "version": "1.19.2", | |
| 4 | "description": "udx is reliable, multiplexed, and congestion-controlled streams over udp", | |
| 5 | "main": "lib/udx.js", | |
| 6 | "files": [ | |
| 7 | "lib", | |
| 8 | "prebuilds", | |
| 9 | "binding.cc", | |
| 10 | "binding.js", | |
| 11 | "CMakeLists.txt" | |
| 12 | ], | |
| 13 | "imports": { | |
| 14 | "events": { | |
| 15 | "bare": "bare-events", | |
| 16 | "default": "events" | |
| 17 | } | |
| 18 | }, | |
| 19 | "addon": true, | |
| 20 | "scripts": { | |
| 21 | "format": "prettier --write .", | |
| 22 | "lint": "prettier --check .", | |
| 23 | "test": "npm run lint && npm run test:bare && npm run test:node", | |
| 24 | "test:node": "node test/all.js", | |
| 25 | "test:bare": "bare test/all.js", | |
| 26 | "test:all": "brittle test/*.js test/slow/*.js", | |
| 27 | "test:generate": "brittle -r test/all.js test/*.js", | |
| 28 | "bench": "brittle test/bench/*.js" | |
| 29 | }, | |
| 30 | "repository": { | |
| 31 | "type": "git", | |
| 32 | "url": "git+https://github.com/holepunchto/udx-native.git" | |
| 33 | }, | |
| 34 | "keywords": [ | |
| 35 | "tcp", | |
| 36 | "udp", | |
| 37 | "stream", | |
| 38 | "reliable" | |
| 39 | ], | |
| 40 | "author": "Holepunch Inc.", | |
| 41 | "license": "Apache-2.0", | |
| 42 | "bugs": { | |
| 43 | "url": "https://github.com/holepunchto/udx-native/issues" | |
| 44 | }, | |
| 45 | "homepage": "https://github.com/holepunchto/udx-native#readme", | |
| 46 | "engines": { | |
| 47 | "bare": ">=1.17.4" | |
| 48 | }, | |
| 49 | "dependencies": { | |
| 50 | "b4a": "^1.5.0", | |
| 51 | "bare-events": "^2.2.0", | |
| 52 | "require-addon": "^1.1.0", | |
| 53 | "streamx": "^2.22.0" | |
| 54 | }, | |
| 55 | "devDependencies": { | |
| 56 | "bare-compat-napi": "^1.3.0", | |
| 57 | "brittle": "^3.1.0", | |
| 58 | "cmake-bare": "^1.1.10", | |
| 59 | "cmake-fetch": "^1.0.1", | |
| 60 | "cmake-napi": "^1.0.5", | |
| 61 | "is-ci": "^3.0.1", | |
| 62 | "prettier": "^3.6.2", | |
| 63 | "prettier-config-holepunch": "^2.0.0", | |
| 64 | "tiny-byte-size": "^1.1.0" | |
| 65 | } | |
| 66 | } |
| … | … | @@ -0,0 +1,9 @@ |
| 1 | sudo: false | |
| 2 | ||
| 3 | language: node_js | |
| 4 | ||
| 5 | node_js: | |
| 6 | - "5" | |
| 7 | - "4" | |
| 8 | - "0.12" | |
| 9 | - "0.10" |
| … | … | @@ -0,0 +1,21 @@ |
| 1 | The MIT License (MIT) | |
| 2 | ||
| 3 | Copyright (c) 2016 Mathias Buus | |
| 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 | |
| 13 | all 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 | |
| 21 | THE SOFTWARE. |
| … | … | @@ -0,0 +1,53 @@ |
| 1 | # unordered-set | |
| 2 | ||
| 3 | A couple of functions that make it easy to maintain an unordered set as an array in an efficient way | |
| 4 | ||
| 5 | ``` | |
| 6 | npm install unordered-set | |
| 7 | ``` | |
| 8 | ||
| 9 | [](https://travis-ci.org/mafintosh/unordered-set) | |
| 10 | ||
| 11 | ## Usage | |
| 12 | ||
| 13 | ``` js | |
| 14 | var set = require('unordered-set') | |
| 15 | var list = [] | |
| 16 | ||
| 17 | var a = {hello: 'world'} | |
| 18 | var b = {hej: 'verden'} | |
| 19 | ||
| 20 | set.add(list, a) | |
| 21 | set.add(list, b) | |
| 22 | ||
| 23 | console.log(list) // prints a and b | |
| 24 | ||
| 25 | set.remove(list, a) | |
| 26 | ||
| 27 | console.log(list) // prints b | |
| 28 | ``` | |
| 29 | ||
| 30 | Items are removed and added using the same technique as in [unordered-array-remove](https://github.com/mafintosh/unordered-array-remove) | |
| 31 | making the removal/additions run in O(1). | |
| 32 | ||
| 33 | ## API | |
| 34 | ||
| 35 | #### `set.add(list, item)` | |
| 36 | ||
| 37 | Add an item. Notes that this sets the property `._index` to a number. If you control the items you insert and this is performance critical it might be benefitial to set `item._index = 0` in the item constructor as v8 tends to like that. | |
| 38 | ||
| 39 | #### `set.remove(list, item)` | |
| 40 | ||
| 41 | Remove an item from the set. Might change the order of the list as well. | |
| 42 | ||
| 43 | #### `bool = set.has(list, item)` | |
| 44 | ||
| 45 | Returns `true` if the item is in the list and `false` otherwise | |
| 46 | ||
| 47 | #### `set.swap(list, a, b)` | |
| 48 | ||
| 49 | Swap the positions of two elements in the set | |
| 50 | ||
| 51 | ## License | |
| 52 | ||
| 53 | MIT |
| … | … | @@ -0,0 +1,36 @@ |
| 1 | exports.add = add | |
| 2 | exports.has = has | |
| 3 | exports.remove = remove | |
| 4 | exports.swap = swap | |
| 5 | ||
| 6 | function add (list, item) { | |
| 7 | if (has(list, item)) return item | |
| 8 | item._index = list.length | |
| 9 | list.push(item) | |
| 10 | return item | |
| 11 | } | |
| 12 | ||
| 13 | function has (list, item) { | |
| 14 | return item._index < list.length && list[item._index] === item | |
| 15 | } | |
| 16 | ||
| 17 | function remove (list, item) { | |
| 18 | if (!has(list, item)) return null | |
| 19 | ||
| 20 | var last = list.pop() | |
| 21 | if (last !== item) { | |
| 22 | list[item._index] = last | |
| 23 | last._index = item._index | |
| 24 | } | |
| 25 | ||
| 26 | return item | |
| 27 | } | |
| 28 | ||
| 29 | function swap (list, a, b) { | |
| 30 | if (!has(list, a) || !has(list, b)) return | |
| 31 | var tmp = a._index | |
| 32 | a._index = b._index | |
| 33 | list[a._index] = a | |
| 34 | b._index = tmp | |
| 35 | list[b._index] = b | |
| 36 | } |
| … | … | @@ -0,0 +1,24 @@ |
| 1 | { | |
| 2 | "name": "unordered-set", | |
| 3 | "version": "2.0.1", | |
| 4 | "description": "A couple of functions that make it easy to maintain an unordered set as an array in an efficient way", | |
| 5 | "main": "index.js", | |
| 6 | "dependencies": {}, | |
| 7 | "devDependencies": { | |
| 8 | "standard": "^6.0.4", | |
| 9 | "tape": "^4.4.0" | |
| 10 | }, | |
| 11 | "scripts": { | |
| 12 | "test": "standard && tape test.js" | |
| 13 | }, | |
| 14 | "repository": { | |
| 15 | "type": "git", | |
| 16 | "url": "https://github.com/mafintosh/unordered-set.git" | |
| 17 | }, | |
| 18 | "author": "Mathias Buus (@mafintosh)", | |
| 19 | "license": "MIT", | |
| 20 | "bugs": { | |
| 21 | "url": "https://github.com/mafintosh/unordered-set/issues" | |
| 22 | }, | |
| 23 | "homepage": "https://github.com/mafintosh/unordered-set" | |
| 24 | } |
| … | … | @@ -0,0 +1,126 @@ |
| 1 | var tape = require('tape') | |
| 2 | var set = require('./') | |
| 3 | ||
| 4 | tape('add', function (t) { | |
| 5 | var list = [] | |
| 6 | var a = {hello: 'world'} | |
| 7 | var b = {hello: 'verden'} | |
| 8 | ||
| 9 | set.add(list, a) | |
| 10 | set.add(list, b) | |
| 11 | ||
| 12 | t.same(list, [a, b]) | |
| 13 | t.end() | |
| 14 | }) | |
| 15 | ||
| 16 | tape('add twice', function (t) { | |
| 17 | var list = [] | |
| 18 | var a = {hello: 'world'} | |
| 19 | var b = {hello: 'verden'} | |
| 20 | ||
| 21 | set.add(list, a) | |
| 22 | set.add(list, b) | |
| 23 | set.add(list, b) | |
| 24 | ||
| 25 | t.same(list, [a, b]) | |
| 26 | t.end() | |
| 27 | }) | |
| 28 | ||
| 29 | tape('remove', function (t) { | |
| 30 | var list = [] | |
| 31 | var a = {hello: 'world'} | |
| 32 | var b = {hello: 'verden'} | |
| 33 | ||
| 34 | set.add(list, a) | |
| 35 | set.add(list, b) | |
| 36 | set.remove(list, a) | |
| 37 | ||
| 38 | t.same(list, [b]) | |
| 39 | t.end() | |
| 40 | }) | |
| 41 | ||
| 42 | tape('remove twice', function (t) { | |
| 43 | var list = [] | |
| 44 | var a = {hello: 'world'} | |
| 45 | var b = {hello: 'verden'} | |
| 46 | ||
| 47 | set.add(list, a) | |
| 48 | set.add(list, b) | |
| 49 | set.remove(list, a) | |
| 50 | set.remove(list, a) | |
| 51 | ||
| 52 | t.same(list, [b]) | |
| 53 | t.end() | |
| 54 | }) | |
| 55 | ||
| 56 | tape('remove all', function (t) { | |
| 57 | var list = [] | |
| 58 | var a = {hello: 'world'} | |
| 59 | var b = {hello: 'verden'} | |
| 60 | ||
| 61 | set.add(list, a) | |
| 62 | set.add(list, b) | |
| 63 | set.remove(list, a) | |
| 64 | set.remove(list, b) | |
| 65 | ||
| 66 | t.same(list, []) | |
| 67 | t.end() | |
| 68 | }) | |
| 69 | ||
| 70 | tape('re-add', function (t) { | |
| 71 | var list = [] | |
| 72 | var a = {hello: 'world'} | |
| 73 | var b = {hello: 'verden'} | |
| 74 | ||
| 75 | set.add(list, a) | |
| 76 | set.add(list, b) | |
| 77 | set.remove(list, a) | |
| 78 | set.remove(list, b) | |
| 79 | set.add(list, b) | |
| 80 | set.add(list, a) | |
| 81 | ||
| 82 | t.same(list, [b, a]) | |
| 83 | t.end() | |
| 84 | }) | |
| 85 | ||
| 86 | tape('has', function (t) { | |
| 87 | var list = [] | |
| 88 | var a = {hello: 'world'} | |
| 89 | var b = {hello: 'verden'} | |
| 90 | ||
| 91 | set.add(list, a) | |
| 92 | set.add(list, b) | |
| 93 | set.remove(list, a) | |
| 94 | ||
| 95 | t.ok(!set.has(list, a)) | |
| 96 | t.ok(set.has(list, b)) | |
| 97 | t.end() | |
| 98 | }) | |
| 99 | ||
| 100 | tape('swap', function (t) { | |
| 101 | var list = [] | |
| 102 | var a = {hello: 'world'} | |
| 103 | var b = {hello: 'verden'} | |
| 104 | ||
| 105 | set.add(list, a) | |
| 106 | set.add(list, b) | |
| 107 | ||
| 108 | set.swap(list, a, b) | |
| 109 | t.same(list, [b, a]) | |
| 110 | ||
| 111 | set.swap(list, a, b) | |
| 112 | t.same(list, [a, b]) | |
| 113 | ||
| 114 | t.end() | |
| 115 | }) | |
| 116 | ||
| 117 | tape('swap same', function (t) { | |
| 118 | var list = [] | |
| 119 | var a = {hello: 'world'} | |
| 120 | ||
| 121 | set.add(list, a) | |
| 122 | ||
| 123 | set.swap(list, a, a) | |
| 124 | t.same(list, [a]) | |
| 125 | t.end() | |
| 126 | }) |
| … | … | @@ -0,0 +1,445 @@ |
| 1 | # Changelog | |
| 2 | ||
| 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. | |
| 4 | ||
| 5 | ## [9.3.0](https://github.com/istanbuljs/v8-to-istanbul/compare/v9.2.0...v9.3.0) (2024-06-22) | |
| 6 | ||
| 7 | ||
| 8 | ### Features | |
| 9 | ||
| 10 | * respect node:coverage comments ([#252](https://github.com/istanbuljs/v8-to-istanbul/issues/252)) ([92d593e](https://github.com/istanbuljs/v8-to-istanbul/commit/92d593e6ad7ad38bfacb3c9d237667df5c452fcb)) | |
| 11 | ||
| 12 | ## [9.2.0](https://github.com/istanbuljs/v8-to-istanbul/compare/v9.1.3...v9.2.0) (2023-11-22) | |
| 13 | ||
| 14 | ||
| 15 | ### Features | |
| 16 | ||
| 17 | * support `/* v8 ignore` ignore hints ([#228](https://github.com/istanbuljs/v8-to-istanbul/issues/228)) ([f8757c0](https://github.com/istanbuljs/v8-to-istanbul/commit/f8757c0c25084a6c60c96a7d9699e67a6e358b14)) | |
| 18 | ||
| 19 | ## [9.1.3](https://github.com/istanbuljs/v8-to-istanbul/compare/v9.1.2...v9.1.3) (2023-10-05) | |
| 20 | ||
| 21 | ||
| 22 | ### Bug Fixes | |
| 23 | ||
| 24 | * no wrapperLength for empty coverage ([#210](https://github.com/istanbuljs/v8-to-istanbul/issues/210)) ([bde6de2](https://github.com/istanbuljs/v8-to-istanbul/commit/bde6de2f0c32c1b14e7eda850d2e6b65f8278ed5)) | |
| 25 | ||
| 26 | ## [9.1.2](https://github.com/istanbuljs/v8-to-istanbul/compare/v9.1.1...v9.1.2) (2023-10-04) | |
| 27 | ||
| 28 | ||
| 29 | ### Bug Fixes | |
| 30 | ||
| 31 | * upgrade `convert-source-map` ([#224](https://github.com/istanbuljs/v8-to-istanbul/issues/224)) ([d2cc490](https://github.com/istanbuljs/v8-to-istanbul/commit/d2cc49094ee4ed20a0df1a2e441b83e8dbb64c22)) | |
| 32 | ||
| 33 | ## [9.1.1](https://github.com/istanbuljs/v8-to-istanbul/compare/v9.1.0...v9.1.1) (2023-10-04) | |
| 34 | ||
| 35 | ||
| 36 | ### Bug Fixes | |
| 37 | ||
| 38 | * ignore hint to mark uncovered files statements and lines ([#218](https://github.com/istanbuljs/v8-to-istanbul/issues/218)) ([c425413](https://github.com/istanbuljs/v8-to-istanbul/commit/c425413a2811311c3bd732a819543f1240cf2e28)) | |
| 39 | ||
| 40 | ## [9.1.0](https://github.com/istanbuljs/v8-to-istanbul/compare/v9.0.1...v9.1.0) (2023-02-14) | |
| 41 | ||
| 42 | ||
| 43 | ### Features | |
| 44 | ||
| 45 | * ignore hint more now accepts more suffixes ([#203](https://github.com/istanbuljs/v8-to-istanbul/issues/203)) ([65e70d1](https://github.com/istanbuljs/v8-to-istanbul/commit/65e70d14fd9977dc987d7ae2f6085895e3bc3cdb)) | |
| 46 | ||
| 47 | ## [9.0.1](https://github.com/istanbuljs/v8-to-istanbul/compare/v9.0.0...v9.0.1) (2022-06-20) | |
| 48 | ||
| 49 | ||
| 50 | ### Bug Fixes | |
| 51 | ||
| 52 | * update `@jridgewell/trace-mapping` ([#194](https://github.com/istanbuljs/v8-to-istanbul/issues/194)) ([83d3ea2](https://github.com/istanbuljs/v8-to-istanbul/commit/83d3ea29648012fef3a5c379fa04d8bfc53f3fd2)) | |
| 53 | ||
| 54 | ## [9.0.0](https://github.com/istanbuljs/v8-to-istanbul/compare/v8.1.1...v9.0.0) (2022-04-20) | |
| 55 | ||
| 56 | ||
| 57 | ### ⚠ BREAKING CHANGES | |
| 58 | ||
| 59 | * migrate from source-map to TraceMap | |
| 60 | ||
| 61 | ### Bug Fixes | |
| 62 | ||
| 63 | * address issues with line selection for Node 10 ([12d01c6](https://github.com/istanbuljs/v8-to-istanbul/commit/12d01c6f1abc6d5a01a1a8cbdfaabed4b43cf05f)) | |
| 64 | ||
| 65 | ||
| 66 | ### Code Refactoring | |
| 67 | ||
| 68 | * migrate from source-map to TraceMap ([c39ac4c](https://github.com/istanbuljs/v8-to-istanbul/commit/c39ac4cb636f3f9f92ff4375f377414d2ff93c16)) | |
| 69 | ||
| 70 | ### [8.1.1](https://github.com/istanbuljs/v8-to-istanbul/compare/v8.1.0...v8.1.1) (2022-01-10) | |
| 71 | ||
| 72 | ||
| 73 | ### Bug Fixes | |
| 74 | ||
| 75 | * handle undefined sourcesContent and null sourcesContent entry ([6c2e2ec](https://github.com/istanbuljs/v8-to-istanbul/commit/6c2e2ecd2aece8b01543f75dfa203744f8a785b9)) | |
| 76 | * **perf:** optimize hit counting and source map performance ([3f83226](https://github.com/istanbuljs/v8-to-istanbul/commit/3f83226212e9fd26231bb313b36db4f2d0661970)), closes [#159](https://github.com/istanbuljs/v8-to-istanbul/issues/159) | |
| 77 | ||
| 78 | ## [8.1.0](https://www.github.com/istanbuljs/v8-to-istanbul/compare/v8.0.0...v8.1.0) (2021-09-27) | |
| 79 | ||
| 80 | ||
| 81 | ### Features | |
| 82 | ||
| 83 | * function to cleanup allocated resources after usage ([#161](https://www.github.com/istanbuljs/v8-to-istanbul/issues/161)) ([a3925e9](https://www.github.com/istanbuljs/v8-to-istanbul/commit/a3925e9951fa88daee0aae5a7d35546b10f063a8)) | |
| 84 | ||
| 85 | ## [8.0.0](https://www.github.com/istanbuljs/v8-to-istanbul/compare/v7.1.2...v8.0.0) (2021-06-03) | |
| 86 | ||
| 87 | ||
| 88 | ### ⚠ BREAKING CHANGES | |
| 89 | ||
| 90 | * minimum Node version now 10.12. | |
| 91 | ||
| 92 | ### Bug Fixes | |
| 93 | ||
| 94 | * address file URL path regression on Windows ([#146](https://www.github.com/istanbuljs/v8-to-istanbul/issues/146)) ([bb04c56](https://www.github.com/istanbuljs/v8-to-istanbul/commit/bb04c561bffe9802b7d2e7e91216aa1d9230490a)) | |
| 95 | ||
| 96 | ### [7.1.2](https://www.github.com/istanbuljs/v8-to-istanbul/compare/v7.1.1...v7.1.2) (2021-05-05) | |
| 97 | ||
| 98 | ||
| 99 | ### Bug Fixes | |
| 100 | ||
| 101 | * fix undefined line in branches and functions ([#139](https://www.github.com/istanbuljs/v8-to-istanbul/issues/139)) ([f5ed83d](https://www.github.com/istanbuljs/v8-to-istanbul/commit/f5ed83d185129db48e5c05c5225470ad114c7609)) | |
| 102 | ||
| 103 | ### [7.1.1](https://www.github.com/istanbuljs/v8-to-istanbul/compare/v7.1.0...v7.1.1) (2021-03-30) | |
| 104 | ||
| 105 | ||
| 106 | ### Bug Fixes | |
| 107 | ||
| 108 | * use original source path if no sources ([#135](https://www.github.com/istanbuljs/v8-to-istanbul/issues/135)) ([64b2c86](https://www.github.com/istanbuljs/v8-to-istanbul/commit/64b2c86099afe3ea9d484324902d4ec4c3965347)) | |
| 109 | ||
| 110 | ## [7.1.0](https://www.github.com/istanbuljs/v8-to-istanbul/compare/v7.0.0...v7.1.0) (2020-12-22) | |
| 111 | ||
| 112 | ||
| 113 | ### Features | |
| 114 | ||
| 115 | * support comment c8 ignore start/stop ([#130](https://www.github.com/istanbuljs/v8-to-istanbul/issues/130)) ([591126b](https://www.github.com/istanbuljs/v8-to-istanbul/commit/591126b102244465b27906cdb8cdb82df1f4e760)) | |
| 116 | ||
| 117 | ## [7.0.0](https://www.github.com/istanbuljs/v8-to-istanbul/compare/v6.0.1...v7.0.0) (2020-10-25) | |
| 118 | ||
| 119 | ||
| 120 | ### ⚠ BREAKING CHANGES | |
| 121 | ||
| 122 | * address off by one error processing branches (#127) | |
| 123 | ||
| 124 | ### Bug Fixes | |
| 125 | ||
| 126 | * address off by one error processing branches ([#127](https://www.github.com/istanbuljs/v8-to-istanbul/issues/127)) ([746390f](https://www.github.com/istanbuljs/v8-to-istanbul/commit/746390f871fb2d1c6ec9738892a605a9ea59af5c)) | |
| 127 | * drop special shebang handling ([#125](https://www.github.com/istanbuljs/v8-to-istanbul/issues/125)) ([0d3b57f](https://www.github.com/istanbuljs/v8-to-istanbul/commit/0d3b57f245003d934c0a824f1b6fe54dcebacdb7)) | |
| 128 | * shebang handling supported in Node v12 ([#128](https://www.github.com/istanbuljs/v8-to-istanbul/issues/128)) ([522e4c2](https://www.github.com/istanbuljs/v8-to-istanbul/commit/522e4c25e6693e31191b47fc5729b6aff9909ce3)) | |
| 129 | ||
| 130 | ### [6.0.1](https://www.github.com/istanbuljs/v8-to-istanbul/compare/v6.0.0...v6.0.1) (2020-10-08) | |
| 131 | ||
| 132 | ||
| 133 | ### Bug Fixes | |
| 134 | ||
| 135 | * **build:** use new relese-please strategy ([c8edd37](https://www.github.com/istanbuljs/v8-to-istanbul/commit/c8edd3741f803dd7485d01ee6f4fcf6a73570700)) | |
| 136 | * **source-maps:** reverts off by one fix for ignore ([#123](https://www.github.com/istanbuljs/v8-to-istanbul/issues/123)) ([a886fb8](https://www.github.com/istanbuljs/v8-to-istanbul/commit/a886fb82d7e2c5332c6e507e201a9378dda84f50)) | |
| 137 | ||
| 138 | ## [6.0.0](https://www.github.com/istanbuljs/v8-to-istanbul/compare/v5.0.1...v6.0.0) (2020-10-08) | |
| 139 | ||
| 140 | ||
| 141 | ### ⚠ BREAKING CHANGES | |
| 142 | ||
| 143 | * address off by one error processing branches (#118) | |
| 144 | ||
| 145 | ### Features | |
| 146 | ||
| 147 | * add support for 1:1 sourcesContent ([ac3c79a](https://www.github.com/istanbuljs/v8-to-istanbul/commit/ac3c79a688665e9f3f05aafe9295a3d71a21267d)) | |
| 148 | ||
| 149 | ||
| 150 | ### Bug Fixes | |
| 151 | ||
| 152 | * address off by one error processing branches ([#118](https://www.github.com/istanbuljs/v8-to-istanbul/issues/118)) ([abe51ea](https://www.github.com/istanbuljs/v8-to-istanbul/commit/abe51ea344171c827a014a8c86b3d3a2be5370ce)) | |
| 153 | * favor mapping at 0th column ([#120](https://www.github.com/istanbuljs/v8-to-istanbul/issues/120)) ([770f17f](https://www.github.com/istanbuljs/v8-to-istanbul/commit/770f17f49e2bf323bdc26f55f91adfedcbb94e25)) | |
| 154 | ||
| 155 | ### [5.0.1](https://www.github.com/istanbuljs/v8-to-istanbul/compare/v5.0.0...v5.0.1) (2020-08-07) | |
| 156 | ||
| 157 | ||
| 158 | ### Bug Fixes | |
| 159 | ||
| 160 | * add missing type in TS definition ([#113](https://www.github.com/istanbuljs/v8-to-istanbul/issues/113)) ([a14ebc2](https://www.github.com/istanbuljs/v8-to-istanbul/commit/a14ebc2f3c71cb854dacb06490ea3e0f9bc17dbd)) | |
| 161 | ||
| 162 | ## [5.0.0](https://www.github.com/istanbuljs/v8-to-istanbul/compare/v4.1.4...v5.0.0) (2020-08-02) | |
| 163 | ||
| 164 | ||
| 165 | ### ⚠ BREAKING CHANGES | |
| 166 | ||
| 167 | * drop Node 8 support (#110) | |
| 168 | * source map files with multiple sources will now be parsed differently than source map files with a single source. | |
| 169 | ||
| 170 | ### Features | |
| 171 | ||
| 172 | * support source map with multiple sources ([#102](https://www.github.com/istanbuljs/v8-to-istanbul/issues/102)) ([d1f435c](https://www.github.com/istanbuljs/v8-to-istanbul/commit/d1f435cf183c510188ec5e43781d5bc10989a4e0)), closes [#21](https://www.github.com/istanbuljs/v8-to-istanbul/issues/21) | |
| 173 | ||
| 174 | ||
| 175 | ### Bug Fixes | |
| 176 | ||
| 177 | * address path related bugs with 1:many source maps ([#108](https://www.github.com/istanbuljs/v8-to-istanbul/issues/108)) ([9a618bc](https://www.github.com/istanbuljs/v8-to-istanbul/commit/9a618bc374ef6e2205c26c5ebf728bc69f4a9288)) | |
| 178 | ||
| 179 | ||
| 180 | ### Build System | |
| 181 | ||
| 182 | * drop Node 8 support ([#110](https://www.github.com/istanbuljs/v8-to-istanbul/issues/110)) ([c8bf7a1](https://www.github.com/istanbuljs/v8-to-istanbul/commit/c8bf7a1bdcf8b911943bb1ffc97e401d979aa11f)) | |
| 183 | ||
| 184 | ### [4.1.4](https://www.github.com/istanbuljs/v8-to-istanbul/compare/v4.1.3...v4.1.4) (2020-05-06) | |
| 185 | ||
| 186 | ||
| 187 | ### Bug Fixes | |
| 188 | ||
| 189 | * handle relative sourceRoots in source map files ([#100](https://www.github.com/istanbuljs/v8-to-istanbul/issues/100)) ([16ad3aa](https://www.github.com/istanbuljs/v8-to-istanbul/commit/16ad3aabd6144e2bf15fb4065e697bf40d1caaf4)) | |
| 190 | ||
| 191 | ### [4.1.3](https://www.github.com/istanbuljs/v8-to-istanbul/compare/v4.1.2...v4.1.3) (2020-03-27) | |
| 192 | ||
| 193 | ||
| 194 | ### Bug Fixes | |
| 195 | ||
| 196 | * handle sourcemap `sources` emtpy edge case ([#94](https://www.github.com/istanbuljs/v8-to-istanbul/issues/94)) ([628af48](https://www.github.com/istanbuljs/v8-to-istanbul/commit/628af48e2f7ab279c52f4355c0ccb92ac16b2c1a)) | |
| 197 | * v8 coverage ranges that fall on \n characters cause exceptions ([#96](https://www.github.com/istanbuljs/v8-to-istanbul/issues/96)) ([c5731a3](https://www.github.com/istanbuljs/v8-to-istanbul/commit/c5731a3b2fe4dccfae9ee581a5bf7a6e362f5cb8)) | |
| 198 | ||
| 199 | ### [4.1.2](https://www.github.com/istanbuljs/v8-to-istanbul/compare/v4.1.1...v4.1.2) (2020-02-09) | |
| 200 | ||
| 201 | ||
| 202 | ### Bug Fixes | |
| 203 | ||
| 204 | * protect against undefined sourcesContent ([#89](https://www.github.com/istanbuljs/v8-to-istanbul/issues/89)) ([5b94fe3](https://www.github.com/istanbuljs/v8-to-istanbul/commit/5b94fe37f9b86686386ad01f1fb8a42182f35ad8)) | |
| 205 | ||
| 206 | ### [4.1.1](https://www.github.com/istanbuljs/v8-to-istanbul/compare/v4.1.0...v4.1.1) (2020-02-07) | |
| 207 | ||
| 208 | ||
| 209 | ### Bug Fixes | |
| 210 | ||
| 211 | * **build:** repository field should have type ([#87](https://www.github.com/istanbuljs/v8-to-istanbul/issues/87)) ([f064542](https://www.github.com/istanbuljs/v8-to-istanbul/commit/f064542844c333d83fc25e0ad7f989f0999f8ceb)) | |
| 212 | ||
| 213 | ## [4.1.0](https://www.github.com/istanbuljs/v8-to-istanbul/compare/v4.0.1...v4.1.0) (2020-02-06) | |
| 214 | ||
| 215 | ||
| 216 | ### Features | |
| 217 | ||
| 218 | * use the inline source content if available ([#85](https://www.github.com/istanbuljs/v8-to-istanbul/issues/85)) ([1a6d47f](https://www.github.com/istanbuljs/v8-to-istanbul/commit/1a6d47f1412d7c2b072fe794b6bd08bfbf4bbd55)) | |
| 219 | ||
| 220 | ### [4.0.1](https://www.github.com/istanbuljs/v8-to-istanbul/compare/v4.0.0...v4.0.1) (2019-12-12) | |
| 221 | ||
| 222 | ||
| 223 | ### Bug Fixes | |
| 224 | ||
| 225 | * loosen engine requirement so it can be installed on node 8 ([#82](https://www.github.com/istanbuljs/v8-to-istanbul/issues/82)) ([18f2587](https://www.github.com/istanbuljs/v8-to-istanbul/commit/18f2587481617e4db5ae1565c4f7052a8314af92)) | |
| 226 | ||
| 227 | ## [4.0.0](https://www.github.com/istanbuljs/v8-to-istanbul/compare/v3.2.6...v4.0.0) (2019-11-23) | |
| 228 | ||
| 229 | ||
| 230 | ### ⚠ BREAKING CHANGES | |
| 231 | ||
| 232 | * paths are now consistently absolute. | |
| 233 | ||
| 234 | ### Features | |
| 235 | ||
| 236 | * adds special (empty-report) block ([#74](https://www.github.com/istanbuljs/v8-to-istanbul/issues/74)) ([e981cc1](https://www.github.com/istanbuljs/v8-to-istanbul/commit/e981cc156b447ce7a936114dafac591126fd65dd)) | |
| 237 | ||
| 238 | ||
| 239 | ### Bug Fixes | |
| 240 | ||
| 241 | * consistently resolve paths to absolute form ([#72](https://www.github.com/istanbuljs/v8-to-istanbul/issues/72)) ([55f4116](https://www.github.com/istanbuljs/v8-to-istanbul/commit/55f411624841f89f8309dc460387f8dd15c8b8f4)) | |
| 242 | ||
| 243 | ### [3.2.6](https://github.com/bcoe/v8-to-istanbul/compare/v3.2.5...v3.2.6) (2019-10-24) | |
| 244 | ||
| 245 | ||
| 246 | ### Bug Fixes | |
| 247 | ||
| 248 | * remove scheme from paths before joining ([#69](https://github.com/bcoe/v8-to-istanbul/issues/69)) ([10612fa](https://github.com/bcoe/v8-to-istanbul/commit/10612fa)) | |
| 249 | ||
| 250 | ### [3.2.5](https://github.com/bcoe/v8-to-istanbul/compare/v3.2.4...v3.2.5) (2019-10-07) | |
| 251 | ||
| 252 | ||
| 253 | ### Bug Fixes | |
| 254 | ||
| 255 | * fs.promises was not introduced until 10 ([#67](https://github.com/bcoe/v8-to-istanbul/issues/67)) ([cdcc225](https://github.com/bcoe/v8-to-istanbul/commit/cdcc225)) | |
| 256 | ||
| 257 | ### [3.2.4](https://github.com/bcoe/v8-to-istanbul/compare/v3.2.3...v3.2.4) (2019-10-06) | |
| 258 | ||
| 259 | ### [3.2.3](https://github.com/bcoe/v8-to-istanbul/compare/v3.2.2...v3.2.3) (2019-06-24) | |
| 260 | ||
| 261 | ||
| 262 | ### Bug Fixes | |
| 263 | ||
| 264 | * regex for detecting Node < 10.16 was off ([4ca7220](https://github.com/bcoe/v8-to-istanbul/commit/4ca7220)) | |
| 265 | ||
| 266 | ||
| 267 | ||
| 268 | ### [3.2.2](https://github.com/bcoe/v8-to-istanbul/compare/v3.2.1...v3.2.2) (2019-06-24) | |
| 269 | ||
| 270 | ||
| 271 | ### Bug Fixes | |
| 272 | ||
| 273 | * Node >10.16.0 now uses new module wrap API ([7d7c9cb](https://github.com/bcoe/v8-to-istanbul/commit/7d7c9cb)) | |
| 274 | ||
| 275 | ||
| 276 | ||
| 277 | ### [3.2.1](https://github.com/bcoe/v8-to-istanbul/compare/v3.2.0...v3.2.1) (2019-06-23) | |
| 278 | ||
| 279 | ||
| 280 | ### Bug Fixes | |
| 281 | ||
| 282 | * logic for handling sourceRoot did not take into account process.cwd() ([#39](https://github.com/bcoe/v8-to-istanbul/issues/39)) ([6ed9524](https://github.com/bcoe/v8-to-istanbul/commit/6ed9524)) | |
| 283 | ||
| 284 | ||
| 285 | ||
| 286 | ## [3.2.0](https://github.com/bcoe/v8-to-istanbul/compare/v3.1.3...v3.2.0) (2019-06-23) | |
| 287 | ||
| 288 | ||
| 289 | ### Build System | |
| 290 | ||
| 291 | * update testing matrix and deps ([#34](https://github.com/bcoe/v8-to-istanbul/issues/34)) ([204afca](https://github.com/bcoe/v8-to-istanbul/commit/204afca)) | |
| 292 | ||
| 293 | ||
| 294 | ### Features | |
| 295 | ||
| 296 | * add a sources option allowing to bypass fs operations ([#36](https://github.com/bcoe/v8-to-istanbul/issues/36)) ([4f5a681](https://github.com/bcoe/v8-to-istanbul/commit/4f5a681)) | |
| 297 | * add TS typings ([#35](https://github.com/bcoe/v8-to-istanbul/issues/35)) ([5251108](https://github.com/bcoe/v8-to-istanbul/commit/5251108)) | |
| 298 | * allow sourceMaps with sourceRoot ([#32](https://github.com/bcoe/v8-to-istanbul/issues/32)) ([8eb2ed0](https://github.com/bcoe/v8-to-istanbul/commit/8eb2ed0)) | |
| 299 | ||
| 300 | ||
| 301 | ||
| 302 | ### [3.1.3](https://github.com/bcoe/v8-to-istanbul/compare/v3.1.2...v3.1.3) (2019-05-11) | |
| 303 | ||
| 304 | ||
| 305 | ### Bug Fixes | |
| 306 | ||
| 307 | * **deps:** source-map should be dependency not dev-dependency ([3f6208e](https://github.com/bcoe/v8-to-istanbul/commit/3f6208e)) | |
| 308 | ||
| 309 | ||
| 310 | ||
| 311 | ## [3.1.2](https://github.com/bcoe/v8-to-istanbul/compare/v3.1.1...v3.1.2) (2019-05-02) | |
| 312 | ||
| 313 | ||
| 314 | ### Bug Fixes | |
| 315 | ||
| 316 | * the line with the ignore comment itself should be skipped ([#25](https://github.com/bcoe/v8-to-istanbul/issues/25)) ([e939594](https://github.com/bcoe/v8-to-istanbul/commit/e939594)) | |
| 317 | ||
| 318 | ||
| 319 | ||
| 320 | ## [3.1.1](https://github.com/bcoe/v8-to-istanbul/compare/v3.1.0...v3.1.1) (2019-05-02) | |
| 321 | ||
| 322 | ||
| 323 | ### Bug Fixes | |
| 324 | ||
| 325 | * we should ignore functions and branches ([#24](https://github.com/bcoe/v8-to-istanbul/issues/24)) ([d468559](https://github.com/bcoe/v8-to-istanbul/commit/d468559)) | |
| 326 | ||
| 327 | ||
| 328 | ||
| 329 | # [3.1.0](https://github.com/bcoe/v8-to-istanbul/compare/v3.0.1...v3.1.0) (2019-05-02) | |
| 330 | ||
| 331 | ||
| 332 | ### Features | |
| 333 | ||
| 334 | * allow uncovered lines to be ignored with special comment ([#23](https://github.com/bcoe/v8-to-istanbul/issues/23)) ([f585cfa](https://github.com/bcoe/v8-to-istanbul/commit/f585cfa)) | |
| 335 | ||
| 336 | ||
| 337 | ||
| 338 | ## [3.0.1](https://github.com/bcoe/v8-to-istanbul/compare/v3.0.0...v3.0.1) (2019-05-01) | |
| 339 | ||
| 340 | ||
| 341 | ### Bug Fixes | |
| 342 | ||
| 343 | * initial column could be 0 on Node 10, after wrapper taken into account ([#22](https://github.com/bcoe/v8-to-istanbul/issues/22)) ([aa3f73b](https://github.com/bcoe/v8-to-istanbul/commit/aa3f73b)) | |
| 344 | ||
| 345 | ||
| 346 | ||
| 347 | # [3.0.0](https://github.com/bcoe/v8-to-istanbul/compare/v2.0.2...v3.0.0) (2019-04-29) | |
| 348 | ||
| 349 | ### Features | |
| 350 | ||
| 351 | * initial support for source-maps ([#19](https://github.com/bcoe/v8-to-istanbul/issues/19)) ([ab0fcdd](https://github.com/bcoe/v8-to-istanbul/commit/ab0fcdd)) | |
| 352 | ||
| 353 | ### BREAKING CHANGES | |
| 354 | ||
| 355 | * v8-to-istanbul is now async, making it possible to use the latest source-map library | |
| 356 | ||
| 357 | ||
| 358 | # [2.1.0](https://github.com/bcoe/v8-to-istanbul/compare/v2.0.5...v2.1.0) (2019-04-21) | |
| 359 | ||
| 360 | ||
| 361 | ### Features | |
| 362 | ||
| 363 | * store source so that it can be used by SourceMaps ([#18](https://github.com/bcoe/v8-to-istanbul/issues/18)) ([5afafd6](https://github.com/bcoe/v8-to-istanbul/commit/5afafd6)) | |
| 364 | ||
| 365 | ||
| 366 | ||
| 367 | ## [2.0.5](https://github.com/bcoe/v8-to-istanbul/compare/v2.0.4...v2.0.5) (2019-04-18) | |
| 368 | ||
| 369 | ||
| 370 | ### Bug Fixes | |
| 371 | ||
| 372 | * don't assume files to have CR characters on Windows ([#16](https://github.com/bcoe/v8-to-istanbul/issues/16)) ([c59a21a](https://github.com/bcoe/v8-to-istanbul/commit/c59a21a)) | |
| 373 | ||
| 374 | ||
| 375 | ||
| 376 | ## [2.0.4](https://github.com/bcoe/v8-to-istanbul/compare/v2.0.3...v2.0.4) (2019-04-07) | |
| 377 | ||
| 378 | ||
| 379 | ### Bug Fixes | |
| 380 | ||
| 381 | * Node 11 no longer wraps scripts by default ([#15](https://github.com/bcoe/v8-to-istanbul/issues/15)) ([fbbd113](https://github.com/bcoe/v8-to-istanbul/commit/fbbd113)) | |
| 382 | ||
| 383 | ||
| 384 | ||
| 385 | ## [2.0.3](https://github.com/bcoe/v8-to-istanbul/compare/v2.0.2...v2.0.3) (2019-04-07) | |
| 386 | ||
| 387 | ||
| 388 | ||
| 389 | <a name="2.0.2"></a> | |
| 390 | ## [2.0.2](https://github.com/bcoe/v8-to-istanbul/compare/v2.0.1...v2.0.2) (2019-01-20) | |
| 391 | ||
| 392 | ||
| 393 | ### Bug Fixes | |
| 394 | ||
| 395 | * windows has \r\n line separator ([#11](https://github.com/bcoe/v8-to-istanbul/issues/11)) ([c10b888](https://github.com/bcoe/v8-to-istanbul/commit/c10b888)) | |
| 396 | ||
| 397 | ||
| 398 | ||
| 399 | <a name="2.0.1"></a> | |
| 400 | ## [2.0.1](https://github.com/bcoe/v8-to-istanbul/compare/v2.0.0...v2.0.1) (2019-01-20) | |
| 401 | ||
| 402 | ||
| 403 | ### Bug Fixes | |
| 404 | ||
| 405 | * functions were not always counted ([#10](https://github.com/bcoe/v8-to-istanbul/issues/10)) ([464a1f0](https://github.com/bcoe/v8-to-istanbul/commit/464a1f0)) | |
| 406 | ||
| 407 | ||
| 408 | ||
| 409 | <a name="2.0.0"></a> | |
| 410 | # [2.0.0](https://github.com/bcoe/v8-to-istanbul/compare/v1.2.1...v2.0.0) (2018-12-21) | |
| 411 | ||
| 412 | ||
| 413 | ### Features | |
| 414 | ||
| 415 | * allow wrapper length to be configured ([#9](https://github.com/bcoe/v8-to-istanbul/issues/9)) ([5e76198](https://github.com/bcoe/v8-to-istanbul/commit/5e76198)) | |
| 416 | ||
| 417 | ||
| 418 | ### BREAKING CHANGES | |
| 419 | ||
| 420 | * we no longer attempt to detect ESM modules, rather the consumer sets a wrapper length | |
| 421 | ||
| 422 | ||
| 423 | ||
| 424 | <a name="1.2.1"></a> | |
| 425 | ## [1.2.1](https://github.com/bcoe/v8-to-istanbul/compare/v1.2.0...v1.2.1) (2018-09-12) | |
| 426 | ||
| 427 | ||
| 428 | ||
| 429 | <a name="1.2.0"></a> | |
| 430 | # [1.2.0](https://github.com/bcoe/v8-to-istanbul/compare/v1.1.0...v1.2.0) (2017-12-05) | |
| 431 | ||
| 432 | ||
| 433 | ### Features | |
| 434 | ||
| 435 | * support ESM modules ([#3](https://github.com/bcoe/v8-to-istanbul/issues/3)) ([992d13a](https://github.com/bcoe/v8-to-istanbul/commit/992d13a)) | |
| 436 | ||
| 437 | ||
| 438 | ||
| 439 | <a name="1.1.0"></a> | |
| 440 | # 1.1.0 (2017-12-01) | |
| 441 | ||
| 442 | ||
| 443 | ### Features | |
| 444 | ||
| 445 | * initial implementation ([6140c6c](https://github.com/bcoe/v8-to-istanbul/commit/6140c6c)) |
| … | … | @@ -0,0 +1,14 @@ |
| 1 | Copyright (c) 2017, Contributors | |
| 2 | ||
| 3 | Permission to use, copy, modify, and/or distribute this software | |
| 4 | for any purpose with or without fee is hereby granted, provided | |
| 5 | that the above copyright notice and this permission notice | |
| 6 | appear in all copies. | |
| 7 | ||
| 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES | |
| 9 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES | |
| 10 | OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE | |
| 11 | LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES | |
| 12 | OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, | |
| 13 | WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, | |
| 14 | ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. |
| … | … | @@ -0,0 +1,88 @@ |
| 1 | # v8-to-istanbul | |
| 2 | ||
| 3 | [](https://github.com/istanbuljs/v8-to-istanbul/actions) | |
| 4 | [](https://conventionalcommits.org) | |
| 5 |  | |
| 6 | ||
| 7 | converts from v8 coverage format to [istanbul's coverage format](https://github.com/gotwarlost/istanbul/blob/master/coverage.json.md). | |
| 8 | ||
| 9 | ## Usage | |
| 10 | ||
| 11 | ```js | |
| 12 | const v8toIstanbul = require('v8-to-istanbul') | |
| 13 | // the path to the original source-file is required, as its contents are | |
| 14 | // used during the conversion algorithm. | |
| 15 | const converter = v8toIstanbul('./path-to-instrumented-file.js') | |
| 16 | await converter.load() // this is required due to async file reading. | |
| 17 | // provide an array of coverage information in v8 format. | |
| 18 | converter.applyCoverage([ | |
| 19 | { | |
| 20 | "functionName": "", | |
| 21 | "ranges": [ | |
| 22 | { | |
| 23 | "startOffset": 0, | |
| 24 | "endOffset": 520, | |
| 25 | "count": 1 | |
| 26 | } | |
| 27 | ], | |
| 28 | "isBlockCoverage": true | |
| 29 | }, | |
| 30 | // ... | |
| 31 | ]) | |
| 32 | // output coverage information in a form that can | |
| 33 | // be consumed by Istanbul. | |
| 34 | console.info(JSON.stringify(converter.toIstanbul())) | |
| 35 | ``` | |
| 36 | ||
| 37 | ## Ignoring Uncovered Lines | |
| 38 | ||
| 39 | Sometimes you might find yourself wanting to ignore uncovered lines | |
| 40 | in your application (for example, perhaps you run your tests in Linux, but | |
| 41 | there's code that only executes on Windows). | |
| 42 | ||
| 43 | To ignore lines, use the special comment `/* v8 ignore next */`. | |
| 44 | ||
| 45 | **NOTE**: Before version `9.2.0` the ignore hint had to contain `c8` keyword, e.g. `/* c8 ignore ...`. | |
| 46 | ||
| 47 | ### ignoring the next line | |
| 48 | ||
| 49 | ```js | |
| 50 | const myVariable = 99 | |
| 51 | /* v8 ignore next */ | |
| 52 | if (process.platform === 'win32') console.info('hello world') | |
| 53 | ``` | |
| 54 | ||
| 55 | ### ignoring the next N lines | |
| 56 | ||
| 57 | ```js | |
| 58 | const myVariable = 99 | |
| 59 | /* v8 ignore next 3 */ | |
| 60 | if (process.platform === 'win32') { | |
| 61 | console.info('hello world') | |
| 62 | } | |
| 63 | ``` | |
| 64 | ||
| 65 | ### ignoring all lines until told | |
| 66 | ||
| 67 | ```js | |
| 68 | /* v8 ignore start */ | |
| 69 | function dontMindMe() { | |
| 70 | // ... | |
| 71 | } | |
| 72 | /* v8 ignore stop */ | |
| 73 | ``` | |
| 74 | ||
| 75 | ### ignoring the same line as the comment | |
| 76 | ||
| 77 | ```js | |
| 78 | const myVariable = 99 | |
| 79 | const os = process.platform === 'darwin' ? 'OSXy' /* v8 ignore next */ : 'Windowsy' | |
| 80 | ``` | |
| 81 | ||
| 82 | ## Testing | |
| 83 | ||
| 84 | To execute tests, simply run: | |
| 85 | ||
| 86 | ```bash | |
| 87 | npm test | |
| 88 | ``` |
| … | … | @@ -0,0 +1,25 @@ |
| 1 | /// <reference types="node" /> | |
| 2 | ||
| 3 | import { Profiler } from 'inspector' | |
| 4 | import { CoverageMapData } from 'istanbul-lib-coverage' | |
| 5 | import { SourceMapInput } from '@jridgewell/trace-mapping' | |
| 6 | ||
| 7 | declare type Sources = | |
| 8 | | { | |
| 9 | source: string | |
| 10 | } | |
| 11 | | { | |
| 12 | source: string | |
| 13 | originalSource: string | |
| 14 | sourceMap: { sourcemap: SourceMapInput } | |
| 15 | } | |
| 16 | declare class V8ToIstanbul { | |
| 17 | load(): Promise<void> | |
| 18 | destroy(): void | |
| 19 | applyCoverage(blocks: ReadonlyArray<Profiler.FunctionCoverage>): void | |
| 20 | toIstanbul(): CoverageMapData | |
| 21 | } | |
| 22 | ||
| 23 | declare function v8ToIstanbul(scriptPath: string, wrapperLength?: number, sources?: Sources, excludePath?: (path: string) => boolean): V8ToIstanbul | |
| 24 | ||
| 25 | export = v8ToIstanbul |
| … | … | @@ -0,0 +1,5 @@ |
| 1 | const V8ToIstanbul = require('./lib/v8-to-istanbul') | |
| 2 | ||
| 3 | module.exports = function (path, wrapperLength, sources, excludePath) { | |
| 4 | return new V8ToIstanbul(path, wrapperLength, sources, excludePath) | |
| 5 | } |
| … | … | @@ -0,0 +1,28 @@ |
| 1 | module.exports = class CovBranch { | |
| 2 | constructor (startLine, startCol, endLine, endCol, count) { | |
| 3 | this.startLine = startLine | |
| 4 | this.startCol = startCol | |
| 5 | this.endLine = endLine | |
| 6 | this.endCol = endCol | |
| 7 | this.count = count | |
| 8 | } | |
| 9 | ||
| 10 | toIstanbul () { | |
| 11 | const location = { | |
| 12 | start: { | |
| 13 | line: this.startLine, | |
| 14 | column: this.startCol | |
| 15 | }, | |
| 16 | end: { | |
| 17 | line: this.endLine, | |
| 18 | column: this.endCol | |
| 19 | } | |
| 20 | } | |
| 21 | return { | |
| 22 | type: 'branch', | |
| 23 | line: this.startLine, | |
| 24 | loc: location, | |
| 25 | locations: [Object.assign({}, location)] | |
| 26 | } | |
| 27 | } | |
| 28 | } |
| … | … | @@ -0,0 +1,29 @@ |
| 1 | module.exports = class CovFunction { | |
| 2 | constructor (name, startLine, startCol, endLine, endCol, count) { | |
| 3 | this.name = name | |
| 4 | this.startLine = startLine | |
| 5 | this.startCol = startCol | |
| 6 | this.endLine = endLine | |
| 7 | this.endCol = endCol | |
| 8 | this.count = count | |
| 9 | } | |
| 10 | ||
| 11 | toIstanbul () { | |
| 12 | const loc = { | |
| 13 | start: { | |
| 14 | line: this.startLine, | |
| 15 | column: this.startCol | |
| 16 | }, | |
| 17 | end: { | |
| 18 | line: this.endLine, | |
| 19 | column: this.endCol | |
| 20 | } | |
| 21 | } | |
| 22 | return { | |
| 23 | name: this.name, | |
| 24 | decl: loc, | |
| 25 | loc, | |
| 26 | line: this.startLine | |
| 27 | } | |
| 28 | } | |
| 29 | } |
| … | … | @@ -0,0 +1,34 @@ |
| 1 | module.exports = class CovLine { | |
| 2 | constructor (line, startCol, lineStr) { | |
| 3 | this.line = line | |
| 4 | // note that startCol and endCol are absolute positions | |
| 5 | // within a file, not relative to the line. | |
| 6 | this.startCol = startCol | |
| 7 | ||
| 8 | // the line length itself does not include the newline characters, | |
| 9 | // these are however taken into account when enumerating absolute offset. | |
| 10 | const matchedNewLineChar = lineStr.match(/\r?\n$/u) | |
| 11 | const newLineLength = matchedNewLineChar ? matchedNewLineChar[0].length : 0 | |
| 12 | this.endCol = startCol + lineStr.length - newLineLength | |
| 13 | ||
| 14 | // we start with all lines having been executed, and work | |
| 15 | // backwards zeroing out lines based on V8 output. | |
| 16 | this.count = 1 | |
| 17 | ||
| 18 | // set by source.js during parsing, if /* c8 ignore next */ is found. | |
| 19 | this.ignore = false | |
| 20 | } | |
| 21 | ||
| 22 | toIstanbul () { | |
| 23 | return { | |
| 24 | start: { | |
| 25 | line: this.line, | |
| 26 | column: 0 | |
| 27 | }, | |
| 28 | end: { | |
| 29 | line: this.line, | |
| 30 | column: this.endCol - this.startCol | |
| 31 | } | |
| 32 | } | |
| 33 | } | |
| 34 | } |
| … | … | @@ -0,0 +1,35 @@ |
| 1 | /** | |
| 2 | * ...something resembling a binary search, to find the lowest line within the range. | |
| 3 | * And then you could break as soon as the line is longer than the range... | |
| 4 | */ | |
| 5 | module.exports.sliceRange = (lines, startCol, endCol, inclusive = false) => { | |
| 6 | let start = 0 | |
| 7 | let end = lines.length | |
| 8 | ||
| 9 | if (inclusive) { | |
| 10 | // I consider this a temporary solution until I find an alternaive way to fix the "off by one issue" | |
| 11 | --startCol | |
| 12 | } | |
| 13 | ||
| 14 | while (start < end) { | |
| 15 | let mid = (start + end) >> 1 | |
| 16 | if (startCol >= lines[mid].endCol) { | |
| 17 | start = mid + 1 | |
| 18 | } else if (endCol < lines[mid].startCol) { | |
| 19 | end = mid - 1 | |
| 20 | } else { | |
| 21 | end = mid | |
| 22 | while (mid >= 0 && startCol < lines[mid].endCol && endCol >= lines[mid].startCol) { | |
| 23 | --mid | |
| 24 | } | |
| 25 | start = mid + 1 | |
| 26 | break | |
| 27 | } | |
| 28 | } | |
| 29 | ||
| 30 | while (end < lines.length && startCol < lines[end].endCol && endCol >= lines[end].startCol) { | |
| 31 | ++end | |
| 32 | } | |
| 33 | ||
| 34 | return lines.slice(start, end) | |
| 35 | } |
| … | … | @@ -0,0 +1,254 @@ |
| 1 | const CovLine = require('./line') | |
| 2 | const { sliceRange } = require('./range') | |
| 3 | const { originalPositionFor, generatedPositionFor, GREATEST_LOWER_BOUND, LEAST_UPPER_BOUND } = require('@jridgewell/trace-mapping') | |
| 4 | ||
| 5 | module.exports = class CovSource { | |
| 6 | constructor (sourceRaw, wrapperLength) { | |
| 7 | sourceRaw = sourceRaw ? sourceRaw.trimEnd() : '' | |
| 8 | this.lines = [] | |
| 9 | this.eof = sourceRaw.length | |
| 10 | this.shebangLength = getShebangLength(sourceRaw) | |
| 11 | this.wrapperLength = wrapperLength - this.shebangLength | |
| 12 | this._buildLines(sourceRaw) | |
| 13 | } | |
| 14 | ||
| 15 | _buildLines (source) { | |
| 16 | let position = 0 | |
| 17 | let ignoreCount = 0 | |
| 18 | let ignoreAll = false | |
| 19 | for (const [i, lineStr] of source.split(/(?<=\r?\n)/u).entries()) { | |
| 20 | const line = new CovLine(i + 1, position, lineStr) | |
| 21 | if (ignoreCount > 0) { | |
| 22 | line.ignore = true | |
| 23 | ignoreCount-- | |
| 24 | } else if (ignoreAll) { | |
| 25 | line.ignore = true | |
| 26 | } | |
| 27 | this.lines.push(line) | |
| 28 | position += lineStr.length | |
| 29 | ||
| 30 | const ignoreToken = this._parseIgnore(lineStr) | |
| 31 | if (!ignoreToken) continue | |
| 32 | ||
| 33 | line.ignore = true | |
| 34 | if (ignoreToken.count !== undefined) { | |
| 35 | ignoreCount = ignoreToken.count | |
| 36 | } | |
| 37 | if (ignoreToken.start || ignoreToken.stop) { | |
| 38 | ignoreAll = ignoreToken.start | |
| 39 | ignoreCount = 0 | |
| 40 | } | |
| 41 | } | |
| 42 | } | |
| 43 | ||
| 44 | /** | |
| 45 | * Parses for comments: | |
| 46 | * c8 ignore next | |
| 47 | * c8 ignore next 3 | |
| 48 | * c8 ignore start | |
| 49 | * c8 ignore stop | |
| 50 | * And equivalent ones for v8, e.g. v8 ignore next. | |
| 51 | * @param {string} lineStr | |
| 52 | * @return {{count?: number, start?: boolean, stop?: boolean}|undefined} | |
| 53 | */ | |
| 54 | _parseIgnore (lineStr) { | |
| 55 | const testIgnoreNextLines = lineStr.match(/^\W*\/\* (?:[cv]8|node:coverage) ignore next (?<count>[0-9]+)/) | |
| 56 | if (testIgnoreNextLines) { | |
| 57 | return { count: Number(testIgnoreNextLines.groups.count) } | |
| 58 | } | |
| 59 | ||
| 60 | // Check if comment is on its own line. | |
| 61 | if (lineStr.match(/^\W*\/\* (?:[cv]8|node:coverage) ignore next/)) { | |
| 62 | return { count: 1 } | |
| 63 | } | |
| 64 | ||
| 65 | if (lineStr.match(/\/\* ([cv]8|node:coverage) ignore next/)) { | |
| 66 | // Won't ignore successive lines, but the current line will be ignored. | |
| 67 | return { count: 0 } | |
| 68 | } | |
| 69 | ||
| 70 | const testIgnoreStartStop = lineStr.match(/\/\* [c|v]8 ignore (?<mode>start|stop)/) | |
| 71 | if (testIgnoreStartStop) { | |
| 72 | if (testIgnoreStartStop.groups.mode === 'start') return { start: true } | |
| 73 | if (testIgnoreStartStop.groups.mode === 'stop') return { stop: true } | |
| 74 | } | |
| 75 | ||
| 76 | const testNodeIgnoreStartStop = lineStr.match(/\/\* node:coverage (?<mode>enable|disable)/) | |
| 77 | if (testNodeIgnoreStartStop) { | |
| 78 | if (testNodeIgnoreStartStop.groups.mode === 'disable') return { start: true } | |
| 79 | if (testNodeIgnoreStartStop.groups.mode === 'enable') return { stop: true } | |
| 80 | } | |
| 81 | } | |
| 82 | ||
| 83 | // given a start column and end column in absolute offsets within | |
| 84 | // a source file (0 - EOF), returns the relative line column positions. | |
| 85 | offsetToOriginalRelative (sourceMap, startCol, endCol) { | |
| 86 | const lines = sliceRange(this.lines, startCol, endCol, true) | |
| 87 | if (!lines.length) return {} | |
| 88 | ||
| 89 | const start = originalPositionTryBoth( | |
| 90 | sourceMap, | |
| 91 | lines[0].line, | |
| 92 | Math.max(0, startCol - lines[0].startCol) | |
| 93 | ) | |
| 94 | if (!(start && start.source)) { | |
| 95 | return {} | |
| 96 | } | |
| 97 | ||
| 98 | let end = originalEndPositionFor( | |
| 99 | sourceMap, | |
| 100 | lines[lines.length - 1].line, | |
| 101 | endCol - lines[lines.length - 1].startCol | |
| 102 | ) | |
| 103 | if (!(end && end.source)) { | |
| 104 | return {} | |
| 105 | } | |
| 106 | ||
| 107 | if (start.source !== end.source) { | |
| 108 | return {} | |
| 109 | } | |
| 110 | ||
| 111 | if (start.line === end.line && start.column === end.column) { | |
| 112 | end = originalPositionFor(sourceMap, { | |
| 113 | line: lines[lines.length - 1].line, | |
| 114 | column: endCol - lines[lines.length - 1].startCol, | |
| 115 | bias: LEAST_UPPER_BOUND | |
| 116 | }) | |
| 117 | end.column -= 1 | |
| 118 | } | |
| 119 | ||
| 120 | return { | |
| 121 | source: start.source, | |
| 122 | startLine: start.line, | |
| 123 | relStartCol: start.column, | |
| 124 | endLine: end.line, | |
| 125 | relEndCol: end.column | |
| 126 | } | |
| 127 | } | |
| 128 | ||
| 129 | relativeToOffset (line, relCol) { | |
| 130 | line = Math.max(line, 1) | |
| 131 | if (this.lines[line - 1] === undefined) return this.eof | |
| 132 | return Math.min(this.lines[line - 1].startCol + relCol, this.lines[line - 1].endCol) | |
| 133 | } | |
| 134 | } | |
| 135 | ||
| 136 | // this implementation is pulled over from istanbul-lib-sourcemap: | |
| 137 | // https://github.com/istanbuljs/istanbuljs/blob/master/packages/istanbul-lib-source-maps/lib/get-mapping.js | |
| 138 | // | |
| 139 | /** | |
| 140 | * AST ranges are inclusive for start positions and exclusive for end positions. | |
| 141 | * Source maps are also logically ranges over text, though interacting with | |
| 142 | * them is generally achieved by working with explicit positions. | |
| 143 | * | |
| 144 | * When finding the _end_ location of an AST item, the range behavior is | |
| 145 | * important because what we're asking for is the _end_ of whatever range | |
| 146 | * corresponds to the end location we seek. | |
| 147 | * | |
| 148 | * This boils down to the following steps, conceptually, though the source-map | |
| 149 | * library doesn't expose primitives to do this nicely: | |
| 150 | * | |
| 151 | * 1. Find the range on the generated file that ends at, or exclusively | |
| 152 | * contains the end position of the AST node. | |
| 153 | * 2. Find the range on the original file that corresponds to | |
| 154 | * that generated range. | |
| 155 | * 3. Find the _end_ location of that original range. | |
| 156 | */ | |
| 157 | function originalEndPositionFor (sourceMap, line, column) { | |
| 158 | // Given the generated location, find the original location of the mapping | |
| 159 | // that corresponds to a range on the generated file that overlaps the | |
| 160 | // generated file end location. Note however that this position on its | |
| 161 | // own is not useful because it is the position of the _start_ of the range | |
| 162 | // on the original file, and we want the _end_ of the range. | |
| 163 | const beforeEndMapping = originalPositionTryBoth( | |
| 164 | sourceMap, | |
| 165 | line, | |
| 166 | Math.max(column - 1, 1) | |
| 167 | ) | |
| 168 | ||
| 169 | if (beforeEndMapping.source === null) { | |
| 170 | return null | |
| 171 | } | |
| 172 | ||
| 173 | // Convert that original position back to a generated one, with a bump | |
| 174 | // to the right, and a rightward bias. Since 'generatedPositionFor' searches | |
| 175 | // for mappings in the original-order sorted list, this will find the | |
| 176 | // mapping that corresponds to the one immediately after the | |
| 177 | // beforeEndMapping mapping. | |
| 178 | const afterEndMapping = generatedPositionFor(sourceMap, { | |
| 179 | source: beforeEndMapping.source, | |
| 180 | line: beforeEndMapping.line, | |
| 181 | column: beforeEndMapping.column + 1, | |
| 182 | bias: LEAST_UPPER_BOUND | |
| 183 | }) | |
| 184 | if ( | |
| 185 | // If this is null, it means that we've hit the end of the file, | |
| 186 | // so we can use Infinity as the end column. | |
| 187 | afterEndMapping.line === null || | |
| 188 | // If these don't match, it means that the call to | |
| 189 | // 'generatedPositionFor' didn't find any other original mappings on | |
| 190 | // the line we gave, so consider the binding to extend to infinity. | |
| 191 | originalPositionFor(sourceMap, afterEndMapping).line !== | |
| 192 | beforeEndMapping.line | |
| 193 | ) { | |
| 194 | return { | |
| 195 | source: beforeEndMapping.source, | |
| 196 | line: beforeEndMapping.line, | |
| 197 | column: Infinity | |
| 198 | } | |
| 199 | } | |
| 200 | ||
| 201 | // Convert the end mapping into the real original position. | |
| 202 | return originalPositionFor(sourceMap, afterEndMapping) | |
| 203 | } | |
| 204 | ||
| 205 | function originalPositionTryBoth (sourceMap, line, column) { | |
| 206 | let original = originalPositionFor(sourceMap, { | |
| 207 | line, | |
| 208 | column, | |
| 209 | bias: GREATEST_LOWER_BOUND | |
| 210 | }) | |
| 211 | if (original.line === null) { | |
| 212 | original = originalPositionFor(sourceMap, { | |
| 213 | line, | |
| 214 | column, | |
| 215 | bias: LEAST_UPPER_BOUND | |
| 216 | }) | |
| 217 | } | |
| 218 | // The source maps generated by https://github.com/istanbuljs/istanbuljs | |
| 219 | // (using @babel/core 7.7.5) have behavior, such that a mapping | |
| 220 | // mid-way through a line maps to an earlier line than a mapping | |
| 221 | // at position 0. Using the line at positon 0 seems to provide better reports: | |
| 222 | // | |
| 223 | // if (true) { | |
| 224 | // cov_y5divc6zu().b[1][0]++; | |
| 225 | // cov_y5divc6zu().s[3]++; | |
| 226 | // console.info('reachable'); | |
| 227 | // } else { ... } | |
| 228 | // ^ ^ | |
| 229 | // l5 l3 | |
| 230 | const min = originalPositionFor(sourceMap, { | |
| 231 | line, | |
| 232 | column: 0, | |
| 233 | bias: GREATEST_LOWER_BOUND | |
| 234 | }) | |
| 235 | if (min.line > original.line) { | |
| 236 | original = min | |
| 237 | } | |
| 238 | return original | |
| 239 | } | |
| 240 | ||
| 241 | // Not required since Node 12, see: https://github.com/nodejs/node/pull/27375 | |
| 242 | const isPreNode12 = /^v1[0-1]\./u.test(process.version) | |
| 243 | function getShebangLength (source) { | |
| 244 | /* c8 ignore start - platform-specific */ | |
| 245 | if (isPreNode12 && source.indexOf('#!') === 0) { | |
| 246 | const match = source.match(/(?<shebang>#!.*)/) | |
| 247 | if (match) { | |
| 248 | return match.groups.shebang.length | |
| 249 | } | |
| 250 | } else { | |
| 251 | /* c8 ignore stop - platform-specific */ | |
| 252 | return 0 | |
| 253 | } | |
| 254 | } |
| … | … | @@ -0,0 +1,323 @@ |
| 1 | const assert = require('assert') | |
| 2 | const convertSourceMap = require('convert-source-map') | |
| 3 | const util = require('util') | |
| 4 | const debuglog = util.debuglog('c8') | |
| 5 | const { dirname, isAbsolute, join, resolve } = require('path') | |
| 6 | const { fileURLToPath } = require('url') | |
| 7 | const CovBranch = require('./branch') | |
| 8 | const CovFunction = require('./function') | |
| 9 | const CovSource = require('./source') | |
| 10 | const { sliceRange } = require('./range') | |
| 11 | const compatError = Error(`requires Node.js ${require('../package.json').engines.node}`) | |
| 12 | const { readFileSync } = require('fs') | |
| 13 | let readFile = () => { throw compatError } | |
| 14 | try { | |
| 15 | readFile = require('fs').promises.readFile | |
| 16 | } catch (_err) { | |
| 17 | // most likely we're on an older version of Node.js. | |
| 18 | } | |
| 19 | const { TraceMap } = require('@jridgewell/trace-mapping') | |
| 20 | const isOlderNode10 = /^v10\.(([0-9]\.)|(1[0-5]\.))/u.test(process.version) | |
| 21 | const isNode8 = /^v8\./.test(process.version) | |
| 22 | ||
| 23 | // Injected when Node.js is loading script into isolate pre Node 10.16.x. | |
| 24 | // see: https://github.com/nodejs/node/pull/21573. | |
| 25 | const cjsWrapperLength = isOlderNode10 ? require('module').wrapper[0].length : 0 | |
| 26 | ||
| 27 | module.exports = class V8ToIstanbul { | |
| 28 | constructor (scriptPath, wrapperLength, sources, excludePath) { | |
| 29 | assert(typeof scriptPath === 'string', 'scriptPath must be a string') | |
| 30 | assert(!isNode8, 'This module does not support node 8 or lower, please upgrade to node 10') | |
| 31 | this.path = parsePath(scriptPath) | |
| 32 | this.wrapperLength = wrapperLength === undefined ? cjsWrapperLength : wrapperLength | |
| 33 | this.excludePath = excludePath || (() => false) | |
| 34 | this.sources = sources || {} | |
| 35 | this.generatedLines = [] | |
| 36 | this.branches = {} | |
| 37 | this.functions = {} | |
| 38 | this.covSources = [] | |
| 39 | this.rawSourceMap = undefined | |
| 40 | this.sourceMap = undefined | |
| 41 | this.sourceTranspiled = undefined | |
| 42 | // Indicate that this report was generated with placeholder data from | |
| 43 | // running --all: | |
| 44 | this.all = false | |
| 45 | } | |
| 46 | ||
| 47 | async load () { | |
| 48 | const rawSource = this.sources.source || await readFile(this.path, 'utf8') | |
| 49 | this.rawSourceMap = this.sources.sourceMap || | |
| 50 | // if we find a source-map (either inline, or a .map file) we load | |
| 51 | // both the transpiled and original source, both of which are used during | |
| 52 | // the backflips we perform to remap absolute to relative positions. | |
| 53 | convertSourceMap.fromSource(rawSource) || convertSourceMap.fromMapFileSource(rawSource, this._readFileFromDir.bind(this)) | |
| 54 | ||
| 55 | if (this.rawSourceMap) { | |
| 56 | if (this.rawSourceMap.sourcemap.sources.length > 1) { | |
| 57 | this.sourceMap = new TraceMap(this.rawSourceMap.sourcemap) | |
| 58 | if (!this.sourceMap.sourcesContent) { | |
| 59 | this.sourceMap.sourcesContent = await this.sourcesContentFromSources() | |
| 60 | } | |
| 61 | this.covSources = this.sourceMap.sourcesContent.map((rawSource, i) => ({ source: new CovSource(rawSource, this.wrapperLength), path: this.sourceMap.sources[i] })) | |
| 62 | this.sourceTranspiled = new CovSource(rawSource, this.wrapperLength) | |
| 63 | } else { | |
| 64 | const candidatePath = this.rawSourceMap.sourcemap.sources.length >= 1 ? this.rawSourceMap.sourcemap.sources[0] : this.rawSourceMap.sourcemap.file | |
| 65 | this.path = this._resolveSource(this.rawSourceMap, candidatePath || this.path) | |
| 66 | this.sourceMap = new TraceMap(this.rawSourceMap.sourcemap) | |
| 67 | ||
| 68 | let originalRawSource | |
| 69 | if (this.sources.sourceMap && this.sources.sourceMap.sourcemap && this.sources.sourceMap.sourcemap.sourcesContent && this.sources.sourceMap.sourcemap.sourcesContent.length === 1) { | |
| 70 | // If the sourcesContent field has been provided, return it rather than attempting | |
| 71 | // to load the original source from disk. | |
| 72 | // TODO: investigate whether there's ever a case where we hit this logic with 1:many sources. | |
| 73 | originalRawSource = this.sources.sourceMap.sourcemap.sourcesContent[0] | |
| 74 | } else if (this.sources.originalSource) { | |
| 75 | // Original source may be populated on the sources object. | |
| 76 | originalRawSource = this.sources.originalSource | |
| 77 | } else if (this.sourceMap.sourcesContent && this.sourceMap.sourcesContent[0]) { | |
| 78 | // perhaps we loaded sourcesContent was populated by an inline source map, or .map file? | |
| 79 | // TODO: investigate whether there's ever a case where we hit this logic with 1:many sources. | |
| 80 | originalRawSource = this.sourceMap.sourcesContent[0] | |
| 81 | } else { | |
| 82 | // We fallback to reading the original source from disk. | |
| 83 | originalRawSource = await readFile(this.path, 'utf8') | |
| 84 | } | |
| 85 | this.covSources = [{ source: new CovSource(originalRawSource, this.wrapperLength), path: this.path }] | |
| 86 | this.sourceTranspiled = new CovSource(rawSource, this.wrapperLength) | |
| 87 | } | |
| 88 | } else { | |
| 89 | this.covSources = [{ source: new CovSource(rawSource, this.wrapperLength), path: this.path }] | |
| 90 | } | |
| 91 | } | |
| 92 | ||
| 93 | _readFileFromDir (filename) { | |
| 94 | return readFileSync(resolve(dirname(this.path), filename), 'utf-8') | |
| 95 | } | |
| 96 | ||
| 97 | async sourcesContentFromSources () { | |
| 98 | const fileList = this.sourceMap.sources.map(relativePath => { | |
| 99 | const realPath = this._resolveSource(this.rawSourceMap, relativePath) | |
| 100 | return readFile(realPath, 'utf-8') | |
| 101 | .then(result => result) | |
| 102 | .catch(err => { | |
| 103 | debuglog(`failed to load ${realPath}: ${err.message}`) | |
| 104 | }) | |
| 105 | }) | |
| 106 | return await Promise.all(fileList) | |
| 107 | } | |
| 108 | ||
| 109 | destroy () { | |
| 110 | // no longer necessary, but preserved for backwards compatibility. | |
| 111 | } | |
| 112 | ||
| 113 | _resolveSource (rawSourceMap, sourcePath) { | |
| 114 | if (sourcePath.startsWith('file://')) { | |
| 115 | return fileURLToPath(sourcePath) | |
| 116 | } | |
| 117 | sourcePath = sourcePath.replace(/^webpack:\/\//, '') | |
| 118 | const sourceRoot = rawSourceMap.sourcemap.sourceRoot ? rawSourceMap.sourcemap.sourceRoot.replace('file://', '') : '' | |
| 119 | const candidatePath = join(sourceRoot, sourcePath) | |
| 120 | ||
| 121 | if (isAbsolute(candidatePath)) { | |
| 122 | return candidatePath | |
| 123 | } else { | |
| 124 | return resolve(dirname(this.path), candidatePath) | |
| 125 | } | |
| 126 | } | |
| 127 | ||
| 128 | applyCoverage (blocks) { | |
| 129 | blocks.forEach(block => { | |
| 130 | block.ranges.forEach((range, i) => { | |
| 131 | const isEmptyCoverage = block.functionName === '(empty-report)' | |
| 132 | const { startCol, endCol, path, covSource } = this._maybeRemapStartColEndCol(range, isEmptyCoverage) | |
| 133 | if (this.excludePath(path)) { | |
| 134 | return | |
| 135 | } | |
| 136 | let lines | |
| 137 | if (isEmptyCoverage) { | |
| 138 | // (empty-report), this will result in a report that has all lines zeroed out. | |
| 139 | lines = covSource.lines.filter((line) => { | |
| 140 | line.count = 0 | |
| 141 | return true | |
| 142 | }) | |
| 143 | this.all = lines.length > 0 | |
| 144 | } else { | |
| 145 | lines = sliceRange(covSource.lines, startCol, endCol) | |
| 146 | } | |
| 147 | if (!lines.length) { | |
| 148 | return | |
| 149 | } | |
| 150 | ||
| 151 | const startLineInstance = lines[0] | |
| 152 | const endLineInstance = lines[lines.length - 1] | |
| 153 | ||
| 154 | if (block.isBlockCoverage) { | |
| 155 | this.branches[path] = this.branches[path] || [] | |
| 156 | // record branches. | |
| 157 | this.branches[path].push(new CovBranch( | |
| 158 | startLineInstance.line, | |
| 159 | startCol - startLineInstance.startCol, | |
| 160 | endLineInstance.line, | |
| 161 | endCol - endLineInstance.startCol, | |
| 162 | range.count | |
| 163 | )) | |
| 164 | ||
| 165 | // if block-level granularity is enabled, we still create a single | |
| 166 | // CovFunction tracking object for each set of ranges. | |
| 167 | if (block.functionName && i === 0) { | |
| 168 | this.functions[path] = this.functions[path] || [] | |
| 169 | this.functions[path].push(new CovFunction( | |
| 170 | block.functionName, | |
| 171 | startLineInstance.line, | |
| 172 | startCol - startLineInstance.startCol, | |
| 173 | endLineInstance.line, | |
| 174 | endCol - endLineInstance.startCol, | |
| 175 | range.count | |
| 176 | )) | |
| 177 | } | |
| 178 | } else if (block.functionName) { | |
| 179 | this.functions[path] = this.functions[path] || [] | |
| 180 | // record functions. | |
| 181 | this.functions[path].push(new CovFunction( | |
| 182 | block.functionName, | |
| 183 | startLineInstance.line, | |
| 184 | startCol - startLineInstance.startCol, | |
| 185 | endLineInstance.line, | |
| 186 | endCol - endLineInstance.startCol, | |
| 187 | range.count | |
| 188 | )) | |
| 189 | } | |
| 190 | ||
| 191 | // record the lines (we record these as statements, such that we're | |
| 192 | // compatible with Istanbul 2.0). | |
| 193 | lines.forEach(line => { | |
| 194 | // make sure branch spans entire line; don't record 'goodbye' | |
| 195 | // branch in `const foo = true ? 'hello' : 'goodbye'` as a | |
| 196 | // 0 for line coverage. | |
| 197 | // | |
| 198 | // All lines start out with coverage of 1, and are later set to 0 | |
| 199 | // if they are not invoked; line.ignore prevents a line from being | |
| 200 | // set to 0, and is set if the special comment /* c8 ignore next */ | |
| 201 | // is used. | |
| 202 | ||
| 203 | if (startCol <= line.startCol && endCol >= line.endCol && !line.ignore) { | |
| 204 | line.count = range.count | |
| 205 | } | |
| 206 | }) | |
| 207 | }) | |
| 208 | }) | |
| 209 | } | |
| 210 | ||
| 211 | _maybeRemapStartColEndCol (range, isEmptyCoverage) { | |
| 212 | let covSource = this.covSources[0].source | |
| 213 | const covSourceWrapperLength = isEmptyCoverage ? 0 : covSource.wrapperLength | |
| 214 | let startCol = Math.max(0, range.startOffset - covSourceWrapperLength) | |
| 215 | let endCol = Math.min(covSource.eof, range.endOffset - covSourceWrapperLength) | |
| 216 | let path = this.path | |
| 217 | ||
| 218 | if (this.sourceMap) { | |
| 219 | const sourceTranspiledWrapperLength = isEmptyCoverage ? 0 : this.sourceTranspiled.wrapperLength | |
| 220 | startCol = Math.max(0, range.startOffset - sourceTranspiledWrapperLength) | |
| 221 | endCol = Math.min(this.sourceTranspiled.eof, range.endOffset - sourceTranspiledWrapperLength) | |
| 222 | ||
| 223 | const { startLine, relStartCol, endLine, relEndCol, source } = this.sourceTranspiled.offsetToOriginalRelative( | |
| 224 | this.sourceMap, | |
| 225 | startCol, | |
| 226 | endCol | |
| 227 | ) | |
| 228 | ||
| 229 | const matchingSource = this.covSources.find(covSource => covSource.path === source) | |
| 230 | covSource = matchingSource ? matchingSource.source : this.covSources[0].source | |
| 231 | path = matchingSource ? matchingSource.path : this.covSources[0].path | |
| 232 | ||
| 233 | // next we convert these relative positions back to absolute positions | |
| 234 | // in the original source (which is the format expected in the next step). | |
| 235 | startCol = covSource.relativeToOffset(startLine, relStartCol) | |
| 236 | endCol = covSource.relativeToOffset(endLine, relEndCol) | |
| 237 | } | |
| 238 | ||
| 239 | return { | |
| 240 | path, | |
| 241 | covSource, | |
| 242 | startCol, | |
| 243 | endCol | |
| 244 | } | |
| 245 | } | |
| 246 | ||
| 247 | getInnerIstanbul (source, path) { | |
| 248 | // We apply the "Resolving Sources" logic (as defined in | |
| 249 | // sourcemaps.info/spec.html) as a final step for 1:many source maps. | |
| 250 | // for 1:1 source maps, the resolve logic is applied while loading. | |
| 251 | // | |
| 252 | // TODO: could we move the resolving logic for 1:1 source maps to the final | |
| 253 | // step as well? currently this breaks some tests in c8. | |
| 254 | let resolvedPath = path | |
| 255 | if (this.rawSourceMap && this.rawSourceMap.sourcemap.sources.length > 1) { | |
| 256 | resolvedPath = this._resolveSource(this.rawSourceMap, path) | |
| 257 | } | |
| 258 | ||
| 259 | if (this.excludePath(resolvedPath)) { | |
| 260 | return | |
| 261 | } | |
| 262 | ||
| 263 | return { | |
| 264 | [resolvedPath]: { | |
| 265 | path: resolvedPath, | |
| 266 | all: this.all, | |
| 267 | ...this._statementsToIstanbul(source, path), | |
| 268 | ...this._branchesToIstanbul(source, path), | |
| 269 | ...this._functionsToIstanbul(source, path) | |
| 270 | } | |
| 271 | } | |
| 272 | } | |
| 273 | ||
| 274 | toIstanbul () { | |
| 275 | return this.covSources.reduce((istanbulOuter, { source, path }) => Object.assign(istanbulOuter, this.getInnerIstanbul(source, path)), {}) | |
| 276 | } | |
| 277 | ||
| 278 | _statementsToIstanbul (source, path) { | |
| 279 | const statements = { | |
| 280 | statementMap: {}, | |
| 281 | s: {} | |
| 282 | } | |
| 283 | source.lines.forEach((line, index) => { | |
| 284 | statements.statementMap[`${index}`] = line.toIstanbul() | |
| 285 | statements.s[`${index}`] = line.ignore ? 1 : line.count | |
| 286 | }) | |
| 287 | return statements | |
| 288 | } | |
| 289 | ||
| 290 | _branchesToIstanbul (source, path) { | |
| 291 | const branches = { | |
| 292 | branchMap: {}, | |
| 293 | b: {} | |
| 294 | } | |
| 295 | this.branches[path] = this.branches[path] || [] | |
| 296 | this.branches[path].forEach((branch, index) => { | |
| 297 | const srcLine = source.lines[branch.startLine - 1] | |
| 298 | const ignore = srcLine === undefined ? true : srcLine.ignore | |
| 299 | branches.branchMap[`${index}`] = branch.toIstanbul() | |
| 300 | branches.b[`${index}`] = [ignore ? 1 : branch.count] | |
| 301 | }) | |
| 302 | return branches | |
| 303 | } | |
| 304 | ||
| 305 | _functionsToIstanbul (source, path) { | |
| 306 | const functions = { | |
| 307 | fnMap: {}, | |
| 308 | f: {} | |
| 309 | } | |
| 310 | this.functions[path] = this.functions[path] || [] | |
| 311 | this.functions[path].forEach((fn, index) => { | |
| 312 | const srcLine = source.lines[fn.startLine - 1] | |
| 313 | const ignore = srcLine === undefined ? true : srcLine.ignore | |
| 314 | functions.fnMap[`${index}`] = fn.toIstanbul() | |
| 315 | functions.f[`${index}`] = ignore ? 1 : fn.count | |
| 316 | }) | |
| 317 | return functions | |
| 318 | } | |
| 319 | } | |
| 320 | ||
| 321 | function parsePath (scriptPath) { | |
| 322 | return scriptPath.startsWith('file://') ? fileURLToPath(scriptPath) : scriptPath | |
| 323 | } |
| … | … | @@ -0,0 +1,49 @@ |
| 1 | { | |
| 2 | "name": "v8-to-istanbul", | |
| 3 | "version": "9.3.0", | |
| 4 | "description": "convert from v8 coverage format to istanbul's format", | |
| 5 | "main": "index.js", | |
| 6 | "types": "index.d.ts", | |
| 7 | "scripts": { | |
| 8 | "fix": "standard --fix", | |
| 9 | "snapshot": "TAP_SNAPSHOT=1 tap test/*.js", | |
| 10 | "test": "c8 --reporter=html --reporter=text tap --no-coverage test/*.js", | |
| 11 | "posttest": "standard", | |
| 12 | "coverage": "c8 report --check-coverage" | |
| 13 | }, | |
| 14 | "repository": "istanbuljs/v8-to-istanbul", | |
| 15 | "keywords": [ | |
| 16 | "istanbul", | |
| 17 | "v8", | |
| 18 | "coverage" | |
| 19 | ], | |
| 20 | "standard": { | |
| 21 | "ignore": [ | |
| 22 | "**/test/fixtures" | |
| 23 | ] | |
| 24 | }, | |
| 25 | "author": "Ben Coe <ben@npmjs.com>", | |
| 26 | "license": "ISC", | |
| 27 | "dependencies": { | |
| 28 | "@jridgewell/trace-mapping": "^0.3.12", | |
| 29 | "@types/istanbul-lib-coverage": "^2.0.1", | |
| 30 | "convert-source-map": "^2.0.0" | |
| 31 | }, | |
| 32 | "devDependencies": { | |
| 33 | "@types/node": "^20.0.0", | |
| 34 | "c8": "^7.2.1", | |
| 35 | "semver": "^7.3.2", | |
| 36 | "should": "13.2.3", | |
| 37 | "source-map": "^0.7.3", | |
| 38 | "standard": "^17.0.0", | |
| 39 | "tap": "^16.0.0" | |
| 40 | }, | |
| 41 | "engines": { | |
| 42 | "node": ">=10.12.0" | |
| 43 | }, | |
| 44 | "files": [ | |
| 45 | "lib/*.js", | |
| 46 | "index.js", | |
| 47 | "index.d.ts" | |
| 48 | ] | |
| 49 | } |
| … | … | @@ -0,0 +1,21 @@ |
| 1 | The MIT License (MIT) | |
| 2 | ||
| 3 | Copyright (c) 2021 Mathias Buus | |
| 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 | |
| 13 | all 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 | |
| 21 | THE SOFTWARE. |
| … | … | @@ -0,0 +1,35 @@ |
| 1 | # xache | |
| 2 | ||
| 3 | Yet another auto expiring, max sizable cache | |
| 4 | ||
| 5 | ``` | |
| 6 | npm install xache | |
| 7 | ``` | |
| 8 | ||
| 9 | ## Usage | |
| 10 | ||
| 11 | ``` js | |
| 12 | const Xache = require('xache') | |
| 13 | ||
| 14 | const cache = new Xache({ | |
| 15 | maxSize: 10, // at max (ish) have 10 entries | |
| 16 | maxAge: 100, // auto expire entries after (ish) 100ms | |
| 17 | createMap () { // optional function to create backing storage | |
| 18 | return new Map() | |
| 19 | } | |
| 20 | }) | |
| 21 | ||
| 22 | // When maxSize is hit, the oldest entries are removed | |
| 23 | // cache has the same api as a map | |
| 24 | ||
| 25 | cache.set('hello', 'world') | |
| 26 | console.log(cache.get('hello')) | |
| 27 | ||
| 28 | console.log(...cache) // iterable! | |
| 29 | ``` | |
| 30 | ||
| 31 | That's it, nice and simple. | |
| 32 | ||
| 33 | ## License | |
| 34 | ||
| 35 | MIT |
| … | … | @@ -0,0 +1,108 @@ |
| 1 | module.exports = class MaxCache { | |
| 2 | constructor ({ maxSize, maxAge, createMap, ongc }) { | |
| 3 | this.maxSize = maxSize | |
| 4 | this.maxAge = maxAge | |
| 5 | this.ongc = ongc || null | |
| 6 | ||
| 7 | this._createMap = createMap || defaultCreateMap | |
| 8 | this._latest = this._createMap() | |
| 9 | this._oldest = this._createMap() | |
| 10 | this._retained = this._createMap() | |
| 11 | this._gced = false | |
| 12 | this._interval = null | |
| 13 | ||
| 14 | if (this.maxAge > 0 && this.maxAge < Infinity) { | |
| 15 | const tick = Math.ceil(2 / 3 * this.maxAge) | |
| 16 | this._interval = setInterval(this._gcAuto.bind(this), tick) | |
| 17 | if (this._interval.unref) this._interval.unref() | |
| 18 | } | |
| 19 | } | |
| 20 | ||
| 21 | * [Symbol.iterator] () { | |
| 22 | for (const it of [this._latest, this._oldest, this._retained]) { | |
| 23 | yield * it | |
| 24 | } | |
| 25 | } | |
| 26 | ||
| 27 | * keys () { | |
| 28 | for (const it of [this._latest, this._oldest, this._retained]) { | |
| 29 | yield * it.keys() | |
| 30 | } | |
| 31 | } | |
| 32 | ||
| 33 | * values () { | |
| 34 | for (const it of [this._latest, this._oldest, this._retained]) { | |
| 35 | yield * it.values() | |
| 36 | } | |
| 37 | } | |
| 38 | ||
| 39 | destroy () { | |
| 40 | this.clear() | |
| 41 | clearInterval(this._interval) | |
| 42 | this._interval = null | |
| 43 | } | |
| 44 | ||
| 45 | clear () { | |
| 46 | this._gced = true | |
| 47 | this._latest.clear() | |
| 48 | this._oldest.clear() | |
| 49 | this._retained.clear() | |
| 50 | } | |
| 51 | ||
| 52 | set (k, v) { | |
| 53 | if (this._retained.has(k)) return this | |
| 54 | this._latest.set(k, v) | |
| 55 | this._oldest.delete(k) || this._retained.delete(k) | |
| 56 | if (this._latest.size >= this.maxSize) this._gc() | |
| 57 | return this | |
| 58 | } | |
| 59 | ||
| 60 | retain (k, v) { | |
| 61 | this._retained.set(k, v) | |
| 62 | this._latest.delete(k) || this._oldest.delete(k) | |
| 63 | return this | |
| 64 | } | |
| 65 | ||
| 66 | delete (k) { | |
| 67 | return this._latest.delete(k) || this._oldest.delete(k) || this._retained.delete(k) | |
| 68 | } | |
| 69 | ||
| 70 | has (k) { | |
| 71 | return this._latest.has(k) || this._oldest.has(k) || this._retained.has(k) | |
| 72 | } | |
| 73 | ||
| 74 | get (k) { | |
| 75 | if (this._latest.has(k)) { | |
| 76 | return this._latest.get(k) | |
| 77 | } | |
| 78 | ||
| 79 | if (this._oldest.has(k)) { | |
| 80 | const v = this._oldest.get(k) | |
| 81 | this._latest.set(k, v) | |
| 82 | this._oldest.delete(k) | |
| 83 | return v | |
| 84 | } | |
| 85 | ||
| 86 | if (this._retained.has(k)) { | |
| 87 | return this._retained.get(k) | |
| 88 | } | |
| 89 | ||
| 90 | return null | |
| 91 | } | |
| 92 | ||
| 93 | _gcAuto () { | |
| 94 | if (!this._gced) this._gc() | |
| 95 | this._gced = false | |
| 96 | } | |
| 97 | ||
| 98 | _gc () { | |
| 99 | this._gced = true | |
| 100 | if (this.ongc !== null && this._oldest.size > 0) this.ongc(this._oldest) | |
| 101 | this._oldest = this._latest | |
| 102 | this._latest = this._createMap() | |
| 103 | } | |
| 104 | } | |
| 105 | ||
| 106 | function defaultCreateMap () { | |
| 107 | return new Map() | |
| 108 | } |
| … | … | @@ -0,0 +1,26 @@ |
| 1 | { | |
| 2 | "name": "xache", | |
| 3 | "version": "1.2.1", | |
| 4 | "description": "Yet another auto expiring, max sizable cache", | |
| 5 | "main": "index.js", | |
| 6 | "files": [ | |
| 7 | "index.js" | |
| 8 | ], | |
| 9 | "devDependencies": { | |
| 10 | "brittle": "^3.3.2", | |
| 11 | "standard": "^17.1.0" | |
| 12 | }, | |
| 13 | "scripts": { | |
| 14 | "test": "standard && brittle test.js" | |
| 15 | }, | |
| 16 | "repository": { | |
| 17 | "type": "git", | |
| 18 | "url": "https://github.com/mafintosh/xache.git" | |
| 19 | }, | |
| 20 | "author": "Mathias Buus (@mafintosh)", | |
| 21 | "license": "MIT", | |
| 22 | "bugs": { | |
| 23 | "url": "https://github.com/mafintosh/xache/issues" | |
| 24 | }, | |
| 25 | "homepage": "https://github.com/mafintosh/xache" | |
| 26 | } |
| … | … | @@ -8,12 +8,16 @@ |
| 8 | 8 | "name": "autobase-doctor", |
| 9 | 9 | "version": "1.0.0", |
| 10 | 10 | "dependencies": { |
| 11 | "@hyperswarm/testnet": "^3.1.4", | |
| 11 | 12 | "autobase": "^7.27.3", |
| 13 | "autobase-test-helpers": "^3.1.0", | |
| 12 | 14 | "bare-path": "^3.0.0", |
| 13 | 15 | "bare-process": "^4.3.0", |
| 16 | "brittle": "^3.19.1", | |
| 14 | 17 | "corestore": "^7.9.2", |
| 15 | 18 | "hyperbee": "^2.22.5", |
| 16 | 19 | "hypercore": "^11.7.3", |
| 20 | "hyperswarm": "^4.17.0", | |
| 17 | 21 | "paparam": "^1.10.1" |
| 18 | 22 | }, |
| 19 | 23 | "bin": { |
| … | … | @@ -37,6 +41,58 @@ |
| 37 | 41 | "unslab": "^1.3.0" |
| 38 | 42 | } |
| 39 | 43 | }, |
| 44 | "node_modules/@hyperswarm/testnet": { | |
| 45 | "version": "3.1.4", | |
| 46 | "resolved": "https://registry.npmjs.org/@hyperswarm/testnet/-/testnet-3.1.4.tgz", | |
| 47 | "integrity": "sha512-Jw8JIZ9kdZ4R9jwJHsqctj5YeRqecw3aeCg2NS12HBXCqMioQIYzVqHs/8BWdOQCf34+BEXkwMQjba9CzaLwkg==", | |
| 48 | "license": "Apache-2.0", | |
| 49 | "dependencies": { | |
| 50 | "hyperdht": "^6.6.0" | |
| 51 | }, | |
| 52 | "bin": { | |
| 53 | "hyperswarm-testnet": "bin.js" | |
| 54 | } | |
| 55 | }, | |
| 56 | "node_modules/@jridgewell/resolve-uri": { | |
| 57 | "version": "3.1.2", | |
| 58 | "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", | |
| 59 | "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", | |
| 60 | "license": "MIT", | |
| 61 | "engines": { | |
| 62 | "node": ">=6.0.0" | |
| 63 | } | |
| 64 | }, | |
| 65 | "node_modules/@jridgewell/sourcemap-codec": { | |
| 66 | "version": "1.5.5", | |
| 67 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", | |
| 68 | "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", | |
| 69 | "license": "MIT" | |
| 70 | }, | |
| 71 | "node_modules/@jridgewell/trace-mapping": { | |
| 72 | "version": "0.3.31", | |
| 73 | "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", | |
| 74 | "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", | |
| 75 | "license": "MIT", | |
| 76 | "dependencies": { | |
| 77 | "@jridgewell/resolve-uri": "^3.1.0", | |
| 78 | "@jridgewell/sourcemap-codec": "^1.4.14" | |
| 79 | } | |
| 80 | }, | |
| 81 | "node_modules/@types/istanbul-lib-coverage": { | |
| 82 | "version": "2.0.6", | |
| 83 | "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", | |
| 84 | "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", | |
| 85 | "license": "MIT" | |
| 86 | }, | |
| 87 | "node_modules/adaptive-timeout": { | |
| 88 | "version": "1.0.1", | |
| 89 | "resolved": "https://registry.npmjs.org/adaptive-timeout/-/adaptive-timeout-1.0.1.tgz", | |
| 90 | "integrity": "sha512-+seGiBtHqbnyZ0Quq/ZGxxwP66153WBABawa07/QeyuPFzbduPBjiGQAyCiriiLDzt3dkbMBskSlfqtRwblK8Q==", | |
| 91 | "license": "Apache-2.0", | |
| 92 | "dependencies": { | |
| 93 | "xache": "^1.2.1" | |
| 94 | } | |
| 95 | }, | |
| 40 | 96 | "node_modules/autobase": { |
| 41 | 97 | "version": "7.27.3", |
| 42 | 98 | "resolved": "https://registry.npmjs.org/autobase/-/autobase-7.27.3.tgz", |
| … | … | @@ -67,6 +123,15 @@ |
| 67 | 123 | "tiny-buffer-map": "^1.1.1" |
| 68 | 124 | } |
| 69 | 125 | }, |
| 126 | "node_modules/autobase-test-helpers": { | |
| 127 | "version": "3.1.0", | |
| 128 | "resolved": "https://registry.npmjs.org/autobase-test-helpers/-/autobase-test-helpers-3.1.0.tgz", | |
| 129 | "integrity": "sha512-3Jk1nWEcMxYOtU6rt4ujpchLz0W/Uv7S6v0tea53wW9rZqwFzgffEYbufNak53/93DBbp3gwX+AgHdfHtpx4RQ==", | |
| 130 | "license": "Apache-2.0", | |
| 131 | "dependencies": { | |
| 132 | "b4a": "^1.6.4" | |
| 133 | } | |
| 134 | }, | |
| 70 | 135 | "node_modules/b4a": { |
| 71 | 136 | "version": "1.8.0", |
| 72 | 137 | "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz", |
| … | … | @@ -131,6 +196,90 @@ |
| 131 | 196 | "bare-inspect": "^3.1.2" |
| 132 | 197 | } |
| 133 | 198 | }, |
| 199 | "node_modules/bare-bundle": { | |
| 200 | "version": "1.10.0", | |
| 201 | "resolved": "https://registry.npmjs.org/bare-bundle/-/bare-bundle-1.10.0.tgz", | |
| 202 | "integrity": "sha512-4LVlnJAHr00Hh6Vu6ZUJS38rcEtJT3b3vChXSsBsJ2mk1TN0lQ+gzd+Dw5L0aV7uqDZv84smuwW+O02X7PfDlw==", | |
| 203 | "license": "Apache-2.0", | |
| 204 | "peerDependencies": { | |
| 205 | "bare-buffer": "*", | |
| 206 | "bare-url": "*" | |
| 207 | }, | |
| 208 | "peerDependenciesMeta": { | |
| 209 | "bare-buffer": { | |
| 210 | "optional": true | |
| 211 | }, | |
| 212 | "bare-url": { | |
| 213 | "optional": true | |
| 214 | } | |
| 215 | } | |
| 216 | }, | |
| 217 | "node_modules/bare-cov": { | |
| 218 | "version": "1.2.1", | |
| 219 | "resolved": "https://registry.npmjs.org/bare-cov/-/bare-cov-1.2.1.tgz", | |
| 220 | "integrity": "sha512-SHNqb/WG9udUEj8m5qGSehoUQ9o0Xzv8nrf4HIdmG+KNU/XdhPLhTP5VPrHnSAMAXhAzEB4Q+2IYZ0x9qPzN1g==", | |
| 221 | "license": "Apache-2.0", | |
| 222 | "dependencies": { | |
| 223 | "bare-fs": "^4.1.2", | |
| 224 | "bare-inspector": "^4.0.1", | |
| 225 | "bare-path": "^3.0.0", | |
| 226 | "bare-process": "^4.2.1", | |
| 227 | "bare-url": "^2.1.5", | |
| 228 | "bare-v8-to-istanbul": "^1.0.2", | |
| 229 | "picomatch": "^4.0.2", | |
| 230 | "which-runtime": "^1.2.1" | |
| 231 | } | |
| 232 | }, | |
| 233 | "node_modules/bare-crypto": { | |
| 234 | "version": "1.13.4", | |
| 235 | "resolved": "https://registry.npmjs.org/bare-crypto/-/bare-crypto-1.13.4.tgz", | |
| 236 | "integrity": "sha512-JiCZ5l2YOG1y8J7yy1BCAKTCZrPnHLb7pDRIdurBTOn5oIwBQDIv8iH5Pl2V85vzjl1NZXRfNY4HZLsE942jJA==", | |
| 237 | "license": "Apache-2.0", | |
| 238 | "dependencies": { | |
| 239 | "bare-assert": "^1.2.0", | |
| 240 | "bare-stream": "^2.6.3" | |
| 241 | }, | |
| 242 | "peerDependencies": { | |
| 243 | "bare-buffer": "*" | |
| 244 | }, | |
| 245 | "peerDependenciesMeta": { | |
| 246 | "bare-buffer": { | |
| 247 | "optional": true | |
| 248 | } | |
| 249 | } | |
| 250 | }, | |
| 251 | "node_modules/bare-debug-log": { | |
| 252 | "version": "2.0.0", | |
| 253 | "resolved": "https://registry.npmjs.org/bare-debug-log/-/bare-debug-log-2.0.0.tgz", | |
| 254 | "integrity": "sha512-Vi42PkMQsNV9PUpx2Gl1hikshx5O9FzMJ6o9Nnopseg7qLBBK7Nl31d0RHcfwLEAfmcPApytpc0ZFfq68u22FQ==", | |
| 255 | "license": "Apache-2.0", | |
| 256 | "dependencies": { | |
| 257 | "bare-os": "^3.0.1" | |
| 258 | } | |
| 259 | }, | |
| 260 | "node_modules/bare-dns": { | |
| 261 | "version": "2.1.4", | |
| 262 | "resolved": "https://registry.npmjs.org/bare-dns/-/bare-dns-2.1.4.tgz", | |
| 263 | "integrity": "sha512-abwjHmpWqSRNB7V5615QxPH92L71AVzFm/kKTs8VYiNTAi2xVdonpv0BjJ0hwXLwomoW+xsSOPjW6PZPO14asg==", | |
| 264 | "license": "Apache-2.0", | |
| 265 | "engines": { | |
| 266 | "bare": ">=1.7.0" | |
| 267 | } | |
| 268 | }, | |
| 269 | "node_modules/bare-encoding": { | |
| 270 | "version": "1.0.3", | |
| 271 | "resolved": "https://registry.npmjs.org/bare-encoding/-/bare-encoding-1.0.3.tgz", | |
| 272 | "integrity": "sha512-Kqf+t/azs13lUeyK4Tb7ha4wdLRXKWCXQ8w1rVmt7KtoPCPdHD/Xwt7LBIsCSwwGglrcmblo5VOLa5avkJqULA==", | |
| 273 | "license": "Apache-2.0", | |
| 274 | "peerDependencies": { | |
| 275 | "bare-buffer": "*" | |
| 276 | }, | |
| 277 | "peerDependenciesMeta": { | |
| 278 | "bare-buffer": { | |
| 279 | "optional": true | |
| 280 | } | |
| 281 | } | |
| 282 | }, | |
| 134 | 283 | "node_modules/bare-env": { |
| 135 | 284 | "version": "3.0.0", |
| 136 | 285 | "resolved": "https://registry.npmjs.org/bare-env/-/bare-env-3.0.0.tgz", |
| … | … | @@ -154,6 +303,15 @@ |
| 154 | 303 | } |
| 155 | 304 | } |
| 156 | 305 | }, |
| 306 | "node_modules/bare-format": { | |
| 307 | "version": "1.0.2", | |
| 308 | "resolved": "https://registry.npmjs.org/bare-format/-/bare-format-1.0.2.tgz", | |
| 309 | "integrity": "sha512-GswdhnOnP9QtwRbrf4wLApw5widkaLMsLe2XOs35fQD2YfEN1ApoGka+cZ7PfvzxMgfYXmMhj/2OGlVn5/Dxgw==", | |
| 310 | "license": "Apache-2.0", | |
| 311 | "dependencies": { | |
| 312 | "bare-inspect": "^3.0.0" | |
| 313 | } | |
| 314 | }, | |
| 157 | 315 | "node_modules/bare-fs": { |
| 158 | 316 | "version": "4.5.6", |
| 159 | 317 | "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.6.tgz", |
| … | … | @@ -184,6 +342,47 @@ |
| 184 | 342 | "integrity": "sha512-VMb3tHo05gsnbu3OXTmkDiwTjMlOsbQmKoysKqKEyR09m77TuDrYFbj3Q5GGk10dAKsUHrnXmwCaeJqzVpB5ZA==", |
| 185 | 343 | "license": "Apache-2.0" |
| 186 | 344 | }, |
| 345 | "node_modules/bare-http-parser": { | |
| 346 | "version": "1.1.3", | |
| 347 | "resolved": "https://registry.npmjs.org/bare-http-parser/-/bare-http-parser-1.1.3.tgz", | |
| 348 | "integrity": "sha512-+dhVvQi6brHq14L/XHNRQ+TLuVE76VjRmMt61wVEtS+Od8xUslfMHWJN/ZjIIt3RtTG6vPuA+x9cOh7KrkBJsA==", | |
| 349 | "license": "Apache-2.0" | |
| 350 | }, | |
| 351 | "node_modules/bare-http1": { | |
| 352 | "version": "4.5.5", | |
| 353 | "resolved": "https://registry.npmjs.org/bare-http1/-/bare-http1-4.5.5.tgz", | |
| 354 | "integrity": "sha512-ADITiRo0huP76JGMbv6Arsh9KehHqjEBoYcmjvAo67IY78+/9mV2MeKLLCkiJSFxA85T/m8cTaE44OwJQCcwdw==", | |
| 355 | "license": "Apache-2.0", | |
| 356 | "dependencies": { | |
| 357 | "bare-events": "^2.6.0", | |
| 358 | "bare-http-parser": "^1.1.1", | |
| 359 | "bare-stream": "^2.10.0", | |
| 360 | "bare-tcp": "^2.2.0" | |
| 361 | }, | |
| 362 | "peerDependencies": { | |
| 363 | "bare-buffer": "*", | |
| 364 | "bare-url": "*" | |
| 365 | }, | |
| 366 | "peerDependenciesMeta": { | |
| 367 | "bare-buffer": { | |
| 368 | "optional": true | |
| 369 | }, | |
| 370 | "bare-url": { | |
| 371 | "optional": true | |
| 372 | } | |
| 373 | } | |
| 374 | }, | |
| 375 | "node_modules/bare-https": { | |
| 376 | "version": "2.1.3", | |
| 377 | "resolved": "https://registry.npmjs.org/bare-https/-/bare-https-2.1.3.tgz", | |
| 378 | "integrity": "sha512-0TI/mJXQGYXmG7UUyWEG+KCJusayIAQLywUjFAskDoKuxfqVnGF+M/mTMrEV8J64DaIdU+5x761FvgmT7M68tA==", | |
| 379 | "license": "Apache-2.0", | |
| 380 | "dependencies": { | |
| 381 | "bare-http1": "^4.4.0", | |
| 382 | "bare-tcp": "^2.2.0", | |
| 383 | "bare-tls": "^2.0.0" | |
| 384 | } | |
| 385 | }, | |
| 187 | 386 | "node_modules/bare-inspect": { |
| 188 | 387 | "version": "3.1.4", |
| 189 | 388 | "resolved": "https://registry.npmjs.org/bare-inspect/-/bare-inspect-3.1.4.tgz", |
| … | … | @@ -197,6 +396,71 @@ |
| 197 | 396 | "bare": ">=1.18.0" |
| 198 | 397 | } |
| 199 | 398 | }, |
| 399 | "node_modules/bare-inspector": { | |
| 400 | "version": "4.0.3", | |
| 401 | "resolved": "https://registry.npmjs.org/bare-inspector/-/bare-inspector-4.0.3.tgz", | |
| 402 | "integrity": "sha512-yYk6+7tTVKJKHOja5+QBn989ETv8oVsmwoH0nCM+QYC6w5MC03GC4aQNK+lipAZSz1EaAj42WZ8Wewjpy0L9TA==", | |
| 403 | "license": "Apache-2.0", | |
| 404 | "dependencies": { | |
| 405 | "bare-events": "^2.1.0", | |
| 406 | "bare-http1": "^4.0.0", | |
| 407 | "bare-stream": "^2.0.0", | |
| 408 | "bare-url": "^2.0.0", | |
| 409 | "bare-ws": "^2.0.0" | |
| 410 | }, | |
| 411 | "engines": { | |
| 412 | "bare": ">=1.2.0" | |
| 413 | }, | |
| 414 | "peerDependencies": { | |
| 415 | "bare-tcp": "*" | |
| 416 | }, | |
| 417 | "peerDependenciesMeta": { | |
| 418 | "bare-tcp": { | |
| 419 | "optional": true | |
| 420 | } | |
| 421 | } | |
| 422 | }, | |
| 423 | "node_modules/bare-module": { | |
| 424 | "version": "6.1.3", | |
| 425 | "resolved": "https://registry.npmjs.org/bare-module/-/bare-module-6.1.3.tgz", | |
| 426 | "integrity": "sha512-5XWsVHsvtWMH4tK4DQWgpNTV0t/sg3ZrAaQLIxrwjrS5+u8Q9vEgc/zQ4QaDPWDse/y/5h+d+YG1Q0JfSMt0zA==", | |
| 427 | "license": "Apache-2.0", | |
| 428 | "dependencies": { | |
| 429 | "bare-bundle": "^1.3.0", | |
| 430 | "bare-module-lexer": "^1.0.0", | |
| 431 | "bare-module-resolve": "^1.8.0", | |
| 432 | "bare-path": "^3.0.0", | |
| 433 | "bare-url": "^2.0.1" | |
| 434 | }, | |
| 435 | "engines": { | |
| 436 | "bare": ">=1.23.0" | |
| 437 | }, | |
| 438 | "peerDependencies": { | |
| 439 | "bare-buffer": "*" | |
| 440 | }, | |
| 441 | "peerDependenciesMeta": { | |
| 442 | "bare-buffer": { | |
| 443 | "optional": true | |
| 444 | } | |
| 445 | } | |
| 446 | }, | |
| 447 | "node_modules/bare-module-lexer": { | |
| 448 | "version": "1.4.7", | |
| 449 | "resolved": "https://registry.npmjs.org/bare-module-lexer/-/bare-module-lexer-1.4.7.tgz", | |
| 450 | "integrity": "sha512-0klU4eMsjh/wcxi8FdHmNom2j2F4kmkXOhyJFL9qTaSFp2lE3m6BtbKgMHY8R5miqC9r8/IfA8wzXnC5Os14WA==", | |
| 451 | "license": "Apache-2.0", | |
| 452 | "dependencies": { | |
| 453 | "require-addon": "^1.0.2" | |
| 454 | }, | |
| 455 | "peerDependencies": { | |
| 456 | "bare-buffer": "*" | |
| 457 | }, | |
| 458 | "peerDependenciesMeta": { | |
| 459 | "bare-buffer": { | |
| 460 | "optional": true | |
| 461 | } | |
| 462 | } | |
| 463 | }, | |
| 200 | 464 | "node_modules/bare-module-resolve": { |
| 201 | 465 | "version": "1.12.1", |
| 202 | 466 | "resolved": "https://registry.npmjs.org/bare-module-resolve/-/bare-module-resolve-1.12.1.tgz", |
| … | … | @@ -214,6 +478,18 @@ |
| 214 | 478 | } |
| 215 | 479 | } |
| 216 | 480 | }, |
| 481 | "node_modules/bare-net": { | |
| 482 | "version": "2.3.1", | |
| 483 | "resolved": "https://registry.npmjs.org/bare-net/-/bare-net-2.3.1.tgz", | |
| 484 | "integrity": "sha512-MypSqDKpDU2Xt7FIfazn5yGvRnV09gFcIPHGWstW0gxuzA4tucTcwJSZeos97C4F89vtU5oGwXDN/HrGN6Y4Jw==", | |
| 485 | "license": "Apache-2.0", | |
| 486 | "dependencies": { | |
| 487 | "bare-events": "^2.2.2", | |
| 488 | "bare-pipe": "^4.0.0", | |
| 489 | "bare-stream": "^2.0.0", | |
| 490 | "bare-tcp": "^2.0.0" | |
| 491 | } | |
| 492 | }, | |
| 217 | 493 | "node_modules/bare-os": { |
| 218 | 494 | "version": "3.8.0", |
| 219 | 495 | "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.8.0.tgz", |
| … | … | @@ -306,6 +582,67 @@ |
| 306 | 582 | } |
| 307 | 583 | } |
| 308 | 584 | }, |
| 585 | "node_modules/bare-stylize": { | |
| 586 | "version": "0.0.1", | |
| 587 | "resolved": "https://registry.npmjs.org/bare-stylize/-/bare-stylize-0.0.1.tgz", | |
| 588 | "integrity": "sha512-l3MjmIl476bWijYWf3RbE+osl4iuXSOMudzp0vAqzIK7gPgn/+G3oAxp8Oin9CFF911KBP0LO9kts8Ci8mGZaQ==", | |
| 589 | "license": "Apache-2.0", | |
| 590 | "dependencies": { | |
| 591 | "bare-ansi-escapes": "^2.2.3", | |
| 592 | "bare-process": "^4.2.1" | |
| 593 | } | |
| 594 | }, | |
| 595 | "node_modules/bare-subprocess": { | |
| 596 | "version": "5.2.3", | |
| 597 | "resolved": "https://registry.npmjs.org/bare-subprocess/-/bare-subprocess-5.2.3.tgz", | |
| 598 | "integrity": "sha512-07wwswlV7M3sC9IykbZRZ/jHAkrXFWVLqdBWGv1y0ojCimtRD9hGwxdHmR5FUFmDUZLNsBmTYJNQqgio5+A85Q==", | |
| 599 | "license": "Apache-2.0", | |
| 600 | "dependencies": { | |
| 601 | "bare-env": "^3.0.0", | |
| 602 | "bare-events": "^2.5.4", | |
| 603 | "bare-os": "^3.0.1", | |
| 604 | "bare-pipe": "^4.0.0", | |
| 605 | "bare-url": "^2.2.2" | |
| 606 | }, | |
| 607 | "engines": { | |
| 608 | "bare": ">=1.7.0" | |
| 609 | }, | |
| 610 | "peerDependencies": { | |
| 611 | "bare-buffer": "*" | |
| 612 | }, | |
| 613 | "peerDependenciesMeta": { | |
| 614 | "bare-buffer": { | |
| 615 | "optional": true | |
| 616 | } | |
| 617 | } | |
| 618 | }, | |
| 619 | "node_modules/bare-tcp": { | |
| 620 | "version": "2.2.7", | |
| 621 | "resolved": "https://registry.npmjs.org/bare-tcp/-/bare-tcp-2.2.7.tgz", | |
| 622 | "integrity": "sha512-rjpqNQ2cOCkNo3NeYA/W4GTK3DRkl8sDHO3uos+AEswUjLC8XXMQF8WrJCSjlIowCbS6NUVxKE92X5RGXjyefg==", | |
| 623 | "license": "Apache-2.0", | |
| 624 | "dependencies": { | |
| 625 | "bare-dns": "^2.0.4", | |
| 626 | "bare-events": "^2.5.4", | |
| 627 | "bare-stream": "^2.6.4" | |
| 628 | }, | |
| 629 | "engines": { | |
| 630 | "bare": ">=1.16.0" | |
| 631 | } | |
| 632 | }, | |
| 633 | "node_modules/bare-tls": { | |
| 634 | "version": "2.2.1", | |
| 635 | "resolved": "https://registry.npmjs.org/bare-tls/-/bare-tls-2.2.1.tgz", | |
| 636 | "integrity": "sha512-hZ+ZqwrUO4dyH7/6WYkYWjgAFNJKjzwEYJiDaMnMs+eRleBDjQ3CvNZawpkw0Ar9jnM9NZK6+f6GqjkZ2FLGmQ==", | |
| 637 | "license": "Apache-2.0", | |
| 638 | "dependencies": { | |
| 639 | "bare-net": "^2.0.1", | |
| 640 | "bare-stream": "^2.6.4" | |
| 641 | }, | |
| 642 | "engines": { | |
| 643 | "bare": ">=1.7.0" | |
| 644 | } | |
| 645 | }, | |
| 309 | 646 | "node_modules/bare-tty": { |
| 310 | 647 | "version": "5.0.3", |
| 311 | 648 | "resolved": "https://registry.npmjs.org/bare-tty/-/bare-tty-5.0.3.tgz", |
| … | … | @@ -334,16 +671,137 @@ |
| 334 | 671 | "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.0.tgz", |
| 335 | 672 | "integrity": "sha512-NSTU5WN+fy/L0DDenfE8SXQna4voXuW0FHM7wH8i3/q9khUSchfPbPezO4zSFMnDGIf9YE+mt/RWhZgNRKRIXA==", |
| 336 | 673 | "license": "Apache-2.0", |
| 674 | "peer": true, | |
| 337 | 675 | "dependencies": { |
| 338 | 676 | "bare-path": "^3.0.0" |
| 339 | 677 | } |
| 340 | 678 | }, |
| 679 | "node_modules/bare-utils": { | |
| 680 | "version": "1.6.0", | |
| 681 | "resolved": "https://registry.npmjs.org/bare-utils/-/bare-utils-1.6.0.tgz", | |
| 682 | "integrity": "sha512-WhQEIkkAxkSnW7u1QgrI0AfNm5JpMruETXeYsb5qnkBJ0TTfNKygZmsh6rkoHBANaV+C/7Jed7bJP9OmEHG7rQ==", | |
| 683 | "license": "Apache-2.0", | |
| 684 | "dependencies": { | |
| 685 | "bare-debug-log": "^2.0.0", | |
| 686 | "bare-encoding": "^1.0.0", | |
| 687 | "bare-format": "^1.0.0", | |
| 688 | "bare-inspect": "^3.0.0", | |
| 689 | "bare-stylize": "^0.0.1", | |
| 690 | "bare-type": "^1.0.6" | |
| 691 | } | |
| 692 | }, | |
| 693 | "node_modules/bare-v8-to-istanbul": { | |
| 694 | "version": "1.0.2", | |
| 695 | "resolved": "https://registry.npmjs.org/bare-v8-to-istanbul/-/bare-v8-to-istanbul-1.0.2.tgz", | |
| 696 | "integrity": "sha512-CM0wPgyQtXTQ7h4q8NjbTZ/FH5he0Jkiwgpq0lFZZA5tkVZzmQ/9ul4R7E+cNlGZJIZlQZ9v4CwbgVaFG3zOAA==", | |
| 697 | "license": "Apache-2.0", | |
| 698 | "dependencies": { | |
| 699 | "bare-assert": "^1.0.2", | |
| 700 | "bare-fs": "^4.1.2", | |
| 701 | "bare-module": "^6.1.2", | |
| 702 | "bare-path": "^3.0.0", | |
| 703 | "bare-process": "^4.2.0", | |
| 704 | "bare-url": "^2.1.5", | |
| 705 | "bare-utils": "^1.2.0", | |
| 706 | "v8-to-istanbul": "^9.3.0", | |
| 707 | "which-runtime": "^1.2.1" | |
| 708 | } | |
| 709 | }, | |
| 710 | "node_modules/bare-ws": { | |
| 711 | "version": "2.1.0", | |
| 712 | "resolved": "https://registry.npmjs.org/bare-ws/-/bare-ws-2.1.0.tgz", | |
| 713 | "integrity": "sha512-2gEWlPK9iyBchACdIY6oQXgmDz3KLrChdwrPgmU3IVXOFTnxTqSUT27WE/+izd4QojHj/SsqDkQiD2HDvuTdAA==", | |
| 714 | "license": "Apache-2.0", | |
| 715 | "dependencies": { | |
| 716 | "bare-crypto": "^1.2.0", | |
| 717 | "bare-events": "^2.3.1", | |
| 718 | "bare-http1": "^4.0.0", | |
| 719 | "bare-https": "^2.0.0", | |
| 720 | "bare-stream": "^2.1.2" | |
| 721 | }, | |
| 722 | "peerDependencies": { | |
| 723 | "bare-buffer": "*", | |
| 724 | "bare-url": "*" | |
| 725 | }, | |
| 726 | "peerDependenciesMeta": { | |
| 727 | "bare-buffer": { | |
| 728 | "optional": true | |
| 729 | }, | |
| 730 | "bare-url": { | |
| 731 | "optional": true | |
| 732 | } | |
| 733 | } | |
| 734 | }, | |
| 341 | 735 | "node_modules/big-sparse-array": { |
| 342 | 736 | "version": "1.0.3", |
| 343 | 737 | "resolved": "https://registry.npmjs.org/big-sparse-array/-/big-sparse-array-1.0.3.tgz", |
| 344 | 738 | "integrity": "sha512-6RjV/3mSZORlMdpUaQ6rUSpG637cZm0//E54YYGtQg1c1O+AbZP8UTdJ/TchsDZcTVLmyWZcseBfp2HBeXUXOQ==", |
| 345 | 739 | "license": "MIT" |
| 346 | 740 | }, |
| 741 | "node_modules/bits-to-bytes": { | |
| 742 | "version": "1.3.0", | |
| 743 | "resolved": "https://registry.npmjs.org/bits-to-bytes/-/bits-to-bytes-1.3.0.tgz", | |
| 744 | "integrity": "sha512-OJoHTpFXS9bXHBCekGTByf3MqM8CGblBDIduKQeeVVeiU9dDWywSSirXIBYGgg3d1zbVuvnMa1vD4r6PA0kOKg==", | |
| 745 | "license": "ISC", | |
| 746 | "dependencies": { | |
| 747 | "b4a": "^1.5.0" | |
| 748 | } | |
| 749 | }, | |
| 750 | "node_modules/blind-relay": { | |
| 751 | "version": "1.4.0", | |
| 752 | "resolved": "https://registry.npmjs.org/blind-relay/-/blind-relay-1.4.0.tgz", | |
| 753 | "integrity": "sha512-6xt7fDfCs6eGmNNym6I9N42jmjcMQn2qwwOVnkP9ZnrkXFk6c4/tdO1xqRmDEzKzV8gigd+DVdCUG/RUYnen7Q==", | |
| 754 | "license": "Apache-2.0", | |
| 755 | "dependencies": { | |
| 756 | "b4a": "^1.6.4", | |
| 757 | "bare-events": "^2.2.0", | |
| 758 | "bits-to-bytes": "^1.3.0", | |
| 759 | "compact-encoding": "^2.12.0", | |
| 760 | "compact-encoding-bitfield": "^1.0.0", | |
| 761 | "protomux": "^3.5.1", | |
| 762 | "sodium-universal": "^5.0.0", | |
| 763 | "streamx": "^2.15.1" | |
| 764 | } | |
| 765 | }, | |
| 766 | "node_modules/bogon": { | |
| 767 | "version": "1.2.0", | |
| 768 | "resolved": "https://registry.npmjs.org/bogon/-/bogon-1.2.0.tgz", | |
| 769 | "integrity": "sha512-FqOBr/1VMzCOsoJd+fzNUMarUYki2+TKt07A2+xaulsNx4r53iJ7MV5k0jbqg7W2U0CsLqxCZOrFibdG6h6HSg==", | |
| 770 | "license": "MIT", | |
| 771 | "dependencies": { | |
| 772 | "compact-encoding": "^2.11.0", | |
| 773 | "compact-encoding-net": "^1.2.0" | |
| 774 | } | |
| 775 | }, | |
| 776 | "node_modules/brittle": { | |
| 777 | "version": "3.19.1", | |
| 778 | "resolved": "https://registry.npmjs.org/brittle/-/brittle-3.19.1.tgz", | |
| 779 | "integrity": "sha512-4Ted1Mt9o9B6oIA6ImJJCtB/Fv++ZO3IDNQgRcHOXWSYGRUidgxchaGrUBaRn+4mlneXrgInPkCPzi0jO8qKbA==", | |
| 780 | "license": "Apache-2.0", | |
| 781 | "dependencies": { | |
| 782 | "b4a": "^1.6.0", | |
| 783 | "bare-assert": "^1.0.2", | |
| 784 | "bare-cov": "^1.1.0", | |
| 785 | "bare-fs": "^4.1.6", | |
| 786 | "bare-os": "^3.6.1", | |
| 787 | "bare-path": "^3.0.0", | |
| 788 | "bare-process": "^4.2.1", | |
| 789 | "bare-subprocess": "^5.0.0", | |
| 790 | "bare-url": "^2.1.6", | |
| 791 | "error-stack-parser": "^2.1.4", | |
| 792 | "globbie": "^1.0.2", | |
| 793 | "paparam": "^1.6.2", | |
| 794 | "same-object": "^1.0.2", | |
| 795 | "test-tmp": "^1.4.0", | |
| 796 | "tmatch": "^5.0.0" | |
| 797 | }, | |
| 798 | "bin": { | |
| 799 | "brittle": "bin/node.js", | |
| 800 | "brittle-bare": "bin/bare.js", | |
| 801 | "brittle-node": "bin/node.js", | |
| 802 | "brittle-pear": "bin/pear.js" | |
| 803 | } | |
| 804 | }, | |
| 347 | 805 | "node_modules/codecs": { |
| 348 | 806 | "version": "3.1.0", |
| 349 | 807 | "resolved": "https://registry.npmjs.org/codecs/-/codecs-3.1.0.tgz", |
| … | … | @@ -362,6 +820,30 @@ |
| 362 | 820 | "b4a": "^1.3.0" |
| 363 | 821 | } |
| 364 | 822 | }, |
| 823 | "node_modules/compact-encoding-bitfield": { | |
| 824 | "version": "1.0.0", | |
| 825 | "resolved": "https://registry.npmjs.org/compact-encoding-bitfield/-/compact-encoding-bitfield-1.0.0.tgz", | |
| 826 | "integrity": "sha512-3nMVKUg+PF72UHfainmCL8uKvyWfxsjqOtUY+HiMPGLPCTjnwzoKfFAMo1Ad7nwTPdjBqtGK5b3BOFTFW4EBTg==", | |
| 827 | "license": "ISC", | |
| 828 | "dependencies": { | |
| 829 | "compact-encoding": "^2.4.1" | |
| 830 | } | |
| 831 | }, | |
| 832 | "node_modules/compact-encoding-net": { | |
| 833 | "version": "1.2.0", | |
| 834 | "resolved": "https://registry.npmjs.org/compact-encoding-net/-/compact-encoding-net-1.2.0.tgz", | |
| 835 | "integrity": "sha512-LVXpNpF7PGQeHRVVLGgYWzuVoYAaDZvKUsUxRioGfkotzvOh4AzoQF1HBH3zMNaSnx7gJXuUr3hkjnijaH/Eng==", | |
| 836 | "license": "ISC", | |
| 837 | "dependencies": { | |
| 838 | "compact-encoding": "^2.4.1" | |
| 839 | } | |
| 840 | }, | |
| 841 | "node_modules/convert-source-map": { | |
| 842 | "version": "2.0.0", | |
| 843 | "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", | |
| 844 | "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", | |
| 845 | "license": "MIT" | |
| 846 | }, | |
| 365 | 847 | "node_modules/core-coupler": { |
| 366 | 848 | "version": "2.0.0", |
| 367 | 849 | "resolved": "https://registry.npmjs.org/core-coupler/-/core-coupler-2.0.0.tgz", |
| … | … | @@ -407,6 +889,26 @@ |
| 407 | 889 | "ready-resource": "^1.2.0" |
| 408 | 890 | } |
| 409 | 891 | }, |
| 892 | "node_modules/dht-rpc": { | |
| 893 | "version": "6.26.3", | |
| 894 | "resolved": "https://registry.npmjs.org/dht-rpc/-/dht-rpc-6.26.3.tgz", | |
| 895 | "integrity": "sha512-KuLfRv/hecUHipQcTXHpVv4/N4Jhpww5sLdsrn3Edm5oHwzK9SgNV34hNt7a2aVZCOcG5SfP4AvcQ7pI+y9YNg==", | |
| 896 | "license": "MIT", | |
| 897 | "dependencies": { | |
| 898 | "adaptive-timeout": "^1.0.1", | |
| 899 | "b4a": "^1.6.1", | |
| 900 | "bare-events": "^2.2.0", | |
| 901 | "compact-encoding": "^2.11.0", | |
| 902 | "compact-encoding-net": "^1.2.0", | |
| 903 | "fast-fifo": "^1.1.0", | |
| 904 | "kademlia-routing-table": "^1.0.1", | |
| 905 | "nat-sampler": "^1.0.1", | |
| 906 | "sodium-universal": "^5.0.0", | |
| 907 | "streamx": "^2.13.2", | |
| 908 | "time-ordered-set": "^2.0.0", | |
| 909 | "udx-native": "^1.5.3" | |
| 910 | } | |
| 911 | }, | |
| 410 | 912 | "node_modules/encryption-encoding": { |
| 411 | 913 | "version": "1.0.3", |
| 412 | 914 | "resolved": "https://registry.npmjs.org/encryption-encoding/-/encryption-encoding-1.0.3.tgz", |
| … | … | @@ -416,6 +918,15 @@ |
| 416 | 918 | "hyperschema": "^1.19.0" |
| 417 | 919 | } |
| 418 | 920 | }, |
| 921 | "node_modules/error-stack-parser": { | |
| 922 | "version": "2.1.4", | |
| 923 | "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", | |
| 924 | "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", | |
| 925 | "license": "MIT", | |
| 926 | "dependencies": { | |
| 927 | "stackframe": "^1.3.4" | |
| 928 | } | |
| 929 | }, | |
| 419 | 930 | "node_modules/events-universal": { |
| 420 | 931 | "version": "1.0.1", |
| 421 | 932 | "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", |
| … | … | @@ -474,6 +985,18 @@ |
| 474 | 985 | "integrity": "sha512-IfTY0dKZM43ACyGvXkbG7De7WY7MxTS5VO6Juhe8oJKpCmrYYXoqp/cJMskkpi0k9H8wuXq0H+eI898/BCqvXg==", |
| 475 | 986 | "license": "MIT" |
| 476 | 987 | }, |
| 988 | "node_modules/globbie": { | |
| 989 | "version": "1.0.3", | |
| 990 | "resolved": "https://registry.npmjs.org/globbie/-/globbie-1.0.3.tgz", | |
| 991 | "integrity": "sha512-hcryJmKcftf82xfBWTWxuUITWIIoYO3ec104V17SMHGFl6VLbm1d1Ju9LX9L+jyJTwICpemX/dmPI/HGYoKr1A==", | |
| 992 | "license": "Apache-2.0", | |
| 993 | "dependencies": { | |
| 994 | "bare-fs": "^4.1.2", | |
| 995 | "bare-path": "^3.0.0", | |
| 996 | "bare-process": "^4.2.1", | |
| 997 | "picomatch": "^4.0.2" | |
| 998 | } | |
| 999 | }, | |
| 477 | 1000 | "node_modules/hyperbee": { |
| 478 | 1001 | "version": "2.27.3", |
| 479 | 1002 | "resolved": "https://registry.npmjs.org/hyperbee/-/hyperbee-2.27.3.tgz", |
| … | … | @@ -574,6 +1097,35 @@ |
| 574 | 1097 | "streamx": "^2.21.1" |
| 575 | 1098 | } |
| 576 | 1099 | }, |
| 1100 | "node_modules/hyperdht": { | |
| 1101 | "version": "6.29.6", | |
| 1102 | "resolved": "https://registry.npmjs.org/hyperdht/-/hyperdht-6.29.6.tgz", | |
| 1103 | "integrity": "sha512-bMMtw02fhiALdsTVLNZ/VFmMZuEY8kxb0/Rprl6oYF4/TCS653i3FDjtJeRbqtnAzwAj6dVZ/EwhoNOSCFGaYQ==", | |
| 1104 | "license": "MIT", | |
| 1105 | "dependencies": { | |
| 1106 | "@hyperswarm/secret-stream": "^6.6.2", | |
| 1107 | "b4a": "^1.3.1", | |
| 1108 | "bare-events": "^2.2.0", | |
| 1109 | "blind-relay": "^1.3.0", | |
| 1110 | "bogon": "^1.0.0", | |
| 1111 | "compact-encoding": "^2.4.1", | |
| 1112 | "dht-rpc": "^6.15.1", | |
| 1113 | "hypercore-crypto": "^3.3.0", | |
| 1114 | "hypercore-id-encoding": "^1.2.0", | |
| 1115 | "noise-curve-ed": "^2.0.0", | |
| 1116 | "noise-handshake": "^4.0.0", | |
| 1117 | "record-cache": "^1.1.1", | |
| 1118 | "safety-catch": "^1.0.1", | |
| 1119 | "signal-promise": "^1.0.3", | |
| 1120 | "sodium-universal": "^5.0.1", | |
| 1121 | "streamx": "^2.16.1", | |
| 1122 | "unslab": "^1.3.0", | |
| 1123 | "xache": "^1.1.0" | |
| 1124 | }, | |
| 1125 | "bin": { | |
| 1126 | "hyperdht": "bin.js" | |
| 1127 | } | |
| 1128 | }, | |
| 577 | 1129 | "node_modules/hyperschema": { |
| 578 | 1130 | "version": "1.20.1", |
| 579 | 1131 | "resolved": "https://registry.npmjs.org/hyperschema/-/hyperschema-1.20.1.tgz", |
| … | … | @@ -586,6 +1138,21 @@ |
| 586 | 1138 | "generate-string": "^1.0.1" |
| 587 | 1139 | } |
| 588 | 1140 | }, |
| 1141 | "node_modules/hyperswarm": { | |
| 1142 | "version": "4.17.0", | |
| 1143 | "resolved": "https://registry.npmjs.org/hyperswarm/-/hyperswarm-4.17.0.tgz", | |
| 1144 | "integrity": "sha512-oe86sK961Ueg7rvDN/veFwG8xH+Iv6vObPhGDkPJcDVxk/NduW41ZhAcVDnHzRbm7S0eLU7WaDUvehOYoKSpRQ==", | |
| 1145 | "license": "MIT", | |
| 1146 | "dependencies": { | |
| 1147 | "b4a": "^1.3.1", | |
| 1148 | "bare-events": "^2.2.0", | |
| 1149 | "hyperdht": "^6.21.0", | |
| 1150 | "safety-catch": "^1.0.2", | |
| 1151 | "shuffled-priority-queue": "^2.1.0", | |
| 1152 | "streamx": "^2.22.1", | |
| 1153 | "unslab": "^1.3.0" | |
| 1154 | } | |
| 1155 | }, | |
| 589 | 1156 | "node_modules/index-encoder": { |
| 590 | 1157 | "version": "3.5.0", |
| 591 | 1158 | "resolved": "https://registry.npmjs.org/index-encoder/-/index-encoder-3.5.0.tgz", |
| … | … | @@ -610,6 +1177,15 @@ |
| 610 | 1177 | "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", |
| 611 | 1178 | "license": "MIT" |
| 612 | 1179 | }, |
| 1180 | "node_modules/kademlia-routing-table": { | |
| 1181 | "version": "1.0.6", | |
| 1182 | "resolved": "https://registry.npmjs.org/kademlia-routing-table/-/kademlia-routing-table-1.0.6.tgz", | |
| 1183 | "integrity": "sha512-Ve6jwIlUCYvUzBnXnzVRHDZCFgXURW9gmF3r7n05kZs/2rNbLHXwGdcq0qIaSwdmJCvtosgR4JensnVU65hzNQ==", | |
| 1184 | "license": "MIT", | |
| 1185 | "dependencies": { | |
| 1186 | "bare-events": "^2.2.0" | |
| 1187 | } | |
| 1188 | }, | |
| 613 | 1189 | "node_modules/mutexify": { |
| 614 | 1190 | "version": "1.4.0", |
| 615 | 1191 | "resolved": "https://registry.npmjs.org/mutexify/-/mutexify-1.4.0.tgz", |
| … | … | @@ -625,6 +1201,12 @@ |
| 625 | 1201 | "integrity": "sha512-7vO7n28+aYO4J+8w96AzhmU8G+Y/xpPDJz/se19ICsqj/momRbb9mh9ZUtkoJ5X3nTnPdhEJyc0qnM6yAsHBaA==", |
| 626 | 1202 | "license": "ISC" |
| 627 | 1203 | }, |
| 1204 | "node_modules/nat-sampler": { | |
| 1205 | "version": "1.0.1", | |
| 1206 | "resolved": "https://registry.npmjs.org/nat-sampler/-/nat-sampler-1.0.1.tgz", | |
| 1207 | "integrity": "sha512-yQvyNN7xbqR8crTKk3U8gRgpcV1Az+vfCEijiHu9oHHsnIl8n3x+yXNHl42M6L3czGynAVoOT9TqBfS87gDdcw==", | |
| 1208 | "license": "MIT" | |
| 1209 | }, | |
| 628 | 1210 | "node_modules/noise-curve-ed": { |
| 629 | 1211 | "version": "2.1.0", |
| 630 | 1212 | "resolved": "https://registry.npmjs.org/noise-curve-ed/-/noise-curve-ed-2.1.0.tgz", |
| … | … | @@ -653,6 +1235,18 @@ |
| 653 | 1235 | "integrity": "sha512-viyQI64VIja0Za3njIzhoEP8ZVkgPowhZPuG0E96NwBfYJ6ZIyrrlhWGtFPkdN7eYLl2L8CTWL+wla0evm1KKQ==", |
| 654 | 1236 | "license": "Apache-2.0" |
| 655 | 1237 | }, |
| 1238 | "node_modules/picomatch": { | |
| 1239 | "version": "4.0.4", | |
| 1240 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", | |
| 1241 | "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", | |
| 1242 | "license": "MIT", | |
| 1243 | "engines": { | |
| 1244 | "node": ">=12" | |
| 1245 | }, | |
| 1246 | "funding": { | |
| 1247 | "url": "https://github.com/sponsors/jonschlinkert" | |
| 1248 | } | |
| 1249 | }, | |
| 656 | 1250 | "node_modules/protocol-buffers-encodings": { |
| 657 | 1251 | "version": "1.2.0", |
| 658 | 1252 | "resolved": "https://registry.npmjs.org/protocol-buffers-encodings/-/protocol-buffers-encodings-1.2.0.tgz", |
| … | … | @@ -739,6 +1333,15 @@ |
| 739 | 1333 | "bare-events": "^2.2.0" |
| 740 | 1334 | } |
| 741 | 1335 | }, |
| 1336 | "node_modules/record-cache": { | |
| 1337 | "version": "1.2.0", | |
| 1338 | "resolved": "https://registry.npmjs.org/record-cache/-/record-cache-1.2.0.tgz", | |
| 1339 | "integrity": "sha512-kyy3HWCez2WrotaL3O4fTn0rsIdfRKOdQQcEJ9KpvmKmbffKVvwsloX063EgRUlpJIXHiDQFhJcTbZequ2uTZw==", | |
| 1340 | "license": "MIT", | |
| 1341 | "dependencies": { | |
| 1342 | "b4a": "^1.3.1" | |
| 1343 | } | |
| 1344 | }, | |
| 742 | 1345 | "node_modules/refcounter": { |
| 743 | 1346 | "version": "1.0.0", |
| 744 | 1347 | "resolved": "https://registry.npmjs.org/refcounter/-/refcounter-1.0.0.tgz", |
| … | … | @@ -793,12 +1396,27 @@ |
| 793 | 1396 | "integrity": "sha512-Zq+J1TefpoEq/HTUabo0YXX5MNvttjWYODGohgPBO2jfko8Wqx3JYMgE823szDFVamdH5PlpByvfiWScTdSYDA==", |
| 794 | 1397 | "license": "MIT" |
| 795 | 1398 | }, |
| 1399 | "node_modules/same-object": { | |
| 1400 | "version": "1.0.2", | |
| 1401 | "resolved": "https://registry.npmjs.org/same-object/-/same-object-1.0.2.tgz", | |
| 1402 | "integrity": "sha512-csHWhvUsLbIOHDM/nP+KHWM+BLPsIzWkFa8HbzaI0G7BqKXgx+7FJpKTGgLXyz5amfdY2OVBcmXTqYOMEk04og==", | |
| 1403 | "license": "MIT" | |
| 1404 | }, | |
| 796 | 1405 | "node_modules/scope-lock": { |
| 797 | 1406 | "version": "1.2.4", |
| 798 | 1407 | "resolved": "https://registry.npmjs.org/scope-lock/-/scope-lock-1.2.4.tgz", |
| 799 | 1408 | "integrity": "sha512-BpSd8VCuCxW9ZitcdIC/vjs3gMaP9bRBL5nkHcyfX2VrS52n13/rHuBA2xJ/S/4DPuRdAO/Bk8pWd8eD/gHCIA==", |
| 800 | 1409 | "license": "Apache-2.0" |
| 801 | 1410 | }, |
| 1411 | "node_modules/shuffled-priority-queue": { | |
| 1412 | "version": "2.1.0", | |
| 1413 | "resolved": "https://registry.npmjs.org/shuffled-priority-queue/-/shuffled-priority-queue-2.1.0.tgz", | |
| 1414 | "integrity": "sha512-xhdh7fHyMsr0m/w2kDfRJuBFRS96b9l8ZPNWGaQ+PMvnUnZ/Eh+gJJ9NsHBd7P9k0399WYlCLzsy18EaMfyadA==", | |
| 1415 | "license": "MIT", | |
| 1416 | "dependencies": { | |
| 1417 | "unordered-set": "^2.0.1" | |
| 1418 | } | |
| 1419 | }, | |
| 802 | 1420 | "node_modules/signal-promise": { |
| 803 | 1421 | "version": "1.0.3", |
| 804 | 1422 | "resolved": "https://registry.npmjs.org/signal-promise/-/signal-promise-1.0.3.tgz", |
| … | … | @@ -878,6 +1496,12 @@ |
| 878 | 1496 | } |
| 879 | 1497 | } |
| 880 | 1498 | }, |
| 1499 | "node_modules/stackframe": { | |
| 1500 | "version": "1.3.4", | |
| 1501 | "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", | |
| 1502 | "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", | |
| 1503 | "license": "MIT" | |
| 1504 | }, | |
| 881 | 1505 | "node_modules/streamx": { |
| 882 | 1506 | "version": "2.25.0", |
| 883 | 1507 | "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz", |
| … | … | @@ -908,6 +1532,17 @@ |
| 908 | 1532 | "streamx": "^2.12.5" |
| 909 | 1533 | } |
| 910 | 1534 | }, |
| 1535 | "node_modules/test-tmp": { | |
| 1536 | "version": "1.4.0", | |
| 1537 | "resolved": "https://registry.npmjs.org/test-tmp/-/test-tmp-1.4.0.tgz", | |
| 1538 | "integrity": "sha512-GVggxGg+jXqP2Wbju50JVLo+9E+nIOPPyWqgr63EbOnNItIKu1cEbJpTWAJeflnyGqXOtcMI7ijHRp88GUkfDA==", | |
| 1539 | "license": "MIT", | |
| 1540 | "dependencies": { | |
| 1541 | "bare-fs": "^4.0.1", | |
| 1542 | "bare-os": "^3.3.0", | |
| 1543 | "bare-path": "^3.0.0" | |
| 1544 | } | |
| 1545 | }, | |
| 911 | 1546 | "node_modules/text-decoder": { |
| 912 | 1547 | "version": "1.2.7", |
| 913 | 1548 | "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", |
| … | … | @@ -917,6 +1552,12 @@ |
| 917 | 1552 | "b4a": "^1.6.4" |
| 918 | 1553 | } |
| 919 | 1554 | }, |
| 1555 | "node_modules/time-ordered-set": { | |
| 1556 | "version": "2.0.1", | |
| 1557 | "resolved": "https://registry.npmjs.org/time-ordered-set/-/time-ordered-set-2.0.1.tgz", | |
| 1558 | "integrity": "sha512-VJEKmgSN2UiOLB8BpN8Sh2b9LGMHTP5OPrQRpnKjvOheOyzk0mufbjzjKTIG2gO4A+Y+vDJ+0TcLbpUmMLsg8A==", | |
| 1559 | "license": "MIT" | |
| 1560 | }, | |
| 920 | 1561 | "node_modules/timeout-refresh": { |
| 921 | 1562 | "version": "2.0.1", |
| 922 | 1563 | "resolved": "https://registry.npmjs.org/timeout-refresh/-/timeout-refresh-2.0.1.tgz", |
| … | … | @@ -932,6 +1573,36 @@ |
| 932 | 1573 | "b4a": "^1.6.0" |
| 933 | 1574 | } |
| 934 | 1575 | }, |
| 1576 | "node_modules/tmatch": { | |
| 1577 | "version": "5.0.0", | |
| 1578 | "resolved": "https://registry.npmjs.org/tmatch/-/tmatch-5.0.0.tgz", | |
| 1579 | "integrity": "sha512-Ib9OtBkpHn07tXP04SlN1SYRxFgTk6wSM2EBmjjxug4u5RXPRVLkdFJSS1PmrQidaSB8Lru9nRtViQBsbxzE5Q==", | |
| 1580 | "license": "ISC", | |
| 1581 | "engines": { | |
| 1582 | "node": ">=8" | |
| 1583 | } | |
| 1584 | }, | |
| 1585 | "node_modules/udx-native": { | |
| 1586 | "version": "1.19.2", | |
| 1587 | "resolved": "https://registry.npmjs.org/udx-native/-/udx-native-1.19.2.tgz", | |
| 1588 | "integrity": "sha512-RNYh+UhfryCsF5hE2ZOuIqcZ+qdipXK3UsarwxWJwsUQZFE3ybwz0mPjwb5ev1PMBcjFahWiepS/q0wwL51c2g==", | |
| 1589 | "license": "Apache-2.0", | |
| 1590 | "dependencies": { | |
| 1591 | "b4a": "^1.5.0", | |
| 1592 | "bare-events": "^2.2.0", | |
| 1593 | "require-addon": "^1.1.0", | |
| 1594 | "streamx": "^2.22.0" | |
| 1595 | }, | |
| 1596 | "engines": { | |
| 1597 | "bare": ">=1.17.4" | |
| 1598 | } | |
| 1599 | }, | |
| 1600 | "node_modules/unordered-set": { | |
| 1601 | "version": "2.0.1", | |
| 1602 | "resolved": "https://registry.npmjs.org/unordered-set/-/unordered-set-2.0.1.tgz", | |
| 1603 | "integrity": "sha512-eUmNTPzdx+q/WvOHW0bgGYLWvWHNT3PTKEQLg0MAQhc0AHASHVHoP/9YytYd4RBVariqno/mEUhVZN98CmD7bg==", | |
| 1604 | "license": "MIT" | |
| 1605 | }, | |
| 935 | 1606 | "node_modules/unslab": { |
| 936 | 1607 | "version": "1.3.0", |
| 937 | 1608 | "resolved": "https://registry.npmjs.org/unslab/-/unslab-1.3.0.tgz", |
| … | … | @@ -941,6 +1612,20 @@ |
| 941 | 1612 | "b4a": "^1.6.6" |
| 942 | 1613 | } |
| 943 | 1614 | }, |
| 1615 | "node_modules/v8-to-istanbul": { | |
| 1616 | "version": "9.3.0", | |
| 1617 | "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", | |
| 1618 | "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", | |
| 1619 | "license": "ISC", | |
| 1620 | "dependencies": { | |
| 1621 | "@jridgewell/trace-mapping": "^0.3.12", | |
| 1622 | "@types/istanbul-lib-coverage": "^2.0.1", | |
| 1623 | "convert-source-map": "^2.0.0" | |
| 1624 | }, | |
| 1625 | "engines": { | |
| 1626 | "node": ">=10.12.0" | |
| 1627 | } | |
| 1628 | }, | |
| 944 | 1629 | "node_modules/varint": { |
| 945 | 1630 | "version": "5.0.0", |
| 946 | 1631 | "resolved": "https://registry.npmjs.org/varint/-/varint-5.0.0.tgz", |
| … | … | @@ -953,6 +1638,12 @@ |
| 953 | 1638 | "integrity": "sha512-5kwCfWml7+b2NO7KrLMhYihjRx0teKkd3yGp1Xk5Vaf2JGdSh+rgVhEALAD9c/59dP+YwJHXoEO7e8QPy7gOkw==", |
| 954 | 1639 | "license": "Apache-2.0" |
| 955 | 1640 | }, |
| 1641 | "node_modules/xache": { | |
| 1642 | "version": "1.2.1", | |
| 1643 | "resolved": "https://registry.npmjs.org/xache/-/xache-1.2.1.tgz", | |
| 1644 | "integrity": "sha512-igRS6jPreJ54ABdzhh4mCDXcz+XMaWO2q1ABRV2yWYuk29jlp8VT7UBdCqNkX7rpYBbXsebVVKkwIuYZjyZNqA==", | |
| 1645 | "license": "MIT" | |
| 1646 | }, | |
| 956 | 1647 | "node_modules/z32": { |
| 957 | 1648 | "version": "1.1.0", |
| 958 | 1649 | "resolved": "https://registry.npmjs.org/z32/-/z32-1.1.0.tgz", |
| … | … | @@ -8,12 +8,16 @@ |
| 8 | 8 | "autobase-doctor": "./bin/autobase-doctor.js" |
| 9 | 9 | }, |
| 10 | 10 | "dependencies": { |
| 11 | "@hyperswarm/testnet": "^3.1.4", | |
| 11 | 12 | "autobase": "^7.27.3", |
| 13 | "autobase-test-helpers": "^3.1.0", | |
| 12 | 14 | "bare-path": "^3.0.0", |
| 13 | 15 | "bare-process": "^4.3.0", |
| 16 | "brittle": "^3.19.1", | |
| 14 | 17 | "corestore": "^7.9.2", |
| 15 | 18 | "hyperbee": "^2.22.5", |
| 16 | 19 | "hypercore": "^11.7.3", |
| 20 | "hyperswarm": "^4.17.0", | |
| 17 | 21 | "paparam": "^1.10.1" |
| 18 | 22 | } |
| 19 | 23 | } |
| … | … | @@ -0,0 +1,422 @@ |
| 1 | #!/usr/bin/env bare | |
| 2 | ||
| 3 | /** | |
| 4 | * Edge case tests for Autobase multi-writer over Hyperswarm. | |
| 5 | * These cover scenarios that could break in production. | |
| 6 | */ | |
| 7 | ||
| 8 | import test from 'brittle' | |
| 9 | import Corestore from 'corestore' | |
| 10 | import Autobase from 'autobase' | |
| 11 | import Hyperbee from 'hyperbee' | |
| 12 | import Hyperswarm from 'hyperswarm' | |
| 13 | import createTestnet from '@hyperswarm/testnet' | |
| 14 | import os from 'bare-os' | |
| 15 | import path from 'bare-path' | |
| 16 | import fs from 'bare-fs' | |
| 17 | ||
| 18 | const tmpdir = os.tmpdir() | |
| 19 | let idx = 0 | |
| 20 | function tmpPath (name) { | |
| 21 | return path.join(tmpdir, 'ab-edge-' + name + '-' + (idx++) + '-' + Date.now()) | |
| 22 | } | |
| 23 | async function cleanup (...dirs) { | |
| 24 | for (const d of dirs) await fs.promises.rm(d, { recursive: true }).catch(() => {}) | |
| 25 | } | |
| 26 | ||
| 27 | function open (store) { | |
| 28 | return new Hyperbee(store.get('view', { valueEncoding: 'json' }), { | |
| 29 | keyEncoding: 'utf-8', | |
| 30 | valueEncoding: 'json' | |
| 31 | }) | |
| 32 | } | |
| 33 | ||
| 34 | // Mimics pear-git LinkedRepo apply — with probe validation | |
| 35 | async function peerGitApply (batch, view, host) { | |
| 36 | const b = view.batch() | |
| 37 | for (const node of batch) { | |
| 38 | const op = node.value | |
| 39 | if (!op || !op.type) continue | |
| 40 | ||
| 41 | switch (op.type) { | |
| 42 | case 'create-issue': { | |
| 43 | await b.put('issue:' + op.id, { | |
| 44 | id: op.id, | |
| 45 | title: op.title, | |
| 46 | body: op.body || '', | |
| 47 | author: op.author || 'unknown', | |
| 48 | status: 'open', | |
| 49 | createdAt: op.timestamp || Date.now() | |
| 50 | }) | |
| 51 | const counter = await b.get('counter:issues') | |
| 52 | const next = counter ? counter.value.next : 0 | |
| 53 | if (op.id >= next) await b.put('counter:issues', { next: op.id + 1 }) | |
| 54 | break | |
| 55 | } | |
| 56 | case 'update-issue': { | |
| 57 | const entry = await b.get('issue:' + op.id) | |
| 58 | if (entry) { | |
| 59 | const issue = entry.value | |
| 60 | if (op.status) issue.status = op.status | |
| 61 | if (op.title) issue.title = op.title | |
| 62 | await b.put('issue:' + op.id, issue) | |
| 63 | } | |
| 64 | break | |
| 65 | } | |
| 66 | case 'add-writer': { | |
| 67 | if (op.key) { | |
| 68 | try { | |
| 69 | await host.addWriter(Buffer.from(op.key, 'hex'), { indexer: op.indexer !== false }) | |
| 70 | } catch {} | |
| 71 | } | |
| 72 | break | |
| 73 | } | |
| 74 | } | |
| 75 | } | |
| 76 | await b.flush() | |
| 77 | } | |
| 78 | ||
| 79 | function createBase (store, key) { | |
| 80 | return new Autobase(store.session(), key, { | |
| 81 | open, | |
| 82 | apply: peerGitApply, | |
| 83 | valueEncoding: 'json', | |
| 84 | ackInterval: 0, | |
| 85 | ackThreshold: 0 | |
| 86 | }) | |
| 87 | } | |
| 88 | ||
| 89 | async function swarmReplicate (storeA, storeB, topic, testnet) { | |
| 90 | const { bootstrap } = testnet | |
| 91 | const swarmA = new Hyperswarm({ bootstrap }) | |
| 92 | const swarmB = new Hyperswarm({ bootstrap }) | |
| 93 | swarmA.on('connection', (conn) => storeA.replicate(conn)) | |
| 94 | swarmB.on('connection', (conn) => storeB.replicate(conn)) | |
| 95 | await swarmA.join(topic, { server: true, client: false }).flushed() | |
| 96 | swarmB.join(topic, { client: true, server: false }) | |
| 97 | await swarmB.flush() | |
| 98 | return { swarmA, swarmB } | |
| 99 | } | |
| 100 | ||
| 101 | async function waitForKey (bases, key, maxWait = 10000) { | |
| 102 | const start = Date.now() | |
| 103 | while (Date.now() - start < maxWait) { | |
| 104 | for (const b of bases) await b.update() | |
| 105 | let allHave = true | |
| 106 | for (const b of bases) { | |
| 107 | if (!(await b.view.get(key))) { allHave = false; break } | |
| 108 | } | |
| 109 | if (allHave) return true | |
| 110 | await new Promise(r => setTimeout(r, 300)) | |
| 111 | } | |
| 112 | return false | |
| 113 | } | |
| 114 | ||
| 115 | // ───────────────────────────────────────────────────────────── | |
| 116 | // Test 1: Reboot persistence — close, reopen, verify multi-writer state | |
| 117 | // ───────────────────────────────────────────────────────────── | |
| 118 | ||
| 119 | test('reboot: multi-writer state persists after close+reopen', async (t) => { | |
| 120 | t.timeout(30000) | |
| 121 | const testnet = await createTestnet(3) | |
| 122 | const dirA = tmpPath('a') | |
| 123 | const dirB = tmpPath('b') | |
| 124 | ||
| 125 | const storeA = new Corestore(dirA, { primaryKey: Buffer.alloc(32, 0), unsafe: true }) | |
| 126 | const storeB = new Corestore(dirB, { primaryKey: Buffer.alloc(32, 1), unsafe: true }) | |
| 127 | ||
| 128 | // Setup multi-writer | |
| 129 | const a = createBase(storeA, null) | |
| 130 | await a.ready() | |
| 131 | const bootstrapKey = a.local.key | |
| 132 | ||
| 133 | const b = createBase(storeB, bootstrapKey) | |
| 134 | await b.ready() | |
| 135 | ||
| 136 | const { swarmA, swarmB } = await swarmReplicate(storeA, storeB, a.discoveryKey, testnet) | |
| 137 | ||
| 138 | await a.append({ type: 'create-issue', id: 1, title: 'from A before writer add', author: 'serverA' }) | |
| 139 | await a.append({ type: 'add-writer', key: b.local.key.toString('hex'), indexer: true }) | |
| 140 | ||
| 141 | await waitForKey([a, b], 'issue:1') | |
| 142 | ||
| 143 | // Confirm writer set | |
| 144 | for (let i = 0; i < 3; i++) { | |
| 145 | if (a.writable) await a.append(null) | |
| 146 | if (b.writable) await b.append(null) | |
| 147 | for (const base of [a, b]) await base.update() | |
| 148 | await new Promise(r => setTimeout(r, 300)) | |
| 149 | } | |
| 150 | ||
| 151 | t.ok(b.writable, 'B writable before reboot') | |
| 152 | ||
| 153 | // B writes an issue | |
| 154 | await b.append({ type: 'create-issue', id: 2, title: 'from B before reboot', author: 'serverB' }) | |
| 155 | await waitForKey([a, b], 'issue:2') | |
| 156 | ||
| 157 | // Close everything | |
| 158 | await swarmA.destroy() | |
| 159 | await swarmB.destroy() | |
| 160 | await a.close() | |
| 161 | await b.close() | |
| 162 | await storeA.close() | |
| 163 | await storeB.close() | |
| 164 | ||
| 165 | // Reopen — simulates server reboot | |
| 166 | const storeA2 = new Corestore(dirA, { primaryKey: Buffer.alloc(32, 0), unsafe: true }) | |
| 167 | const storeB2 = new Corestore(dirB, { primaryKey: Buffer.alloc(32, 1), unsafe: true }) | |
| 168 | ||
| 169 | const a2 = createBase(storeA2, bootstrapKey) | |
| 170 | await a2.ready() | |
| 171 | ||
| 172 | const b2 = createBase(storeB2, bootstrapKey) | |
| 173 | await b2.ready() | |
| 174 | ||
| 175 | t.comment('A2 writable: ' + a2.writable + ', members: ' + a2.system.members) | |
| 176 | t.comment('B2 writable: ' + b2.writable + ', members: ' + b2.system.members) | |
| 177 | ||
| 178 | // Reconnect | |
| 179 | const testnet2 = await createTestnet(3) | |
| 180 | const { swarmA: sw2A, swarmB: sw2B } = await swarmReplicate(storeA2, storeB2, a2.discoveryKey, testnet2) | |
| 181 | ||
| 182 | await waitForKey([a2, b2], 'issue:2') | |
| 183 | ||
| 184 | // Verify all data survived reboot | |
| 185 | const i1a = await a2.view.get('issue:1') | |
| 186 | const i2a = await a2.view.get('issue:2') | |
| 187 | const i1b = await b2.view.get('issue:1') | |
| 188 | const i2b = await b2.view.get('issue:2') | |
| 189 | ||
| 190 | t.ok(i1a, 'A2 has issue:1') | |
| 191 | t.ok(i2a, 'A2 has issue:2') | |
| 192 | t.ok(i1b, 'B2 has issue:1') | |
| 193 | t.ok(i2b, 'B2 has issue:2') | |
| 194 | t.is(i1a.value.author, 'serverA') | |
| 195 | t.is(i2a.value.author, 'serverB') | |
| 196 | ||
| 197 | // B is still writable after reboot | |
| 198 | t.ok(b2.writable, 'B2 still writable after reboot') | |
| 199 | ||
| 200 | // B can still write new issues after reboot | |
| 201 | await b2.append({ type: 'create-issue', id: 3, title: 'from B after reboot', author: 'serverB' }) | |
| 202 | await waitForKey([a2, b2], 'issue:3') | |
| 203 | ||
| 204 | const i3a = await a2.view.get('issue:3') | |
| 205 | t.ok(i3a, 'A2 sees B2 issue written after reboot') | |
| 206 | t.is(i3a.value.author, 'serverB') | |
| 207 | ||
| 208 | await sw2A.destroy() | |
| 209 | await sw2B.destroy() | |
| 210 | await a2.close() | |
| 211 | await b2.close() | |
| 212 | await storeA2.close() | |
| 213 | await storeB2.close() | |
| 214 | await testnet.destroy() | |
| 215 | await testnet2.destroy() | |
| 216 | await cleanup(dirA, dirB) | |
| 217 | }) | |
| 218 | ||
| 219 | // ───────────────────────────────────────────────────────────── | |
| 220 | // Test 2: Concurrent writes — both append before syncing | |
| 221 | // ───────────────────────────────────────────────────────────── | |
| 222 | ||
| 223 | test('concurrent: both writers append simultaneously, linearization converges', async (t) => { | |
| 224 | t.timeout(30000) | |
| 225 | const testnet = await createTestnet(3) | |
| 226 | const dirA = tmpPath('a') | |
| 227 | const dirB = tmpPath('b') | |
| 228 | ||
| 229 | const storeA = new Corestore(dirA, { primaryKey: Buffer.alloc(32, 0), unsafe: true }) | |
| 230 | const storeB = new Corestore(dirB, { primaryKey: Buffer.alloc(32, 1), unsafe: true }) | |
| 231 | ||
| 232 | const a = createBase(storeA, null) | |
| 233 | await a.ready() | |
| 234 | const b = createBase(storeB, a.local.key) | |
| 235 | await b.ready() | |
| 236 | ||
| 237 | // Add B as writer via pipe (fast, reliable setup) | |
| 238 | const s1 = a.replicate(true) | |
| 239 | const s2 = b.replicate(false) | |
| 240 | s1.pipe(s2).pipe(s1) | |
| 241 | ||
| 242 | await a.append({ type: 'add-writer', key: b.local.key.toString('hex'), indexer: true }) | |
| 243 | ||
| 244 | for (let i = 0; i < 5; i++) { | |
| 245 | for (const base of [a, b]) await base.update() | |
| 246 | if (a.writable) await a.append(null) | |
| 247 | if (b.writable) await b.append(null) | |
| 248 | await new Promise(r => setTimeout(r, 200)) | |
| 249 | } | |
| 250 | ||
| 251 | t.ok(b.writable, 'B is writable') | |
| 252 | ||
| 253 | s1.destroy() | |
| 254 | s2.destroy() | |
| 255 | await new Promise(r => setTimeout(r, 500)) | |
| 256 | ||
| 257 | // Both write issues CONCURRENTLY while disconnected | |
| 258 | for (let i = 1; i <= 5; i++) { | |
| 259 | await a.append({ type: 'create-issue', id: i, title: 'A-issue-' + i, author: 'A' }) | |
| 260 | } | |
| 261 | for (let i = 6; i <= 10; i++) { | |
| 262 | await b.append({ type: 'create-issue', id: i, title: 'B-issue-' + i, author: 'B' }) | |
| 263 | } | |
| 264 | ||
| 265 | // Reconnect via Hyperswarm | |
| 266 | const { swarmA, swarmB } = await swarmReplicate(storeA, storeB, a.discoveryKey, testnet) | |
| 267 | ||
| 268 | // Wait for convergence | |
| 269 | await waitForKey([a, b], 'issue:10') | |
| 270 | ||
| 271 | // Verify all 10 issues present on both sides | |
| 272 | for (let i = 1; i <= 10; i++) { | |
| 273 | const ia = await a.view.get('issue:' + i) | |
| 274 | const ib = await b.view.get('issue:' + i) | |
| 275 | t.ok(ia, 'A has issue:' + i) | |
| 276 | t.ok(ib, 'B has issue:' + i) | |
| 277 | } | |
| 278 | ||
| 279 | t.comment('A members: ' + a.system.members) | |
| 280 | t.comment('B members: ' + b.system.members) | |
| 281 | ||
| 282 | await swarmA.destroy() | |
| 283 | await swarmB.destroy() | |
| 284 | await a.close() | |
| 285 | await b.close() | |
| 286 | await storeA.close() | |
| 287 | await storeB.close() | |
| 288 | await testnet.destroy() | |
| 289 | await cleanup(dirA, dirB) | |
| 290 | }) | |
| 291 | ||
| 292 | // ───────────────────────────────────────────────────────────── | |
| 293 | // Test 3: Disconnect + reconnect — offline writes converge | |
| 294 | // ───────────────────────────────────────────────────────────── | |
| 295 | ||
| 296 | test('disconnect: offline writes on both sides converge on reconnect', async (t) => { | |
| 297 | t.timeout(30000) | |
| 298 | const testnet = await createTestnet(3) | |
| 299 | const dirA = tmpPath('a') | |
| 300 | const dirB = tmpPath('b') | |
| 301 | ||
| 302 | const storeA = new Corestore(dirA, { primaryKey: Buffer.alloc(32, 0), unsafe: true }) | |
| 303 | const storeB = new Corestore(dirB, { primaryKey: Buffer.alloc(32, 1), unsafe: true }) | |
| 304 | ||
| 305 | const a = createBase(storeA, null) | |
| 306 | await a.ready() | |
| 307 | const b = createBase(storeB, a.local.key) | |
| 308 | await b.ready() | |
| 309 | ||
| 310 | // Setup multi-writer and initial sync | |
| 311 | const { swarmA, swarmB } = await swarmReplicate(storeA, storeB, a.discoveryKey, testnet) | |
| 312 | ||
| 313 | await a.append({ type: 'add-writer', key: b.local.key.toString('hex'), indexer: true }) | |
| 314 | await a.append({ type: 'create-issue', id: 1, title: 'initial sync test', author: 'A' }) | |
| 315 | ||
| 316 | await waitForKey([a, b], 'issue:1') | |
| 317 | ||
| 318 | // Confirm | |
| 319 | for (let i = 0; i < 3; i++) { | |
| 320 | if (a.writable) await a.append(null) | |
| 321 | if (b.writable) await b.append(null) | |
| 322 | for (const base of [a, b]) await base.update() | |
| 323 | await new Promise(r => setTimeout(r, 300)) | |
| 324 | } | |
| 325 | ||
| 326 | t.ok(b.writable, 'B writable') | |
| 327 | ||
| 328 | // Disconnect | |
| 329 | await swarmA.destroy() | |
| 330 | await swarmB.destroy() | |
| 331 | await new Promise(r => setTimeout(r, 500)) | |
| 332 | ||
| 333 | // Both write while disconnected | |
| 334 | await a.append({ type: 'create-issue', id: 2, title: 'A wrote offline', author: 'A' }) | |
| 335 | await a.append({ type: 'create-issue', id: 3, title: 'A also wrote this offline', author: 'A' }) | |
| 336 | await b.append({ type: 'create-issue', id: 4, title: 'B wrote offline', author: 'B' }) | |
| 337 | ||
| 338 | // Reconnect | |
| 339 | const testnet2 = await createTestnet(3) | |
| 340 | const { swarmA: sw2A, swarmB: sw2B } = await swarmReplicate(storeA, storeB, a.discoveryKey, testnet2) | |
| 341 | ||
| 342 | await waitForKey([a, b], 'issue:4') | |
| 343 | ||
| 344 | // All issues present on both sides | |
| 345 | for (let i = 1; i <= 4; i++) { | |
| 346 | const ia = await a.view.get('issue:' + i) | |
| 347 | const ib = await b.view.get('issue:' + i) | |
| 348 | t.ok(ia, 'A has issue:' + i) | |
| 349 | t.ok(ib, 'B has issue:' + i) | |
| 350 | } | |
| 351 | ||
| 352 | await sw2A.destroy() | |
| 353 | await sw2B.destroy() | |
| 354 | await a.close() | |
| 355 | await b.close() | |
| 356 | await storeA.close() | |
| 357 | await storeB.close() | |
| 358 | await testnet.destroy() | |
| 359 | await testnet2.destroy() | |
| 360 | await cleanup(dirA, dirB) | |
| 361 | }) | |
| 362 | ||
| 363 | // ───────────────────────────────────────────────────────────── | |
| 364 | // Test 4: Probe validation blocks bogus key in real apply | |
| 365 | // ───────────────────────────────────────────────────────────── | |
| 366 | ||
| 367 | test('probe validation: bogus key skipped in pear-git apply, real key works', async (t) => { | |
| 368 | t.timeout(20000) | |
| 369 | const testnet = await createTestnet(3) | |
| 370 | const dirA = tmpPath('a') | |
| 371 | const dirB = tmpPath('b') | |
| 372 | ||
| 373 | const storeA = new Corestore(dirA, { primaryKey: Buffer.alloc(32, 0), unsafe: true }) | |
| 374 | const storeB = new Corestore(dirB, { primaryKey: Buffer.alloc(32, 1), unsafe: true }) | |
| 375 | ||
| 376 | const a = createBase(storeA, null) | |
| 377 | await a.ready() | |
| 378 | const b = createBase(storeB, a.local.key) | |
| 379 | await b.ready() | |
| 380 | ||
| 381 | const { swarmA, swarmB } = await swarmReplicate(storeA, storeB, a.discoveryKey, testnet) | |
| 382 | ||
| 383 | // First: add bogus key (simulates the old bug) | |
| 384 | const bogusKey = Buffer.alloc(32, 0xde).toString('hex') | |
| 385 | await a.append({ type: 'add-writer', key: bogusKey, indexer: true }) | |
| 386 | ||
| 387 | // Then: add correct key | |
| 388 | await a.append({ type: 'add-writer', key: b.local.key.toString('hex'), indexer: true }) | |
| 389 | ||
| 390 | // Issue before and after | |
| 391 | await a.append({ type: 'create-issue', id: 1, title: 'survives bogus writer', author: 'A' }) | |
| 392 | ||
| 393 | // Sync | |
| 394 | await waitForKey([a, b], 'issue:1') | |
| 395 | ||
| 396 | // Confirm | |
| 397 | for (let i = 0; i < 3; i++) { | |
| 398 | if (a.writable) await a.append(null) | |
| 399 | if (b.writable) await b.append(null) | |
| 400 | for (const base of [a, b]) await base.update() | |
| 401 | await new Promise(r => setTimeout(r, 300)) | |
| 402 | } | |
| 403 | ||
| 404 | t.ok(b.writable, 'B writable (correct key worked despite bogus key in log)') | |
| 405 | t.comment('A members: ' + a.system.members) | |
| 406 | ||
| 407 | // B can write | |
| 408 | await b.append({ type: 'create-issue', id: 2, title: 'B writes after bogus+correct', author: 'B' }) | |
| 409 | await waitForKey([a, b], 'issue:2') | |
| 410 | ||
| 411 | const i2 = await a.view.get('issue:2') | |
| 412 | t.ok(i2, 'A sees B issue written after bogus key was skipped') | |
| 413 | ||
| 414 | await swarmA.destroy() | |
| 415 | await swarmB.destroy() | |
| 416 | await a.close() | |
| 417 | await b.close() | |
| 418 | await storeA.close() | |
| 419 | await storeB.close() | |
| 420 | await testnet.destroy() | |
| 421 | await cleanup(dirA, dirB) | |
| 422 | }) |
| … | … | @@ -0,0 +1,391 @@ |
| 1 | #!/usr/bin/env bare | |
| 2 | ||
| 3 | /** | |
| 4 | * Integration test: two-way Autobase multi-writer over Hyperswarm. | |
| 5 | * | |
| 6 | * Proves the correct flow for federation issue sync: | |
| 7 | * 1. Server A creates an Autobase, writes issues | |
| 8 | * 2. Server B adopts the same Autobase (same bootstrap key) | |
| 9 | * 3. Both connect via Hyperswarm testnet and replicate | |
| 10 | * 4. Server A adds Server B as a writer using B's base.local.key | |
| 11 | * 5. Server B writes issues | |
| 12 | * 6. Both servers see all issues from both writers | |
| 13 | * | |
| 14 | * This is the pattern pear-git needs for LinkedRepo federation. | |
| 15 | */ | |
| 16 | ||
| 17 | import test from 'brittle' | |
| 18 | import Corestore from 'corestore' | |
| 19 | import Autobase from 'autobase' | |
| 20 | import Hyperbee from 'hyperbee' | |
| 21 | import Hyperswarm from 'hyperswarm' | |
| 22 | import createTestnet from '@hyperswarm/testnet' | |
| 23 | import os from 'bare-os' | |
| 24 | import path from 'bare-path' | |
| 25 | import fs from 'bare-fs' | |
| 26 | ||
| 27 | const tmpdir = os.tmpdir() | |
| 28 | let idx = 0 | |
| 29 | ||
| 30 | function tmpPath (name) { | |
| 31 | return path.join(tmpdir, 'ab-mw-' + name + '-' + (idx++) + '-' + Date.now()) | |
| 32 | } | |
| 33 | ||
| 34 | async function cleanup (...dirs) { | |
| 35 | for (const d of dirs) await fs.promises.rm(d, { recursive: true }).catch(() => {}) | |
| 36 | } | |
| 37 | ||
| 38 | function open (store) { | |
| 39 | return new Hyperbee(store.get('view', { valueEncoding: 'json' }), { | |
| 40 | keyEncoding: 'utf-8', | |
| 41 | valueEncoding: 'json' | |
| 42 | }) | |
| 43 | } | |
| 44 | ||
| 45 | async function apply (batch, view, host) { | |
| 46 | const b = view.batch() | |
| 47 | for (const node of batch) { | |
| 48 | const op = node.value | |
| 49 | if (op.type === 'put') { | |
| 50 | await b.put(op.key, op.value) | |
| 51 | } | |
| 52 | if (op.type === 'add-writer') { | |
| 53 | await host.addWriter(Buffer.from(op.key, 'hex'), { | |
| 54 | indexer: op.indexer !== false | |
| 55 | }) | |
| 56 | } | |
| 57 | } | |
| 58 | await b.flush() | |
| 59 | } | |
| 60 | ||
| 61 | function createBase (store, key) { | |
| 62 | return new Autobase(store.session(), key, { | |
| 63 | open, | |
| 64 | apply, | |
| 65 | valueEncoding: 'json', | |
| 66 | ackInterval: 0, | |
| 67 | ackThreshold: 0 | |
| 68 | }) | |
| 69 | } | |
| 70 | ||
| 71 | async function swarmReplicate (storeA, storeB, topic, testnet) { | |
| 72 | const { bootstrap } = testnet | |
| 73 | const swarmA = new Hyperswarm({ bootstrap }) | |
| 74 | const swarmB = new Hyperswarm({ bootstrap }) | |
| 75 | ||
| 76 | swarmA.on('connection', (conn) => storeA.replicate(conn)) | |
| 77 | swarmB.on('connection', (conn) => storeB.replicate(conn)) | |
| 78 | ||
| 79 | await swarmA.join(topic, { server: true, client: false }).flushed() | |
| 80 | swarmB.join(topic, { client: true, server: false }) | |
| 81 | ||
| 82 | await swarmB.flush() | |
| 83 | ||
| 84 | return { swarmA, swarmB } | |
| 85 | } | |
| 86 | ||
| 87 | async function waitForSync (bases, checkKey, maxWait = 10000) { | |
| 88 | const start = Date.now() | |
| 89 | while (Date.now() - start < maxWait) { | |
| 90 | for (const b of bases) await b.update() | |
| 91 | if (checkKey) { | |
| 92 | let allHave = true | |
| 93 | for (const b of bases) { | |
| 94 | const entry = await b.view.get(checkKey) | |
| 95 | if (!entry) { allHave = false; break } | |
| 96 | } | |
| 97 | if (allHave) return true | |
| 98 | } | |
| 99 | await new Promise(r => setTimeout(r, 300)) | |
| 100 | } | |
| 101 | return false | |
| 102 | } | |
| 103 | ||
| 104 | // ───────────────────────────────────────────────────────────── | |
| 105 | // Test 1: One-way replication over Hyperswarm (adoption only) | |
| 106 | // ───────────────────────────────────────────────────────────── | |
| 107 | ||
| 108 | test('one-way: server B reads server A issues via Hyperswarm', async (t) => { | |
| 109 | t.timeout(30000) | |
| 110 | const testnet = await createTestnet(3) | |
| 111 | const dirA = tmpPath('a') | |
| 112 | const dirB = tmpPath('b') | |
| 113 | ||
| 114 | const storeA = new Corestore(dirA, { primaryKey: Buffer.alloc(32, 0), unsafe: true }) | |
| 115 | const storeB = new Corestore(dirB, { primaryKey: Buffer.alloc(32, 1), unsafe: true }) | |
| 116 | ||
| 117 | // Server A creates Autobase and writes issues | |
| 118 | const baseA = createBase(storeA, null) | |
| 119 | await baseA.ready() | |
| 120 | const bootstrapKey = baseA.local.key | |
| 121 | ||
| 122 | await baseA.append({ type: 'put', key: 'issue:1', value: { title: 'first issue' } }) | |
| 123 | await baseA.append({ type: 'put', key: 'issue:2', value: { title: 'second issue' } }) | |
| 124 | await baseA.update() | |
| 125 | ||
| 126 | const check1 = await baseA.view.get('issue:1') | |
| 127 | t.ok(check1, 'A has issue:1') | |
| 128 | ||
| 129 | // Server B adopts with same bootstrap key (like _adoptIssueAutobase) | |
| 130 | const baseB = createBase(storeB, bootstrapKey) | |
| 131 | await baseB.ready() | |
| 132 | ||
| 133 | // Connect via Hyperswarm | |
| 134 | const { swarmA, swarmB } = await swarmReplicate(storeA, storeB, baseA.discoveryKey, testnet) | |
| 135 | ||
| 136 | // Wait for B to sync A's data | |
| 137 | await waitForSync([baseA, baseB], 'issue:2') | |
| 138 | ||
| 139 | const e1 = await baseB.view.get('issue:1') | |
| 140 | const e2 = await baseB.view.get('issue:2') | |
| 141 | t.ok(e1, 'B sees issue:1') | |
| 142 | t.ok(e2, 'B sees issue:2') | |
| 143 | t.is(e1.value.title, 'first issue') | |
| 144 | t.is(e2.value.title, 'second issue') | |
| 145 | ||
| 146 | // B is NOT writable (not added as writer) | |
| 147 | t.is(baseB.writable, false, 'B is read-only') | |
| 148 | ||
| 149 | await swarmA.destroy() | |
| 150 | await swarmB.destroy() | |
| 151 | await baseA.close() | |
| 152 | await baseB.close() | |
| 153 | await storeA.close() | |
| 154 | await storeB.close() | |
| 155 | await testnet.destroy() | |
| 156 | await cleanup(dirA, dirB) | |
| 157 | }) | |
| 158 | ||
| 159 | // ───────────────────────────────────────────────────────────── | |
| 160 | // Test 2: Two-way multi-writer over Hyperswarm | |
| 161 | // ───────────────────────────────────────────────────────────── | |
| 162 | ||
| 163 | test('two-way: A adds B as writer, both write issues via Hyperswarm', async (t) => { | |
| 164 | t.timeout(30000) | |
| 165 | const testnet = await createTestnet(3) | |
| 166 | const dirA = tmpPath('a') | |
| 167 | const dirB = tmpPath('b') | |
| 168 | ||
| 169 | const storeA = new Corestore(dirA, { primaryKey: Buffer.alloc(32, 0), unsafe: true }) | |
| 170 | const storeB = new Corestore(dirB, { primaryKey: Buffer.alloc(32, 1), unsafe: true }) | |
| 171 | ||
| 172 | // Server A creates Autobase | |
| 173 | const baseA = createBase(storeA, null) | |
| 174 | await baseA.ready() | |
| 175 | const bootstrapKey = baseA.local.key | |
| 176 | ||
| 177 | // Server B adopts | |
| 178 | const baseB = createBase(storeB, bootstrapKey) | |
| 179 | await baseB.ready() | |
| 180 | ||
| 181 | t.comment('A local key: ' + baseA.local.key.toString('hex').slice(0, 16)) | |
| 182 | t.comment('B local key: ' + baseB.local.key.toString('hex').slice(0, 16)) | |
| 183 | ||
| 184 | // Connect via Hyperswarm | |
| 185 | const { swarmA, swarmB } = await swarmReplicate(storeA, storeB, baseA.discoveryKey, testnet) | |
| 186 | ||
| 187 | // Wait for initial connection | |
| 188 | await new Promise(r => setTimeout(r, 2000)) | |
| 189 | ||
| 190 | // A writes initial issue | |
| 191 | await baseA.append({ type: 'put', key: 'issue:1', value: { title: 'from server A' } }) | |
| 192 | ||
| 193 | // A adds B as a writer — using B's ACTUAL local core key | |
| 194 | // This is the critical part: must be base.local.key, not a server identity key | |
| 195 | await baseA.append({ | |
| 196 | type: 'add-writer', | |
| 197 | key: baseB.local.key.toString('hex'), | |
| 198 | indexer: true | |
| 199 | }) | |
| 200 | ||
| 201 | // Sync so B gets the add-writer op | |
| 202 | await waitForSync([baseA, baseB], 'issue:1') | |
| 203 | ||
| 204 | // Ack from both sides to confirm the writer set | |
| 205 | if (baseA.writable) await baseA.append(null) | |
| 206 | await new Promise(r => setTimeout(r, 1000)) | |
| 207 | for (const b of [baseA, baseB]) await b.update() | |
| 208 | ||
| 209 | if (baseB.writable) await baseB.append(null) | |
| 210 | await new Promise(r => setTimeout(r, 1000)) | |
| 211 | for (const b of [baseA, baseB]) await b.update() | |
| 212 | ||
| 213 | t.comment('B writable after add-writer: ' + baseB.writable) | |
| 214 | t.comment('A members: ' + baseA.system.members) | |
| 215 | t.comment('B members: ' + baseB.system.members) | |
| 216 | ||
| 217 | t.ok(baseB.writable, 'B is writable after being added') | |
| 218 | t.is(baseA.system.members, 2, 'A sees 2 members') | |
| 219 | ||
| 220 | // Now B writes an issue | |
| 221 | if (baseB.writable) { | |
| 222 | await baseB.append({ type: 'put', key: 'issue:2', value: { title: 'from server B' } }) | |
| 223 | } | |
| 224 | ||
| 225 | // Sync again | |
| 226 | await waitForSync([baseA, baseB], 'issue:2') | |
| 227 | ||
| 228 | // Both should see both issues | |
| 229 | const a1 = await baseA.view.get('issue:1') | |
| 230 | const a2 = await baseA.view.get('issue:2') | |
| 231 | const b1 = await baseB.view.get('issue:1') | |
| 232 | const b2 = await baseB.view.get('issue:2') | |
| 233 | ||
| 234 | t.ok(a1, 'A sees issue:1') | |
| 235 | t.ok(b1, 'B sees issue:1') | |
| 236 | t.is(a1.value.title, 'from server A') | |
| 237 | t.is(b1.value.title, 'from server A') | |
| 238 | ||
| 239 | if (baseB.writable) { | |
| 240 | t.ok(a2, 'A sees issue:2 from B') | |
| 241 | t.ok(b2, 'B sees issue:2 from B') | |
| 242 | t.is(a2.value.title, 'from server B') | |
| 243 | t.is(b2.value.title, 'from server B') | |
| 244 | } | |
| 245 | ||
| 246 | await swarmA.destroy() | |
| 247 | await swarmB.destroy() | |
| 248 | await baseA.close() | |
| 249 | await baseB.close() | |
| 250 | await storeA.close() | |
| 251 | await storeB.close() | |
| 252 | await testnet.destroy() | |
| 253 | await cleanup(dirA, dirB) | |
| 254 | }) | |
| 255 | ||
| 256 | // ───────────────────────────────────────────────────────────── | |
| 257 | // Test 3: Wrong key type causes writable=false (the bug we hit) | |
| 258 | // ───────────────────────────────────────────────────────────── | |
| 259 | ||
| 260 | test('wrong key: using identity key instead of local.key fails gracefully', async (t) => { | |
| 261 | t.timeout(30000) | |
| 262 | const testnet = await createTestnet(3) | |
| 263 | const dirA = tmpPath('a') | |
| 264 | const dirB = tmpPath('b') | |
| 265 | ||
| 266 | const storeA = new Corestore(dirA, { primaryKey: Buffer.alloc(32, 0), unsafe: true }) | |
| 267 | const storeB = new Corestore(dirB, { primaryKey: Buffer.alloc(32, 1), unsafe: true }) | |
| 268 | ||
| 269 | const baseA = createBase(storeA, null) | |
| 270 | await baseA.ready() | |
| 271 | const bootstrapKey = baseA.local.key | |
| 272 | ||
| 273 | const baseB = createBase(storeB, bootstrapKey) | |
| 274 | await baseB.ready() | |
| 275 | ||
| 276 | const { swarmA, swarmB } = await swarmReplicate(storeA, storeB, baseA.discoveryKey, testnet) | |
| 277 | await new Promise(r => setTimeout(r, 2000)) | |
| 278 | ||
| 279 | // Simulate the bug: use a random key (like an Ed25519 identity key) | |
| 280 | // instead of baseB.local.key | |
| 281 | const fakeIdentityKey = Buffer.alloc(32, 0xf5).toString('hex') | |
| 282 | t.comment('fake key: ' + fakeIdentityKey.slice(0, 16)) | |
| 283 | t.comment('correct key would be: ' + baseB.local.key.toString('hex').slice(0, 16)) | |
| 284 | ||
| 285 | await baseA.append({ | |
| 286 | type: 'add-writer', | |
| 287 | key: fakeIdentityKey, | |
| 288 | indexer: true | |
| 289 | }) | |
| 290 | ||
| 291 | // Sync | |
| 292 | for (let i = 0; i < 10; i++) { | |
| 293 | for (const b of [baseA, baseB]) await b.update() | |
| 294 | await new Promise(r => setTimeout(r, 300)) | |
| 295 | } | |
| 296 | ||
| 297 | // B should still NOT be writable — wrong key was used | |
| 298 | t.is(baseB.writable, false, 'B is NOT writable (wrong key used)') | |
| 299 | ||
| 300 | // A should still have member count reflecting the bogus key | |
| 301 | t.comment('A members: ' + baseA.system.members + ' (includes bogus)') | |
| 302 | ||
| 303 | await swarmA.destroy() | |
| 304 | await swarmB.destroy() | |
| 305 | await baseA.close() | |
| 306 | await baseB.close() | |
| 307 | await storeA.close() | |
| 308 | await storeB.close() | |
| 309 | await testnet.destroy() | |
| 310 | await cleanup(dirA, dirB) | |
| 311 | }) | |
| 312 | ||
| 313 | // ───────────────────────────────────────────────────────────── | |
| 314 | // Test 4: Correct key vs wrong key — side by side | |
| 315 | // ───────────────────────────────────────────────────────────── | |
| 316 | ||
| 317 | test('correct key = writable, wrong key = not writable', async (t) => { | |
| 318 | t.timeout(15000) | |
| 319 | ||
| 320 | // Correct: use base.local.key | |
| 321 | const dirA = tmpPath('a') | |
| 322 | const dirB = tmpPath('b') | |
| 323 | const storeA = new Corestore(dirA, { primaryKey: Buffer.alloc(32, 0), unsafe: true }) | |
| 324 | const storeB = new Corestore(dirB, { primaryKey: Buffer.alloc(32, 1), unsafe: true }) | |
| 325 | ||
| 326 | const a = createBase(storeA, null) | |
| 327 | await a.ready() | |
| 328 | const b = createBase(storeB, a.local.key) | |
| 329 | await b.ready() | |
| 330 | ||
| 331 | // Add B with correct key | |
| 332 | await a.append({ type: 'add-writer', key: b.local.key.toString('hex'), indexer: true }) | |
| 333 | ||
| 334 | // Pipe replicate (fast, no swarm needed for this test) | |
| 335 | const s1 = a.replicate(true) | |
| 336 | const s2 = b.replicate(false) | |
| 337 | s1.pipe(s2).pipe(s1) | |
| 338 | ||
| 339 | // Confirm | |
| 340 | for (let i = 0; i < 3; i++) { | |
| 341 | for (const base of [a, b]) await base.update() | |
| 342 | if (a.writable) await a.append(null) | |
| 343 | if (b.writable) await b.append(null) | |
| 344 | await new Promise(r => setTimeout(r, 200)) | |
| 345 | for (const base of [a, b]) await base.update() | |
| 346 | } | |
| 347 | ||
| 348 | t.ok(b.writable, 'correct key → writable') | |
| 349 | t.is(a.system.members, 2, '2 members') | |
| 350 | ||
| 351 | s1.destroy() | |
| 352 | s2.destroy() | |
| 353 | await a.close() | |
| 354 | await b.close() | |
| 355 | await storeA.close() | |
| 356 | await storeB.close() | |
| 357 | ||
| 358 | // Wrong: use random key | |
| 359 | const dirC = tmpPath('c') | |
| 360 | const dirD = tmpPath('d') | |
| 361 | const storeC = new Corestore(dirC, { primaryKey: Buffer.alloc(32, 2), unsafe: true }) | |
| 362 | const storeD = new Corestore(dirD, { primaryKey: Buffer.alloc(32, 3), unsafe: true }) | |
| 363 | ||
| 364 | const c = createBase(storeC, null) | |
| 365 | await c.ready() | |
| 366 | const d = createBase(storeD, c.local.key) | |
| 367 | await d.ready() | |
| 368 | ||
| 369 | // Add D with WRONG key | |
| 370 | const wrongKey = Buffer.alloc(32, 0xab).toString('hex') | |
| 371 | await c.append({ type: 'add-writer', key: wrongKey, indexer: true }) | |
| 372 | ||
| 373 | const s3 = c.replicate(true) | |
| 374 | const s4 = d.replicate(false) | |
| 375 | s3.pipe(s4).pipe(s3) | |
| 376 | ||
| 377 | for (let i = 0; i < 3; i++) { | |
| 378 | for (const base of [c, d]) await base.update() | |
| 379 | await new Promise(r => setTimeout(r, 200)) | |
| 380 | } | |
| 381 | ||
| 382 | t.is(d.writable, false, 'wrong key → NOT writable') | |
| 383 | ||
| 384 | s3.destroy() | |
| 385 | s4.destroy() | |
| 386 | await c.close() | |
| 387 | await d.close() | |
| 388 | await storeC.close() | |
| 389 | await storeD.close() | |
| 390 | await cleanup(dirA, dirB, dirC, dirD) | |
| 391 | }) |
| … | … | @@ -0,0 +1,242 @@ |
| 1 | #!/usr/bin/env bare | |
| 2 | ||
| 3 | /** | |
| 4 | * Integration test: reproduce Autobase corruption from poisoned addWriter | |
| 5 | * and verify the drain loop patch prevents infinite hang. | |
| 6 | * | |
| 7 | * Uses autobase-test-helpers for replication (same as upstream tests). | |
| 8 | */ | |
| 9 | ||
| 10 | import test from 'brittle' | |
| 11 | import Corestore from 'corestore' | |
| 12 | import Autobase from 'autobase' | |
| 13 | import Hyperbee from 'hyperbee' | |
| 14 | import crypto from 'hypercore-crypto' | |
| 15 | import os from 'bare-os' | |
| 16 | import path from 'bare-path' | |
| 17 | import fs from 'bare-fs' | |
| 18 | import { replicate, replicateAndSync } from 'autobase-test-helpers' | |
| 19 | ||
| 20 | const tmpdir = os.tmpdir() | |
| 21 | let idx = 0 | |
| 22 | ||
| 23 | function tmpPath (name) { | |
| 24 | return path.join(tmpdir, 'ab-poison-' + name + '-' + (idx++) + '-' + Date.now()) | |
| 25 | } | |
| 26 | ||
| 27 | function open (store) { | |
| 28 | return new Hyperbee(store.get('view', { valueEncoding: 'json' }), { | |
| 29 | keyEncoding: 'utf-8', | |
| 30 | valueEncoding: 'json' | |
| 31 | }) | |
| 32 | } | |
| 33 | ||
| 34 | async function cleanup (...dirs) { | |
| 35 | for (const d of dirs) await fs.promises.rm(d, { recursive: true }).catch(() => {}) | |
| 36 | } | |
| 37 | ||
| 38 | // Apply that validates before calling host.addWriter (our fix) | |
| 39 | async function safeApply (nodes, view, host) { | |
| 40 | const batch = view.batch() | |
| 41 | for (const node of nodes) { | |
| 42 | const op = node.value | |
| 43 | if (op.type === 'put') { | |
| 44 | await batch.put(op.key, op.value) | |
| 45 | } | |
| 46 | if (op.type === 'add-writer' && op.writerKey) { | |
| 47 | const writerKey = Buffer.from(op.writerKey, 'hex') | |
| 48 | try { | |
| 49 | const probe = host.base.store.get({ key: writerKey, active: false }) | |
| 50 | await probe.ready() | |
| 51 | const valid = probe.manifest !== null || probe.length > 0 | |
| 52 | await probe.close() | |
| 53 | if (valid) { | |
| 54 | await host.addWriter(writerKey, { indexer: false }) | |
| 55 | } | |
| 56 | } catch {} | |
| 57 | } | |
| 58 | } | |
| 59 | await batch.flush() | |
| 60 | } | |
| 61 | ||
| 62 | // Apply that blindly calls host.addWriter (the old broken behavior) | |
| 63 | async function unsafeApply (nodes, view, host) { | |
| 64 | const batch = view.batch() | |
| 65 | for (const node of nodes) { | |
| 66 | const op = node.value | |
| 67 | if (op.type === 'put') { | |
| 68 | await batch.put(op.key, op.value) | |
| 69 | } | |
| 70 | if (op.type === 'add-writer' && op.writerKey) { | |
| 71 | await host.addWriter(Buffer.from(op.writerKey, 'hex'), { indexer: false }) | |
| 72 | } | |
| 73 | } | |
| 74 | await batch.flush() | |
| 75 | } | |
| 76 | ||
| 77 | function createBase (store, key, apply) { | |
| 78 | return new Autobase(store.session(), key, { | |
| 79 | open, | |
| 80 | apply, | |
| 81 | valueEncoding: 'json', | |
| 82 | ackInterval: 0, | |
| 83 | ackThreshold: 0, | |
| 84 | fastForward: false | |
| 85 | }) | |
| 86 | } | |
| 87 | ||
| 88 | // ───────────────────────────────────────────────────────────── | |
| 89 | ||
| 90 | test('baseline: two peers replicate and sync', async (t) => { | |
| 91 | t.timeout(15000) | |
| 92 | const dirA = tmpPath('a') | |
| 93 | const dirB = tmpPath('b') | |
| 94 | ||
| 95 | const storeA = new Corestore(dirA, { primaryKey: Buffer.alloc(32, 0), unsafe: true }) | |
| 96 | const storeB = new Corestore(dirB, { primaryKey: Buffer.alloc(32, 1), unsafe: true }) | |
| 97 | ||
| 98 | const a = createBase(storeA, null, safeApply) | |
| 99 | await a.ready() | |
| 100 | ||
| 101 | const b = createBase(storeB, a.local.key, safeApply) | |
| 102 | await b.ready() | |
| 103 | ||
| 104 | await a.append({ type: 'put', key: 'msg:1', value: { text: 'hello' } }) | |
| 105 | ||
| 106 | await replicateAndSync([a, b]) | |
| 107 | ||
| 108 | const entry = await b.view.get('msg:1') | |
| 109 | t.ok(entry, 'replicated') | |
| 110 | t.is(entry.value.text, 'hello') | |
| 111 | ||
| 112 | await a.close() | |
| 113 | await b.close() | |
| 114 | await storeA.close() | |
| 115 | await storeB.close() | |
| 116 | await cleanup(dirA, dirB) | |
| 117 | }) | |
| 118 | ||
| 119 | // ───────────────────────────────────────────────────────────── | |
| 120 | ||
| 121 | test('safe apply: poisoned add-writer does not hang on replication', async (t) => { | |
| 122 | t.timeout(15000) | |
| 123 | const dirA = tmpPath('a') | |
| 124 | const dirB = tmpPath('b') | |
| 125 | ||
| 126 | const storeA = new Corestore(dirA, { primaryKey: Buffer.alloc(32, 0), unsafe: true }) | |
| 127 | const storeB = new Corestore(dirB, { primaryKey: Buffer.alloc(32, 1), unsafe: true }) | |
| 128 | ||
| 129 | const a = createBase(storeA, null, safeApply) | |
| 130 | await a.ready() | |
| 131 | ||
| 132 | const b = createBase(storeB, a.local.key, safeApply) | |
| 133 | await b.ready() | |
| 134 | ||
| 135 | // Write data + poisoned add-writer + more data | |
| 136 | await a.append({ type: 'put', key: 'msg:1', value: { text: 'before' } }) | |
| 137 | const bogusKey = crypto.randomBytes(32).toString('hex') | |
| 138 | await a.append({ type: 'add-writer', writerKey: bogusKey }) | |
| 139 | await a.append({ type: 'put', key: 'msg:2', value: { text: 'after' } }) | |
| 140 | ||
| 141 | await replicateAndSync([a, b]) | |
| 142 | ||
| 143 | const e1 = await b.view.get('msg:1') | |
| 144 | const e2 = await b.view.get('msg:2') | |
| 145 | t.ok(e1, 'msg:1 replicated') | |
| 146 | t.ok(e2, 'msg:2 replicated past poison') | |
| 147 | t.is(e1.value.text, 'before') | |
| 148 | t.is(e2.value.text, 'after') | |
| 149 | ||
| 150 | await a.close() | |
| 151 | await b.close() | |
| 152 | await storeA.close() | |
| 153 | await storeB.close() | |
| 154 | await cleanup(dirA, dirB) | |
| 155 | }) | |
| 156 | ||
| 157 | // ───────────────────────────────────────────────────────────── | |
| 158 | ||
| 159 | test('unsafe apply: poisoned add-writer — reopen must not hang', async (t) => { | |
| 160 | t.timeout(15000) | |
| 161 | const dirA = tmpPath('a') | |
| 162 | ||
| 163 | const storeA = new Corestore(dirA, { primaryKey: Buffer.alloc(32, 0), unsafe: true }) | |
| 164 | const a = createBase(storeA, null, unsafeApply) | |
| 165 | await a.ready() | |
| 166 | const key = a.local.key | |
| 167 | ||
| 168 | // Poison it | |
| 169 | await a.append({ type: 'put', key: 'msg:1', value: { text: 'before' } }) | |
| 170 | const bogusKey = crypto.randomBytes(32).toString('hex') | |
| 171 | await a.append({ type: 'add-writer', writerKey: bogusKey }) | |
| 172 | await a.update() | |
| 173 | ||
| 174 | await a.close() | |
| 175 | await storeA.close() | |
| 176 | ||
| 177 | // Reopen — this is where the drain loop used to hang | |
| 178 | const storeA2 = new Corestore(dirA, { primaryKey: Buffer.alloc(32, 0), unsafe: true }) | |
| 179 | const a2 = createBase(storeA2, key, unsafeApply) | |
| 180 | await a2.ready() | |
| 181 | ||
| 182 | t.comment('writable: ' + a2.writable) | |
| 183 | ||
| 184 | const timer = setTimeout(() => { | |
| 185 | t.fail('update() hung for 10 seconds') | |
| 186 | t.end() | |
| 187 | }, 10000) | |
| 188 | ||
| 189 | await a2.update() | |
| 190 | clearTimeout(timer) | |
| 191 | t.pass('update() did not hang on reopen') | |
| 192 | ||
| 193 | await a2.close() | |
| 194 | await storeA2.close() | |
| 195 | await cleanup(dirA) | |
| 196 | }) | |
| 197 | ||
| 198 | // ───────────────────────────────────────────────────────────── | |
| 199 | ||
| 200 | test('unsafe apply: poisoned writer replicated to peer — peer must not hang', async (t) => { | |
| 201 | t.timeout(15000) | |
| 202 | const dirA = tmpPath('a') | |
| 203 | const dirB = tmpPath('b') | |
| 204 | ||
| 205 | const storeA = new Corestore(dirA, { primaryKey: Buffer.alloc(32, 0), unsafe: true }) | |
| 206 | const storeB = new Corestore(dirB, { primaryKey: Buffer.alloc(32, 1), unsafe: true }) | |
| 207 | ||
| 208 | const a = createBase(storeA, null, unsafeApply) | |
| 209 | await a.ready() | |
| 210 | ||
| 211 | const b = createBase(storeB, a.local.key, unsafeApply) | |
| 212 | await b.ready() | |
| 213 | ||
| 214 | // Write data + poison | |
| 215 | await a.append({ type: 'put', key: 'msg:1', value: { text: 'hello' } }) | |
| 216 | const bogusKey = crypto.randomBytes(32).toString('hex') | |
| 217 | await a.append({ type: 'add-writer', writerKey: bogusKey }) | |
| 218 | ||
| 219 | // Replicate — peer B receives the poison | |
| 220 | const done = replicate([a, b]) | |
| 221 | ||
| 222 | const timer = setTimeout(() => { | |
| 223 | t.fail('sync hung for 10 seconds') | |
| 224 | t.end() | |
| 225 | }, 10000) | |
| 226 | ||
| 227 | // Try to update B — should not hang with the drain loop patch | |
| 228 | for (let i = 0; i < 5; i++) { | |
| 229 | await b.update() | |
| 230 | await new Promise(r => setTimeout(r, 200)) | |
| 231 | } | |
| 232 | ||
| 233 | clearTimeout(timer) | |
| 234 | t.pass('peer B update() did not hang despite receiving poisoned writer') | |
| 235 | ||
| 236 | await done() | |
| 237 | await a.close() | |
| 238 | await b.close() | |
| 239 | await storeA.close() | |
| 240 | await storeB.close() | |
| 241 | await cleanup(dirA, dirB) | |
| 242 | }) |