Repository: nyariv/SandboxJS Branch: main Commit: 826865251232 Files: 140 Total size: 761.5 KB Directory structure: gitextract_tkgsxi77/ ├── .github/ │ └── workflows/ │ ├── deploy.yml │ ├── npm-publish.yml │ ├── shields.yml │ └── test.yml ├── .gitignore ├── .husky/ │ └── pre-commit ├── .npmignore ├── .prettierignore ├── .prettierrc ├── .vscode/ │ └── settings.json ├── LICENSE ├── README.md ├── TODO.md ├── css/ │ └── style.css ├── eslint.config.js ├── index.html ├── jest.config.js ├── package.json ├── scripts/ │ ├── build.mjs │ └── export-tests.ts ├── src/ │ ├── Sandbox.ts │ ├── SandboxExec.ts │ ├── eval/ │ │ └── index.ts │ ├── executor/ │ │ ├── executorUtils.ts │ │ ├── index.ts │ │ ├── ops/ │ │ │ ├── assignment.ts │ │ │ ├── call.ts │ │ │ ├── comparison.ts │ │ │ ├── control.ts │ │ │ ├── functions.ts │ │ │ ├── index.ts │ │ │ ├── literals.ts │ │ │ ├── misc.ts │ │ │ ├── object.ts │ │ │ ├── prop.ts │ │ │ ├── unary.ts │ │ │ └── variables.ts │ │ └── opsRegistry.ts │ ├── parser/ │ │ ├── index.ts │ │ ├── lisp.ts │ │ ├── lispTypes/ │ │ │ ├── conditionals.ts │ │ │ ├── control.ts │ │ │ ├── declarations.ts │ │ │ ├── index.ts │ │ │ ├── operators.ts │ │ │ ├── shared.ts │ │ │ ├── structures.ts │ │ │ └── values.ts │ │ └── parserUtils.ts │ └── utils/ │ ├── CodeString.ts │ ├── ExecContext.ts │ ├── Prop.ts │ ├── Scope.ts │ ├── errors.ts │ ├── functionReplacements.ts │ ├── index.ts │ ├── types.ts │ └── unraw.ts ├── test/ │ ├── audit.spec.ts │ ├── compileRerun.spec.ts │ ├── delaySynchronousResult.spec.ts │ ├── eval/ │ │ ├── README.md │ │ ├── reveseLinkedList.js │ │ ├── script.js │ │ ├── testCases/ │ │ │ ├── arithmetic-operators.data.ts │ │ │ ├── arithmetic-operators.spec.ts │ │ │ ├── assignment-operators.data.ts │ │ │ ├── assignment-operators.spec.ts │ │ │ ├── bitwise-operators.data.ts │ │ │ ├── bitwise-operators.spec.ts │ │ │ ├── comments.data.ts │ │ │ ├── comments.spec.ts │ │ │ ├── comparison-operators.data.ts │ │ │ ├── comparison-operators.spec.ts │ │ │ ├── complex-expressions.data.ts │ │ │ ├── complex-expressions.spec.ts │ │ │ ├── conditionals.data.ts │ │ │ ├── conditionals.spec.ts │ │ │ ├── data-types.data.ts │ │ │ ├── data-types.spec.ts │ │ │ ├── defaults.data.ts │ │ │ ├── defaults.spec.ts │ │ │ ├── destructuring.data.ts │ │ │ ├── destructuring.spec.ts │ │ │ ├── error-handling.data.ts │ │ │ ├── error-handling.spec.ts │ │ │ ├── function-replacements.data.ts │ │ │ ├── function-replacements.spec.ts │ │ │ ├── functions.data.ts │ │ │ ├── functions.spec.ts │ │ │ ├── generators.data.ts │ │ │ ├── generators.spec.ts │ │ │ ├── index.ts │ │ │ ├── logical-operators.data.ts │ │ │ ├── logical-operators.spec.ts │ │ │ ├── loops.data.ts │ │ │ ├── loops.spec.ts │ │ │ ├── objects-and-arrays.data.ts │ │ │ ├── objects-and-arrays.spec.ts │ │ │ ├── operator-precedence.data.ts │ │ │ ├── operator-precedence.spec.ts │ │ │ ├── other-operators.data.ts │ │ │ ├── other-operators.spec.ts │ │ │ ├── security.data.ts │ │ │ ├── security.spec.ts │ │ │ ├── switch.data.ts │ │ │ ├── switch.spec.ts │ │ │ ├── syntax-errors.data.ts │ │ │ ├── syntax-errors.spec.ts │ │ │ ├── template-literals.data.ts │ │ │ ├── template-literals.spec.ts │ │ │ ├── test-utils.ts │ │ │ └── types.ts │ │ └── tests.json │ ├── evalCompletionValue.spec.ts │ ├── expression.spec.ts │ ├── parse.spec.ts │ ├── performance.mjs │ ├── sandboxErrorCatch.spec.ts │ ├── sandboxRestrictions.spec.ts │ ├── semicolonInsertion.spec.ts │ ├── subscriptions.spec.ts │ ├── symbol.spec.ts │ ├── taggedTemplateEscaping.spec.ts │ ├── ticks/ │ │ ├── sandboxArrayTicks.spec.ts │ │ ├── sandboxCollectionTicks.spec.ts │ │ ├── sandboxNativeTicks.spec.ts │ │ ├── sandboxObjectTicks.spec.ts │ │ ├── sandboxSpreadTicks.spec.ts │ │ └── sandboxStringTicks.spec.ts │ ├── ticksQuotaHalt.spec.ts │ ├── timers.spec.ts │ ├── timersAsync.spec.ts │ ├── timersAsyncHalt.spec.ts │ ├── timersHalt.spec.ts │ ├── tryFinallyControlFlow.spec.ts │ ├── tsconfig.json │ └── unraw.spec.ts ├── tsconfig.jest.json └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/deploy.yml ================================================ name: Deploy to GitHub Pages on: push: branches: - main jobs: build-and-deploy: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '22' - name: Install dependencies run: npm install - name: Build project run: npm run build - name: Prepare deployment directory run: | mkdir pages cp -r dist pages/ cp -r test pages/ cp -r css pages/ cp index.html pages/ cp logo.svg pages/ - name: Deploy to GitHub Pages uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./pages ================================================ FILE: .github/workflows/npm-publish.yml ================================================ # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages name: Node.js Package on: # release: # types: [create] push: branches: - main jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 24 - run: npm install - run: npm run build - run: npm test publish-npm: needs: test runs-on: ubuntu-latest permissions: id-token: write contents: read steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 24 registry-url: https://registry.npmjs.org/ - run: npm install - run: npm run build - run: npm publish --access public --provenance ================================================ FILE: .github/workflows/shields.yml ================================================ name: Shield badges on: push: branches: [main] jobs: bundle-size: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 24 - run: npm install - run: npm run build - name: Measure bundle size (gzip) id: size run: | BYTES=$(gzip -9 -c dist/umd/Sandbox.min.js | wc -c) echo "bytes=$BYTES" >> $GITHUB_OUTPUT - name: Format size id: fmt run: | python3 -c " b = ${{ steps.size.outputs.bytes }} if b >= 1024*1024: s = f'{b/1024/1024:.1f} MB' elif b >= 1024: s = f'{b/1024:.1f} kB' else: s = f'{b} B' print('size=' + s) " >> $GITHUB_OUTPUT - name: Update Gist badge uses: schneegans/dynamic-badges-action@v1.7.0 with: auth: ${{ secrets.GIST_TOKEN }} gistID: dd4a46f4c2fff1c43d4f2e8fb4b52862 filename: bundle-size.json label: minified (gzip) message: ${{ steps.fmt.outputs.size }} color: blue tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 24 - run: npm install - name: Run tests with JSON reporter id: test run: | NODE_OPTIONS='--no-warnings=ExperimentalWarning' npx jest --json --outputFile=jest-results.json || true - name: Extract test counts id: counts run: | python3 -c " import json with open('jest-results.json') as f: data = json.load(f) passed = data['numPassedTests'] total = data['numTotalTests'] print(f'passed={passed}') print(f'total={total}') print(f'message={passed}/{total}') color = '#439e2e' if passed == total else 'red' print(f'color={color}') " >> $GITHUB_OUTPUT - name: Update Gist badge uses: schneegans/dynamic-badges-action@v1.7.0 with: auth: ${{ secrets.GIST_TOKEN }} gistID: dd4a46f4c2fff1c43d4f2e8fb4b52862 filename: tests.json label: passing tests message: ${{ steps.counts.outputs.message }} color: ${{ steps.counts.outputs.color }} sandbox-badge: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 24 - name: Prepare sandbox badge id: sandbox_badge run: | # collapse SVG to a single line svg_inline="$(tr -d '\n' < ./logo.svg)" echo "message=Sandbox protected" >> "$GITHUB_OUTPUT" echo "color=#202830" >> "$GITHUB_OUTPUT" echo "label=" >> "$GITHUB_OUTPUT" echo "logoSvg=${svg_inline}" >> "$GITHUB_OUTPUT" - name: Update Gist badge uses: schneegans/dynamic-badges-action@v1.7.0 with: auth: ${{ secrets.GIST_TOKEN }} gistID: dd4a46f4c2fff1c43d4f2e8fb4b52862 filename: sandbox-protected.json label: ${{ steps.sandbox_badge.outputs.label }} message: ${{ steps.sandbox_badge.outputs.message }} color: ${{ steps.sandbox_badge.outputs.color }} logoSvg: ${{ steps.sandbox_badge.outputs.logoSvg }} ================================================ FILE: .github/workflows/test.yml ================================================ name: Run Tests on: push: branches: - main pull_request: branches: - '**' # Optional: also run on PRs jobs: test: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '22' - name: Install dependencies run: npm install - name: Run lint run: npm run lint - name: Run tests run: npm test - name: Upload coverage to Codecov uses: codecov/codecov-action@v4 with: files: ./coverage/lcov.info fail_ci_if_error: false token: ${{ secrets.CODECOV_TOKEN }} performance: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '22' - name: Install dependencies run: npm install - name: Build run: npm run build - name: Run performance tests run: npm run test:perf ================================================ FILE: .gitignore ================================================ node_modules/ dist/ build/ coverage/ ================================================ FILE: .husky/pre-commit ================================================ npx lint-staged ================================================ FILE: .npmignore ================================================ node_modules/ .github/ .gitignore .git/ .vscode/ .husky .claude js/ test/ css/ src/ index.html tsconfig.json rollup.config.mjs .eslintrc.json .prettierrc jest.config.js tsconfig.jest.json package-lock.json ================================================ FILE: .prettierignore ================================================ # Dependencies node_modules # Build outputs dist build coverage # Minified files *.min.js # Configuration files rollup.config.mjs jest.config.js # Test files that should be ignored test/eval/jquery.min.js ================================================ FILE: .prettierrc ================================================ { "printWidth": 100, "singleQuote": true } ================================================ FILE: .vscode/settings.json ================================================ { "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true } ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2019 nyariv Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ [![GitHub](https://img.shields.io/github/license/nyariv/SandboxJS)](https://github.com/nyariv/SandboxJS/blob/main/LICENSE) ![npm](https://img.shields.io/npm/v/@nyariv/sandboxjs) ![Bundle size](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/nyariv/dd4a46f4c2fff1c43d4f2e8fb4b52862/raw/bundle-size.json) [![GitHub issues](https://img.shields.io/github/issues-raw/nyariv/SandboxJS)](https://github.com/nyariv/SandboxJS/issues) [![codecov](https://codecov.io/gh/nyariv/SandboxJS/branch/main/graph/badge.svg)](https://codecov.io/gh/nyariv/SandboxJS) [![passing tests](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/nyariv/dd4a46f4c2fff1c43d4f2e8fb4b52862/raw/tests.json)](https://nyariv.github.io/SandboxJS/) [![Sandbox protected](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/nyariv/dd4a46f4c2fff1c43d4f2e8fb4b52862/raw/sandbox-protected.json)](https://github.com/nyariv/SandboxJS) ---
![sanboxjs logo](./logo.svg)

SandboxJS

Safe eval runtime
--- This is a javascript sandboxing library. When embedding any kind of js code inside your app (either web or nodejs based) you are essentially giving access to the entire kingdom, hoping there is no malicious code in a dependency such as with supply chain attacks. For securing code, sandboxing is needed. > a "sandbox" is a security mechanism for separating running programs, usually in an effort to mitigate system failures or software vulnerabilities from spreading. It is often used to execute untested or untrusted programs or code, possibly from unverified or untrusted third parties, suppliers, users or websites, without risking harm to the host machine or operating system. - Wikipedia There are many vulnerable modules available on the global scope of a js environment, and unfortunately it is way to easy to get access to that if 3rd party code is allowed to be included. The main way is through the `eval` or `Function` globals because they execute code in the global context. Trying to block access to some global components through proxies or limiting scope variables is fruitless because of one main issue: _every function inherits the `Function` prototype, and can invoke `eval` by calling its constructor_, essentially making `eval` at most two properties away from anything in js. Example: ```javascript [].filter.constructor("alert('jailbreak')")() ``` To make matters worse, it is extremely difficult to blacklist functions because code is easily obfuscated. For example, it is possible to execute anything using only `(`, `)`, `[`, `]`, `!`, and `+`. ([source](http://www.jsfuck.com/)) ```javascript [+!+[]]+[] // This evaluates to the number one, go a head type that in console ``` **SandboxJS** solves this problem by parsing js code and executing it though its own js runtime, while in the process checking every single prototype function that is being called. This allows whitelisting anything and everything, regardless of obfuscation. This means that you can potentially give different libraries different permissions, such as allowing `fetch()` for one library, or allowing access to the `Node` prototype for another, depending what the library requires and nothing more, and any objects that are gotten from the sandbox will remain sandboxed when used outside of it. Additionaly, `eval` and `Function` are sandboxed as well, and can be used recursively safely, which is why they are considered safe globals in SandboxJS. There is an `audit` method that will return all the accessed functions and prototypes during runtime if you need to know what permissions to give a certain library. Since parsing and executing are separated, execution with SandboxJS can be sometimes even faster than `eval`, allowing to prepare the execution code ahead of time. ## Installation ``` npm install @nyariv/sandboxjs ``` ## Usage The following is the bare minimum of code for using SandboxJS. This assumes safe whitelisted defaults. ```javascript const code = `return myTest;`; const scope = { myTest: "hello world" }; const sandbox = new Sandbox(); const exec = sandbox.compile(code); const result = exec(scope).run(); // result: "hello world" ``` It is possible to defined multiple scopes in case you are reusing scopes with multiple layers. ```javascript const sandbox = new Sandbox(); const scopeA = {a: 1}; const scopeB = {b: 2}; const scopeC = {c: 3}; const code = `a = 4; let d = 5; let b = 6`; const exec = sandbox.compile(code); exec(scopeA, scopeB, scopeC).run(); console.log(scopeA); // {a: 4} console.log(scopeB); // {b: 2} console.log(scopeC); // {c: 3, d: 5, b: 6} ``` You can set your own whilelisted prototypes and global properties like so (`alert` and `Node` are added to whitelist in the following code): ```javascript const prototypeWhitelist = Sandbox.SAFE_PROTOTYPES; prototypeWhitelist.set(Node, new Set()); const globals = {...Sandbox.SAFE_GLOBALS, alert}; const sandbox = new Sandbox({globals, prototypeWhitelist}); ``` You can audit a piece of code, which will permit all globals and prototypes but will return a json with accessed globals and prototypes over time. ```javascript const code = `console.log("test")`; console.log(Sandbox.audit(code)); ``` ## Safe Globals - `globalThis` - `Function` - `eval` - `setTimeout` - excluded by default - `setInterval` - excluded by default - `clearTimeout` - excluded by default - `clearInterval` - excluded by default - `console` - `isFinite` - `isNaN` - `parseFloat` - `parseInt` - `decodeURI` - `decodeURIComponent` - `encodeURI` - `encodeURIComponent` - `escape` - `unescape` - `Boolean` - `Number` - `BigInt` - `String` - `Object` - `Array` - `Symbol` - `Error` - `EvalError` - `RangeError` - `ReferenceError` - `SyntaxError` - `TypeError` - `URIError` - `Int8Array` - `Uint8Array` - `Uint8ClampedArray` - `Int16Array` - `Uint16Array` - `Int32Array` - `Uint32Array` - `Float32Array` - `Float64Array` - `Map` - `Set` - `WeakMap` - `WeakSet` - `Promise` - `Intl` - `JSON` - `Math` # Safe Prototypes - `SandboxGlobal` - `Function` - `Boolean` - `Object` - `Number` - `BigInt` - `String` - `Date` - `RegExp` - `Error` - `Array` - `Int8Array` - `Uint8Array` - `Uint8ClampedArray` - `Int16Array` - `Uint16Array` - `Int32Array` - `Uint32Array` - `Float32Array` - `Float64Array` - `Map` - `Set` - `WeakMap` - `WeakSet` - `Promise` ## Goals |Feature|Status| |---|---| |Prototype access protection|done| |Globals access protection|done| |Prototype proxying|done| |Single line sandboxing|done| |Multi line sandboxing|done| |Functions support|done| |Audit prototype and globals access|done| |Code blocks (try/catch, ifs, and loops)|done| |Async/await|done| |Execution time protection|done| |Extensibility|done| |Full ECMAScript support|90%| |Script source and import sandboxing|Won't fix - handled by 3rd party| |DOM ownership and inherited permissions|See [scope-js](https://github.com/nyariv/scope-js)| |Tests|done| 📋 **[ECMAScript Feature Implementation Status](TODO.md)** - See which JavaScript features are supported and tested (~90% of core ES5-ES2018 features) ================================================ FILE: TODO.md ================================================ # SandboxJS - ECMAScript Feature Status [![codecov](https://codecov.io/gh/nyariv/SandboxJS/branch/main/graph/badge.svg)](https://codecov.io/gh/nyariv/SandboxJS) This document describes the current implementation status of ECMAScript features in SandboxJS. **Test Coverage**: 1598 total tests | Code Coverage: ~97% statement coverage, ~92% branch coverage --- ## 🐛 Known Bugs & Limitations The following limitations have been identified during testing: 1. **Unicode identifier escapes** - `\uXXXX` escape sequences in variable names are not supported --- ## ✅ Supported Features SandboxJS supports the following ECMAScript features with comprehensive test coverage: ### Arithmetic Operators - ✅ **Addition** - `1+1` → `2` - ✅ **Subtraction** - `1 * 2 + 3 * (4 + 5) * 6` → `164` - ✅ **Multiplication** - `1 * 2` → `2` - ✅ **Division** - `1+2*4/5-6+7/8 % 9+10-11-12/13*14` → `-16.448...` - ✅ **Modulus** - `test2 %= 1` → `0` - ✅ **Exponentiation** - `2 ** 3` → `8`, `3 ** 2 ** 2` → `81` - ✅ **Exponentiation assignment** - `test2 **= 0` → `1` - ✅ **Unary plus** - `+'1'` → `1` - ✅ **Unary minus** - `-'1'` → `-1` ### Logical Operators - ✅ **Logical AND** - `true && true || false` → `true` - ✅ **Logical OR** - `test2 || false` → `3` - ✅ **Logical NOT** - `!test2` → `false`, `!!test2` → `true` - ✅ **Nullish coalescing** - `null ?? 'default'` → `'default'`, `0 ?? 'default'` → `0` ### Comparison Operators - ✅ **Equality** - `test2 == '3'` → `true` - ✅ **Strict equality** - `test2 === '3'` → `false` - ✅ **Inequality** - `test2 != '3'` → `false` - ✅ **Strict inequality** - `test2 !== '3'` → `true` - ✅ **Less than** - `test2 < 3` → `false` - ✅ **Greater than** - `test2 > 3` → `false` - ✅ **Less than or equal** - `test2 <= 3` → `true` - ✅ **Greater than or equal** - `test2 >= 3` → `true` ### Bitwise Operators - ✅ **Bitwise AND** - `test2 & 1` → `1` - ✅ **Bitwise OR** - `test2 | 4` → `7` - ✅ **Bitwise NOT** - `~test2` → `-2` - ✅ **Bitwise XOR** - `test2 ^= 1` → `1` - ✅ **Left shift** - Tested in complex expressions - ✅ **Right shift** - Tested in complex expressions - ✅ **Unsigned right shift** - Tested in complex expressions - ✅ **Left shift assignment** - `let x = 5; x <<= 1` → `10` - ✅ **Right shift assignment** - `let x = 8; x >>= 1` → `4` - ✅ **Unsigned right shift assignment** - `let x = 8; x >>>= 2` → `2` - ✅ **XOR assignment** - `test2 ^= 1` → `1` - ✅ **AND assignment** - `test2 &= 3` → `1` - ✅ **OR assignment** - `test2 |= 2` → `3` ### Assignment Operators - ✅ **Simple assignment** - `test2 = 1` → `1` - ✅ **Addition assignment** - `test2 += 1` → `2` - ✅ **Subtraction assignment** - `test2 -= 1` → `1` - ✅ **Multiplication assignment** - `test2 *= 2` → `2` - ✅ **Division assignment** - `test2 /= 2` → `1` - ✅ **Exponentiation assignment** - `test2 **= 0` → `1` - ✅ **Modulus assignment** - `test2 %= 1` → `0` - ✅ **XOR assignment** - `test2 ^= 1` → `1` - ✅ **AND assignment** - `test2 &= 3` → `1` - ✅ **OR assignment** - `test2 |= 2` → `3` - ✅ **Logical AND assignment (&&=)** - `let x = 10; x &&= 5` → `5` - ✅ **Logical OR assignment (||=)** - `let x = 0; x ||= 5` → `5` - ✅ **Nullish coalescing assignment (??=)** - `let x = null; x ??= 5` → `5` - ✅ **Post-increment** - `test2++` → `1` - ✅ **Pre-increment** - `++test2` → `3` ### Other Operators - ✅ **Conditional (ternary)** - `test[test2] ? true : false ? 'not ok' : 'ok'` → `'ok'` - ✅ **Optional chaining** - `!({}).a?.a` → `true`, `({}).a?.toString()` → `undefined` - ✅ **Comma operator** - `1,2` → `2` - ✅ **typeof** - `typeof '1'` → `'string'`, `typeof x === 'undefined'` → `true` - ✅ **instanceof** - `{} instanceof Object` → `true` - ✅ **in operator** - `'a' in {a: 1}` → `true` - ✅ **delete operator** - `delete 1` → `true`, `let a = {b: 1}; return delete a.b` → `true` - ✅ **void operator** - `void 2 == '2'` → `false` - ✅ **new operator** - `new Date(0).toISOString()` → `'1970-01-01T00:00:00.000Z'` ### Data Types - ✅ **Numbers** - `2.2204460492503130808472633361816E-16` → Scientific notation - ✅ **BigInt** - `(1n + 0x1n).toString()` → `'2'` - ✅ **Binary literals** - `0b1010` → `10`, `0B1111` → `15`, `0b1010n` → `'10'` (BigInt), `0b1_000` → `8` (with separators) - ✅ **Octal literals** - `0o17` → `15`, `0O77` → `63`, `0o17n` → `'15'` (BigInt), `0o7_777` → `4095` (with separators) - ✅ **Strings** - `"test2"` → `'test2'` - ✅ **Template literals** - `` `test2 is ${`also ${test2}`}` `` → `'test2 is also 1'` - ✅ **Tagged template functions** - ``tag`hello ${"world"}` `` → function receives string parts and interpolated values - ✅ **Escape sequences** - `"\\"` → `'\\'`, `"\\xd9"` → `'Ù'`, `"\\n"` → `'\n'` - ✅ **Boolean** - `true`, `false` - ✅ **null** - `null ?? 'default'` → `'default'` - ✅ **undefined** - `typeof x === 'undefined'` → `true` - ✅ **Arrays** - `[test2, 2]` → `[1, 2]` - ✅ **Objects** - `{"aa": test[0](), b: test2 * 3}` → `{ "aa": 1, "b": 3 }` - ✅ **Regular expressions** - `/a/.test('a')` → `true`, `/a/i.test('A')` → `true` ### Objects & Arrays - ✅ **Object literals** - `{a: 1, b: 2}` → `{ a: 1, b: 2 }` - ✅ **Array literals** - `[1, 2]` → `[1, 2]` - ✅ **Property access (dot)** - `a.b.c` → `2` - ✅ **Property access (bracket)** - `a['b']['c']` → `2` - ✅ **Computed property names** - `{"aa": test[0]()}` → `{ "aa": 1 }` - ✅ **Object spread** - `{a: 1, ...{b: 2, c: {d: test2,}}, e: 5}` → Full object - ✅ **Array spread** - `[1, ...[2, [test2, 4]], 5]` → `[1, 2, [3, 4], 5]` - ✅ **Object method shorthand** - `let y = {a: 1, b(x) {return this.a + x}}; return y.b(2)` → `3` ### Functions - ✅ **Function declarations** - `function f(a) { return a + 1 } return f(2);` → `3` - ✅ **Function expressions** - `(function () { return 1 })()` → `1` - ✅ **Arrow functions (single param)** - `(a => a + 1)(1)` → `2` - ✅ **Arrow functions (multiple params)** - `((a) => {return a + 1})(1)` → `2` - ✅ **Arrow functions (expression body)** - `(a => a + 1)(1)` → `2` - ✅ **Arrow functions (block body)** - `(() => {return 1})()` → `1` - ✅ **Async arrow functions** - `(async () => 1)()` → `1` - ✅ **Async function expressions** - `(async () => await 1)()` → `1` - ✅ **Rest parameters** - `[0,1].filter((...args) => args[1])` → `[1]` - ✅ **Parameter default values** - `function fn(a = 1) { return a; }` → `1` - ✅ **Spread in function calls** - `Math.pow(...[2, 2])` → `4` - ✅ **Constructor functions** - `function LinkedListNode(e){this.value=e,this.next=null}` with `new` - ✅ **Recursive functions** - Linked list reverse example #### Destructuring - ✅ **Array destructuring** - `const [a, b] = [1, 2]` - ✅ **Object destructuring** - `const {a, b} = {a: 1, b: 2}` - ✅ **Nested destructuring** - `const {a: {b}} = {a: {b: 42}}` - ✅ **Destructuring with defaults** - `const {a = 1} = {}` - ✅ **Custom variable names (renaming)** - `const {a: myA} = {a: 1}` - ✅ **Destructuring in function parameters** - `function fn({a, b}) { }` - ✅ **Rest in destructuring** - `const [a, ...rest] = [1, 2, 3]`, `const {a, ...rest} = obj` - ✅ **Computed property names in destructuring** - `const {[key]: val} = obj` - ✅ **Destructuring in for-of/for-in loops** - `for (const [a, b] of arr) { }` - ✅ **Destructuring in function parameters with defaults** - `function fn({a = 1, b = 2} = {}) { }` ### Control Flow #### Conditionals - ✅ **if statement** - `if (true) { return true; } else return false` → `true` - ✅ **else statement** - `if (false) { return true; } else return false` → `false` - ✅ **if/else chains** - `if (false) return true; else if (false) {return true} else return false` → `false` - ✅ **Nested if statements** - Complex nested if/else with 9 levels - ✅ **Inline ternary** - `true ? 1 : 2` → `1` #### Loops - ✅ **for loop** - `let x; for(let i = 0; i < 2; i++){ x = i }; return x;` → `1` - ✅ **while loop** - `let x = 2; while(--x){ }; return x;` → `0` - ✅ **do-while loop** - `let x = 1; do {x++} while(x < 1); return x;` → `2` - ✅ **for-of loop** - `for(let i of [1,2]){ return i };` → `1` - ✅ **for-in loop** - `for(let i in [1,2]){ return i };` → `'0'` - ✅ **break statement** - `for(let i = 0; i < 2; i++){ x = i; break; }` → Exits early - ✅ **continue statement** - `for (let i = 0; i < 5; i++) { if (i === 2) continue; sum += i; }` → Skips iteration #### Switch - ✅ **switch statement** - `switch(1) {case 1: b = 2; break; case 2: b = 3; default: b = 4}; return b` → `2` - ✅ **case clauses** - Multiple case tests - ✅ **default clause** - `switch(3) {case 1: b = 2; break; case 2: b = 3; default: b = 4}; return b` → `4` - ✅ **Fall-through behavior** - `switch(1) {case 1:b = 2; case 2: b = 3; default: b = 4}; return b` → `4` #### Error Handling - ✅ **try/catch** - `try {a.x.a} catch {return 1}; return 2` → `1` - ✅ **try/catch with exception variable** - `try { throw new Error('msg'); } catch(e) { return e.message; }` → `'msg'` - ✅ **finally block** - `try { return 1; } finally { x = 2; }` → Finally executes before return - ✅ **finally overrides return** - `try { return 1; } finally { return 2; }` → `2` - ✅ **finally overrides error** - `try { throw Error('a'); } finally { throw Error('b'); }` → Error: 'b' - ✅ **throw statement** - `throw new Error('test')` → Error with message #### Other - ✅ **Code blocks** - `{let j = 1; i += j;}` → Block scope - ✅ **this binding** - `let y = {a: 1, b(x) {return this.a + x}}` → Method context - ✅ **Closures** - `const a = () => {return 1}; const b = () => {return 2}; return (() => a() + b())()` → `3` ### Variables - ✅ **var declaration** - `var i = 1; return i + 1` → `2` - ✅ **let declaration** - `let j = 1; return j + 1` → `2` - ✅ **const declaration** - `const k = 1; return k + 1` → `2` - ✅ **const immutability** - `const l = 1; return l = 2` → Error ### Async/Await - ✅ **async functions** - `(async () => 1)()` → Promise resolves to `1` - ✅ **await keyword** - `(async () => await 1)()` → `1` - ✅ **await with promises** - `(async () => await (async () => 1)())()` → `1` - ✅ **async with variables** - `let p = (async () => 1)(); return (async () => 'i = ' + await p)()` → `'i = 1'` - ✅ **Async arrow functions** - `let i = 0; (async () => i += 1)(); return i;` → `1` - ✅ **for-await-of loops** - `for await (const item of asyncIterable) { }` #### Generators (ES6) - ✅ **Generator functions (function*)** - `function* gen() { yield 1; }` - ✅ **yield keyword** - ✅ **`yield` as expression value** - `const x = yield 1` - ✅ **yield* delegation** - ✅ **Async generators** - `async function* gen() { yield 1; }` - ✅ **Iterator `.return()`/`.throw()`** - Protocol methods for early termination and error injection - ✅ **`next(value)` injection** - Sending values back into a paused generator ### Other Built-in Objects - ✅ **WeakMap** - All methods work: `set()`, `get()`, `has()`, `delete()` (6 tests) - ✅ **WeakSet** - All methods work: `add()`, `has()`, `delete()` ### Comments - ✅ **Single-line comments** - `1 // 2` → `1` - ✅ **Multi-line comments** - `/* 2 */ 1` → `1` ### Operator Precedence Comprehensive operator precedence testing has been implemented with 35 tests covering: - NOT (!) with comparison operators - Logical NOT with AND/OR - Comparison operator chaining - Bitwise vs logical operators - Bitwise shift with arithmetic - Mixed bitwise operators (correct precedence: shift > & > ^ > |) - Exponentiation (right-associative) - typeof, delete, void with various operators - Optional chaining and nullish coalescing - Increment/decrement with arithmetic - Multiple unary operators - Comma operator in expressions --- ## ❌ Not Supported Features The following ECMAScript features are not currently supported in SandboxJS: ### HIGH PRIORITY #### Classes (ES6) - ❌ **class declarations** - ❌ **extends keyword (inheritance)** - ❌ **super keyword** - ❌ **Static methods** - ❌ **Class fields (public)** - ❌ **Private fields (#field)** - ❌ **Private methods** - ❌ **Static class fields** - ❌ **Static initialization blocks** #### Object Features - ❌ **Getters in object literals** - `{get prop() { return 1; }}` - ❌ **Setters in object literals** - `{set prop(v) { this.val = v; }}` ### LOW PRIORITY #### Modules Module features are not supported by design as SandboxJS is intended for sandboxed code execution: - ❌ **import statements** - ❌ **export statements** - ❌ **Dynamic import()** - ❌ **import.meta** --- ## 🔒 Security-Related Restrictions (Intentionally Blocked) These features are intentionally blocked for security reasons: - 🔒 Direct access to global scope - 🔒 Access to `__proto__` (prototype pollution prevention) - 🔒 Global object pollution - 🔒 Prototype method overriding - 🔒 Access to non-whitelisted globals - 🔒 Access to non-whitelisted prototype methods - 🔒 `with` statement - 🔒 `arguments` object (security risk, use rest parameters `...args` instead) - 🔒 Execution beyond quota limits --- ## 📝 Notes - **Priority Levels**: - **HIGH**: Common patterns used frequently in production code - **MEDIUM**: Less common but still important for completeness - **LOW**: Edge cases and advanced features with limited use - **Implementation Focus**: SandboxJS focuses on core ES5-ES2018 features with strong security controls - **Performance**: Advanced meta-programming features are omitted to maintain sandbox safety ================================================ FILE: css/style.css ================================================ :root { --bg: #0f1117; --surface: #1a1d27; --surface2: #22263a; --border: #2e3248; --accent: #4a9eff; --accent2: #ffe94d; --text: #e4e8f0; --muted: #8892aa; --pass: #34d399; --fail: #f87171; --header-h: 130px; --sidebar-w: 260px; --font-bump: 0px; font-size: 15px; } *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } body { background: var(--bg); color: var(--text); font-family: 'Inter', 'Segoe UI', system-ui, sans-serif; font-size: calc(1rem + var(--font-bump)); min-height: 100vh; } body.modal-open { overflow: hidden; } /* ── Header ──────────────────────────────────────────────── */ .site-header { position: sticky; top: 0; z-index: 100; background: var(--surface); border-bottom: 1px solid var(--border); padding: 14px 24px 10px; backdrop-filter: blur(10px); } .header-inner { display: flex; align-items: center; gap: 20px; flex-wrap: wrap; } .brand { display: flex; align-items: center; gap: 14px; flex: 1 1 auto; } .logo { width: 48px; height: 48px; flex-shrink: 0; } .brand-text h1 { font-size: calc(1.4rem + var(--font-bump)); font-weight: 700; letter-spacing: -0.3px; color: var(--text); } .tagline { font-size: calc(0.75rem + var(--font-bump)); color: var(--muted); letter-spacing: 0.5px; text-transform: uppercase; } .controls { display: flex; align-items: center; gap: 14px; flex-shrink: 0; } /* toggle */ .toggle-label { display: flex; align-items: center; gap: 8px; cursor: pointer; font-size: calc(0.85rem + var(--font-bump)); color: var(--muted); user-select: none; } .toggle-label input { display: none; } .toggle-track { position: relative; width: 34px; height: 18px; background: var(--border); border-radius: 9px; transition: background 0.2s; } .toggle-thumb { position: absolute; top: 3px; left: 3px; width: 12px; height: 12px; background: var(--muted); border-radius: 50%; transition: left 0.2s, background 0.2s; } .toggle-label input:checked + .toggle-track, .settings-toggle-label input:checked + .toggle-track { background: var(--accent); } .toggle-label input:checked + .toggle-track .toggle-thumb, .settings-toggle-label input:checked + .toggle-track .toggle-thumb { left: 19px; background: #fff; } .select-wrap select { background: var(--surface2); color: var(--text); border: 1px solid var(--border); border-radius: 6px; padding: 5px 10px; font-size: calc(0.85rem + var(--font-bump)); cursor: pointer; outline: none; appearance: none; padding-right: 28px; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%238892aa'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 8px center; } .run-btn { background: var(--accent); color: #fff; border: none; border-radius: 6px; padding: 6px 16px; font-size: calc(0.85rem + var(--font-bump)); font-weight: 600; cursor: pointer; transition: opacity 0.15s; } .run-btn:hover { opacity: 0.85; } .secondary-btn, .icon-btn { background: var(--surface2); color: var(--text); border: 1px solid var(--border); border-radius: 6px; padding: 6px 16px; font-size: calc(0.85rem + var(--font-bump)); font-weight: 600; cursor: pointer; transition: border-color 0.15s, background 0.15s, opacity 0.15s; } .secondary-btn:hover, .icon-btn:hover { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 12%, var(--surface2)); } .icon-btn { padding-inline: 12px; } .description { margin-top: 8px; font-size: calc(0.78rem + var(--font-bump)); color: var(--muted); line-height: 1.5; max-width: 900px; } .description code { background: var(--surface2); border: 1px solid var(--border); border-radius: 3px; padding: 1px 4px; font-family: 'Fira Code', 'Cascadia Code', monospace; font-size: calc(0.75rem + var(--font-bump)); color: var(--accent); } /* ── Layout ──────────────────────────────────────────────── */ .page-body { display: flex; gap: 0; min-height: calc(100vh - var(--header-h)); } /* ── Sidebar ─────────────────────────────────────────────── */ .sidebar { width: var(--sidebar-w); flex-shrink: 0; padding: 20px 16px; border-right: 1px solid var(--border); position: sticky; top: var(--header-h); height: calc(100vh - var(--header-h)); overflow-y: auto; display: flex; flex-direction: column; gap: 20px; } @media (min-width: 721px) { .sidebar { padding-top: 0; } } .card-title { font-size: calc(0.7rem + var(--font-bump)); font-weight: 600; letter-spacing: 1px; text-transform: uppercase; color: var(--muted); margin-bottom: 10px; } /* perf card */ .perf-card { background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 14px; } #times { width: 100%; border-collapse: collapse; } #times th, #times td { padding: 4px 6px; font-size: calc(0.78rem + var(--font-bump)); border: none; color: var(--text); } #times th { color: var(--muted); font-weight: 500; } #times td:not(:first-child) { text-align: right; font-variant-numeric: tabular-nums; font-family: 'Fira Code', monospace; font-size: calc(0.75rem + var(--font-bump)); } #times tr { border-bottom: 1px solid var(--border); } #times tr:last-child { border-bottom: none; } /* category nav */ .category-nav { flex: 1; } #category-list { list-style: none; display: flex; flex-direction: column; gap: 2px; } #category-list li .category-link { display: flex; align-items: center; justify-content: space-between; width: 100%; padding: 6px 10px; border-radius: 6px; border: none; background: transparent; text-align: left; color: var(--muted); font-size: calc(0.82rem + var(--font-bump)); transition: background 0.15s, color 0.15s; cursor: pointer; } #category-list li .category-link:hover { background: var(--surface2); color: var(--text); } #category-list li .category-link.active { background: color-mix(in srgb, var(--accent) 15%, transparent); color: var(--accent); } .cat-badge { font-size: calc(0.7rem + var(--font-bump)); background: var(--surface2); border-radius: 10px; padding: 1px 7px; color: var(--muted); font-variant-numeric: tabular-nums; } .cat-pass { color: var(--pass); } .cat-fail { color: var(--fail); } /* ── Main ────────────────────────────────────────────────── */ .tests-main { flex: 1; padding: 20px 24px; min-width: 0; } /* summary */ .summary-bar { display: flex; gap: 16px; margin-bottom: 20px; flex-wrap: wrap; } .summary-chip { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 8px 16px; display: flex; flex-direction: column; gap: 2px; } .summary-chip .chip-val { font-size: calc(1.4rem + var(--font-bump)); font-weight: 700; font-variant-numeric: tabular-nums; } .summary-chip .chip-label { font-size: calc(0.72rem + var(--font-bump)); color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; } .chip-pass .chip-val { color: var(--pass); } .chip-fail .chip-val { color: var(--fail); } /* sections */ .test-section { margin-bottom: 32px; } .section-header { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; cursor: pointer; user-select: none; } .section-title { font-size: 1.6rem; font-weight: 600; color: var(--text); } .section-count { font-size: calc(0.75rem + var(--font-bump)); color: var(--muted); background: var(--surface2); border: 1px solid var(--border); border-radius: 12px; padding: 1px 9px; } .section-toggle { margin-left: auto; font-size: calc(0.7rem + var(--font-bump)); color: var(--muted); transition: transform 0.2s; } .test-section.collapsed .section-toggle { transform: rotate(-90deg); } .test-section.collapsed .section-body { display: none; } .section-pass-rate { font-size: calc(0.72rem + var(--font-bump)); padding: 2px 8px; border-radius: 10px; font-weight: 600; } .rate-good { background: color-mix(in srgb, var(--pass) 15%, transparent); color: var(--pass); } .rate-bad { background: color-mix(in srgb, var(--fail) 15%, transparent); color: var(--fail); } /* table */ .section-body { border: 1px solid var(--border); border-radius: 10px; overflow: hidden; } table.test-table { width: 100%; border-collapse: collapse; table-layout: fixed; } table.test-table col.col-code { width: 46%; } table.test-table col.col-eval { width: 22%; } table.test-table col.col-sandbox { width: 22%; } table.test-table col.col-verdict { width: 10%; } table.test-table thead th { background: var(--surface2); color: var(--muted); font-size: calc(0.72rem + var(--font-bump)); font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; padding: 8px 12px; text-align: left; border-bottom: 1px solid var(--border); } table.test-table tbody tr { border-bottom: 1px solid var(--border); transition: background 0.1s; } table.test-table tbody tr:last-child { border-bottom: none; } table.test-table tbody tr:hover { background: var(--surface2); } table.test-table td { padding: 7px 12px; font-size: calc(0.82rem + var(--font-bump)); vertical-align: middle; } td.td-code { font-family: 'Fira Code', 'Cascadia Code', monospace; font-size: calc(0.78rem + var(--font-bump)); color: var(--accent); max-width: 340px; } .td-code-wrap { display: flex; align-items: center; gap: 10px; min-width: 0; } .td-code-text { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1; } .runner-link { display: inline-flex; align-items: center; justify-content: center; width: 2.3rem; height: 2.3rem; flex-shrink: 0; border-radius: 999px; border: 1px solid var(--border); background: linear-gradient(180deg, color-mix(in srgb, var(--surface2) 92%, white 8%), var(--surface2)); color: var(--accent); text-decoration: none; box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.05); transition: transform 0.15s, background 0.15s, border-color 0.15s, color 0.15s, box-shadow 0.15s; } .runner-link-icon { width: 1.45rem; height: 1.45rem; } .runner-link:hover { background: color-mix(in srgb, var(--accent) 12%, var(--surface2)); border-color: var(--accent); color: #fff; transform: translateY(-1px); box-shadow: 0 6px 14px rgb(0 0 0 / 0.18); } .runner-link:focus-visible { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px rgb(74 158 255 / 0.18); } td.td-result { font-family: 'Fira Code', 'Cascadia Code', monospace; font-size: calc(0.75rem + var(--font-bump)); color: var(--text); max-width: 180px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } td.td-verdict { font-weight: 700; font-size: calc(0.78rem + var(--font-bump)); white-space: nowrap; text-align: left; } .positive { color: var(--pass); } .negative { color: var(--fail); } .error { cursor: help; color: var(--fail); } .hidden { display: none; } .modal-shell { position: fixed; inset: 0; z-index: 300; } .modal-backdrop { position: absolute; inset: 0; background: rgb(8 10 16 / 0.72); backdrop-filter: blur(10px); } .sandbox-modal { position: relative; width: min(900px, calc(100vw - 32px)); max-height: calc(100vh - 48px); margin: 24px auto; background: linear-gradient(180deg, rgb(31 36 54 / 0.98), rgb(20 24 36 / 0.98)); border: 1px solid color-mix(in srgb, var(--accent) 22%, var(--border)); border-radius: 18px; box-shadow: 0 30px 80px rgb(0 0 0 / 0.45); overflow: hidden; display: flex; flex-direction: column; } .sandbox-modal-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; padding: 20px 22px 16px; border-bottom: 1px solid var(--border); } .sandbox-modal-header h2 { font-size: calc(1.1rem + var(--font-bump)); margin-bottom: 4px; } .sandbox-modal-header p { font-size: calc(0.82rem + var(--font-bump)); color: var(--muted); line-height: 1.5; } .sandbox-modal-body { padding: 20px 22px 22px; overflow: auto; display: grid; gap: 18px; } .sandbox-field { display: grid; gap: 10px; } .sandbox-field-header { display: flex; align-items: center; gap: 10px; } .sandbox-field-label { font-size: calc(0.76rem + var(--font-bump)); font-weight: 600; letter-spacing: 0.8px; text-transform: uppercase; color: var(--muted); flex: 1; } .settings-gear-btn { position: relative; display: inline-flex; align-items: center; justify-content: center; width: 2rem; height: 2rem; border-radius: 6px; border: 1px solid var(--border); background: transparent; color: var(--muted); cursor: pointer; transition: color 0.15s, border-color 0.15s, background 0.15s; flex-shrink: 0; } .settings-gear-btn svg { width: 1.1rem; height: 1.1rem; } .settings-gear-btn:hover, .settings-gear-btn.active { color: var(--accent); border-color: var(--accent); background: color-mix(in srgb, var(--accent) 10%, transparent); } .settings-badge { position: absolute; top: -4px; right: -4px; min-width: 14px; height: 14px; background: var(--accent); color: #fff; font-size: 0.6rem; font-weight: 700; border-radius: 7px; display: flex; align-items: center; justify-content: center; padding: 0 3px; line-height: 1; } .settings-badge.hidden { display: none; } .sandbox-settings-panel { background: rgb(10 13 22 / 0.82); border: 1px solid var(--border); border-radius: 12px; overflow: hidden; } .settings-panel-inner { padding: 14px 16px; display: grid; gap: 14px; } .settings-section { display: grid; gap: 8px; } .settings-section-label { font-size: calc(0.7rem + var(--font-bump)); font-weight: 600; letter-spacing: 0.8px; text-transform: uppercase; color: var(--muted); } .settings-grid { display: flex; flex-wrap: wrap; gap: 10px 20px; } .settings-toggle-label { display: flex; align-items: center; gap: 8px; cursor: pointer; font-size: calc(0.82rem + var(--font-bump)); color: var(--text); user-select: none; font-family: 'Fira Code', 'Cascadia Code', monospace; } .settings-toggle-label input { display: none; } .settings-field-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; } .settings-input { background: var(--surface2); border: 1px solid var(--border); border-radius: 6px; color: var(--text); font-family: 'Fira Code', 'Cascadia Code', monospace; font-size: calc(0.82rem + var(--font-bump)); padding: 5px 10px; width: 160px; outline: none; transition: border-color 0.15s; appearance: textfield; -moz-appearance: textfield; } .settings-input::-webkit-outer-spin-button, .settings-input::-webkit-inner-spin-button { -webkit-appearance: none; } .settings-input:focus { border-color: var(--accent); } .settings-input.settings-field-error { border-color: var(--fail); } .settings-textarea { background: var(--surface2); border: 1px solid var(--border); border-radius: 6px; color: var(--text); font-family: 'Fira Code', 'Cascadia Code', monospace; font-size: calc(0.82rem + var(--font-bump)); padding: 7px 10px; width: 100%; resize: vertical; outline: none; transition: border-color 0.15s; line-height: 1.5; } .settings-textarea:focus { border-color: var(--accent); } .settings-textarea.settings-field-error { border-color: var(--fail); } .settings-field-hint { font-size: calc(0.72rem + var(--font-bump)); color: var(--muted); } .settings-reset-btn { align-self: start; background: transparent; color: var(--muted); border: 1px solid var(--border); border-radius: 6px; padding: 4px 12px; font-size: calc(0.78rem + var(--font-bump)); cursor: pointer; transition: color 0.15s, border-color 0.15s; } .settings-reset-btn:hover { color: var(--fail); border-color: var(--fail); } #sandbox-editor { border: 1px solid var(--border); border-radius: 12px; overflow: hidden; transition: border-color 0.15s, box-shadow 0.15s; } #sandbox-editor:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px rgb(74 158 255 / 0.18); } #sandbox-editor .cm-editor { background: rgb(12 15 24 / 0.92); } #sandbox-editor .cm-scroller { min-height: 240px; } .sandbox-actions { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; } .sandbox-ticks { font-size: calc(0.78rem + var(--font-bump)); color: var(--muted); font-family: 'Fira Code', 'Cascadia Code', monospace; white-space: nowrap; } .sandbox-ticks::before { content: 'execution ticks: '; opacity: 0.55; } .sandbox-status { margin-right: auto; font-size: calc(0.78rem + var(--font-bump)); font-weight: 700; letter-spacing: 0.4px; text-transform: uppercase; color: var(--muted); } .sandbox-status.status-running { color: var(--accent2); } .sandbox-status.status-success { color: var(--pass); } .sandbox-status.status-error { color: var(--fail); } .sandbox-status.status-halted { color: #f97316; } .sandbox-notice { display: flex; align-items: center; gap: 10px; border-radius: 12px; padding: 12px 14px; border: 1px solid var(--border); font-size: calc(0.82rem + var(--font-bump)); line-height: 1.45; } .sandbox-notice strong { font-size: calc(1rem + var(--font-bump)); } .sandbox-notice-safe { background: color-mix(in srgb, var(--pass) 14%, rgb(10 13 22 / 0.78)); border-color: color-mix(in srgb, var(--pass) 35%, var(--border)); color: var(--pass); } .sandbox-notice-critical { background: color-mix(in srgb, var(--fail) 15%, rgb(10 13 22 / 0.78)); border-color: color-mix(in srgb, var(--fail) 40%, var(--border)); color: #ffb2b2; } .sandbox-output-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; } .output-card { background: rgb(10 13 22 / 0.78); border: 1px solid var(--border); border-radius: 12px; overflow: hidden; } .output-card h3 { font-size: calc(0.78rem + var(--font-bump)); letter-spacing: 0.6px; text-transform: uppercase; color: var(--muted); padding: 12px 14px; border-bottom: 1px solid var(--border); } .output-card pre { min-height: 100px; padding: 14px; white-space: pre-wrap; word-break: break-word; font-family: 'Fira Code', 'Cascadia Code', monospace; font-size: calc(0.8rem + var(--font-bump)); line-height: 1.55; color: var(--text); } @media (max-width: 900px) { .sandbox-output-grid { grid-template-columns: 1fr; } } @media (max-width: 720px) { .controls { width: 100%; flex-wrap: wrap; } .page-body { flex-direction: column; } .sidebar { position: static; width: 100%; height: auto; border-right: none; border-bottom: 1px solid var(--border); } .tests-main { padding-inline: 16px; } .sandbox-modal { width: calc(100vw - 20px); max-height: calc(100vh - 20px); margin: 10px auto; } .sandbox-modal-header, .sandbox-actions { flex-direction: column; align-items: stretch; } .sandbox-status { margin-right: 0; } } /* scrollbar */ ::-webkit-scrollbar { width: 6px; height: 6px; } ::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; } ::-webkit-scrollbar-thumb:hover { background: var(--muted); } ================================================ FILE: eslint.config.js ================================================ const eslint = require('@eslint/js'); const tseslint = require('@typescript-eslint/eslint-plugin'); const tsparser = require('@typescript-eslint/parser'); const globals = require('globals'); const prettierConfig = require('eslint-config-prettier'); module.exports = [ // Global ignores { ignores: [ 'node_modules/**', 'dist/**', 'build/**', 'test/**', 'coverage/**', '*.min.js', 'test/eval/jquery.min.js', 'rollup.config.mjs', 'jest.config.js', 'eslint.config.js' ] }, // Base config for all TypeScript files { files: ['**/*.ts'], languageOptions: { parser: tsparser, parserOptions: { ecmaVersion: 'latest', sourceType: 'module', project: './tsconfig.json' }, globals: { ...globals.browser, ...globals.node, ...globals.es2021 } }, plugins: { '@typescript-eslint': tseslint }, rules: { ...eslint.configs.recommended.rules, ...tseslint.configs.recommended.rules, ...prettierConfig.rules, '@typescript-eslint/no-explicit-any': 'warn', '@typescript-eslint/no-empty-object-type': 'warn', '@typescript-eslint/no-unused-expressions': 'warn', '@typescript-eslint/no-unsafe-function-type': 'off', 'no-fallthrough': 'off', '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^lastLastLastLastPart' }], '@typescript-eslint/no-unsafe-assignment': 'off', '@typescript-eslint/no-this-alias': 'off' } }, // Scripts that are part of the test tsconfig { files: ['scripts/export-tests.ts'], languageOptions: { parser: tsparser, parserOptions: { ecmaVersion: 'latest', sourceType: 'module', project: './test/tsconfig.json' }, globals: { ...globals.node, ...globals.es2021 } }, plugins: { '@typescript-eslint': tseslint }, rules: { ...eslint.configs.recommended.rules, ...tseslint.configs.recommended.rules, ...prettierConfig.rules, '@typescript-eslint/no-explicit-any': 'warn', '@typescript-eslint/no-empty-object-type': 'warn', '@typescript-eslint/no-unused-expressions': 'warn', '@typescript-eslint/no-unsafe-function-type': 'off', 'no-fallthrough': 'off', '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^lastLastLastLastPart' }], '@typescript-eslint/no-unsafe-assignment': 'off', } } ]; ================================================ FILE: index.html ================================================ SandboxJS — Test Suite
================================================ FILE: jest.config.js ================================================ /** * For a detailed explanation regarding each configuration property, visit: * https://jestjs.io/docs/configuration */ /** @type {import('jest').Config} */ export default { // All imported modules in your tests should be mocked automatically // automock: false, // Stop running tests after `n` failures // bail: 0, // The directory where Jest should store its cached dependency information // cacheDirectory: "C:\\Users\\user\\AppData\\Local\\Temp\\jest", // Automatically clear mock calls, instances, contexts and results before every test // clearMocks: false, // Indicates whether the coverage information should be collected while executing the test collectCoverage: true, // An array of glob patterns indicating a set of files for which coverage information should be collected // collectCoverageFrom: undefined, // The directory where Jest should output its coverage files coverageDirectory: "coverage", // An array of regexp pattern strings used to skip coverage collection coveragePathIgnorePatterns: [ "/node_modules/", "/test/eval/testCases/.*\\.data\\.ts$", "/test/eval/testCases/types\\.ts$", "/test/eval/testCases/test-utils\\.ts$", "/test/eval/testCases/index\\.ts$", "/test/eval/export-tests\\.ts$", "/test/eval/tests\\.json$" ], // Indicates which provider should be used to instrument code for coverage coverageProvider: "v8", // A list of reporter names that Jest uses when writing coverage reports // coverageReporters: [ // "json", // "text", // "lcov", // "clover" // ], // An object that configures minimum threshold enforcement for coverage results // coverageThreshold: undefined, // A path to a custom dependency extractor // dependencyExtractor: undefined, // Make calling deprecated APIs throw helpful error messages // errorOnDeprecated: false, // The default configuration for fake timers // fakeTimers: { // "enableGlobally": false // }, // Force coverage collection from ignored files using an array of glob patterns // forceCoverageMatch: [], // A path to a module which exports an async function that is triggered once before all test suites // globalSetup: undefined, // A path to a module which exports an async function that is triggered once after all test suites // globalTeardown: undefined, // A set of global variables that need to be available in all test environments // globals: {}, // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. maxWorkers: '50%', // An array of directory names to be searched recursively up from the requiring module's location // moduleDirectories: [ // "node_modules" // ], // An array of file extensions your modules use moduleFileExtensions: [ "js", "mjs", "cjs", "jsx", "ts", "tsx", "json", "node" ], // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module moduleNameMapper: { '^(\\.{1,2}/.*)\\.js$': '$1', // fixes path issues }, // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader // modulePathIgnorePatterns: [], // Activates notifications for test results // notify: false, // An enum that specifies notification mode. Requires { notify: true } // notifyMode: "failure-change", // A preset that is used as a base for Jest's configuration preset: 'ts-jest/presets/default-esm', // Run tests from one or more projects // projects: undefined, // Use this configuration option to add custom reporters to Jest // reporters: undefined, // Automatically reset mock state before every test // resetMocks: false, // Reset the module registry before running each individual test // resetModules: false, // A path to a custom resolver // resolver: undefined, // Automatically restore mock state and implementation before every test // restoreMocks: false, // The root directory that Jest should scan for tests and modules within // rootDir: undefined, // A list of paths to directories that Jest should use to search for files in // roots: [ // "" // ], // Allows you to use a custom runner instead of Jest's default test runner // runner: "jest-runner", // The paths to modules that run some code to configure or set up the testing environment before each test // setupFiles: [], // A list of paths to modules that run some code to configure or set up the testing framework before each test // setupFilesAfterEnv: [], // The number of seconds after which a test is considered as slow and reported as such in the results. // slowTestThreshold: 5, // A list of paths to snapshot serializer modules Jest should use for snapshot testing // snapshotSerializers: [], // The test environment that will be used for testing testEnvironment: 'node', // Options that will be passed to the testEnvironment // testEnvironmentOptions: {}, // Adds a location field to test results // testLocationInResults: false, // The glob patterns Jest uses to detect test files // testMatch: [ // "**/__tests__/**/*.[jt]s?(x)", // "**/?(*.)+(spec|test).[tj]s?(x)" // ], // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped // testPathIgnorePatterns: [ // "\\\\node_modules\\\\" // ], // The regexp pattern or array of patterns that Jest uses to detect test files // testRegex: [], // This option allows the use of a custom results processor // testResultsProcessor: undefined, // This option allows use of a custom test runner // testRunner: "jest-circus/runner", // A map from regular expressions to paths to transformers transform: { '^.+\.tsx?$': ['ts-jest', { useESM: true, tsconfig: 'tsconfig.jest.json', }], }, // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation // transformIgnorePatterns: [ // "\\\\node_modules\\\\", // "\\.pnp\\.[^\\\\]+$" // ], // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them // unmockedModulePathPatterns: undefined, // Indicates whether each individual test should be reported during the run // verbose: undefined, // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode // watchPathIgnorePatterns: [], // Whether to use watchman for file crawling // watchman: true, }; ================================================ FILE: package.json ================================================ { "name": "@nyariv/sandboxjs", "version": "0.9.6", "description": "Javascript sandboxing library.", "main": "dist/cjs/Sandbox.js", "module": "./dist/esm/Sandbox.js", "browser": "./dist/umd/Sandbox.min.js", "types": "./dist/esm/Sandbox.d.ts", "exports": { ".": { "types": "./dist/esm/Sandbox.d.ts", "import": "./dist/esm/Sandbox.js", "require": "./dist/cjs/Sandbox.js", "default": "./dist/esm/Sandbox.js" }, "./SandboxExec": { "types": "./dist/esm/SandboxExec.d.ts", "import": "./dist/esm/SandboxExec.js", "require": "./dist/cjs/SandboxExec.js", "default": "./dist/esm/SandboxExec.js" }, "./package.json": "./package.json" }, "scripts": { "test": "NODE_OPTIONS='--no-warnings=ExperimentalWarning' jest", "test:perf": "NODE_OPTIONS='--no-warnings=ExperimentalWarning' node --expose-gc test/performance.mjs", "build": "node scripts/build.mjs", "lint": "prettier --check \"**/*.+(ts|json)\" && eslint --ext .ts .", "lint:fix": "prettier --write \"**/*.+(ts|json)\" && eslint --ext .ts --fix .", "patch": "npm version patch", "prepare": "husky" }, "repository": { "type": "git", "url": "git+https://github.com/nyariv/SandboxJS.git" }, "author": "", "license": "MIT", "bugs": { "url": "https://github.com/nyariv/SandboxJS/issues" }, "homepage": "https://github.com/nyariv/SandboxJS#readme", "devDependencies": { "@types/jest": "^30.0.0", "@typescript-eslint/eslint-plugin": "^8.53.0", "@typescript-eslint/parser": "^8.53.0", "chalk": "^5.6.2", "cli-table3": "^0.6.5", "eslint": "^9.39.2", "eslint-config-prettier": "^10.1.8", "husky": "^9.1.7", "jest": "^30.2.0", "lint-staged": "^16.2.7", "node-fetch": "^3.3.2", "prettier": "^3.8.0", "terser": "^5.46.1", "tinybench": "^6.0.0", "ts-jest": "^29.4.6", "tslib": "^2.8.1", "typescript": "^5.9.3", "vite": "^8.0.8", "vite-plugin-dts": "^4.5.4" }, "lint-staged": { "*.ts": [ "prettier --write", "eslint --fix" ], "*.json": [ "prettier --write" ] } } ================================================ FILE: scripts/build.mjs ================================================ import { resolve, dirname } from 'node:path' import { fileURLToPath } from 'node:url' import { rmSync, writeFileSync } from 'node:fs' import { execSync } from 'node:child_process' import { build } from 'vite' import dts from 'vite-plugin-dts' const __dirname = dirname(fileURLToPath(import.meta.url)) const root = resolve(__dirname, '..') try { execSync('npx tsc --noEmit', { cwd: root, stdio: 'inherit' }) } catch { process.exit(1) } rmSync(resolve(root, 'dist'), { recursive: true, force: true }) const entries = { Sandbox: resolve(root, 'src/Sandbox.ts'), SandboxExec: resolve(root, 'src/SandboxExec.ts'), } const minifyOptions = { minify: 'terser', terserOptions: { keep_fnames: /^Sandbox(Symbol|(Async)?(Generator)?(Function|Global)?)$/, }, } function writeModuleTypeManifest(outDir, type) { writeFileSync(resolve(root, outDir, 'package.json'), `${JSON.stringify({ type }, null, 2)}\n`) } // 1. CJS build → dist/node/ await build({ root, build: { outDir: 'dist/cjs', minify: false, lib: { entry: entries, formats: ['cjs'], }, rollupOptions: { output: { preserveModules: true, entryFileNames: '[name].js', exports: 'named', }, }, }, plugins: [ dts({ outDir: 'dist/cjs', include: ['src'], tsconfigPath: resolve(root, 'tsconfig.json'), }), ], }) writeModuleTypeManifest('dist/cjs', 'commonjs') // 2. UMD build → dist/Sandbox.umd.js await build({ root, build: { outDir: 'dist/umd', emptyOutDir: false, sourcemap: true, lib: { entry: resolve(root, 'src/Sandbox.ts'), name: 'Sandbox', formats: ['umd'], fileName: () => 'Sandbox.min.js', }, rollupOptions: { output: { exports: 'named' }, }, ...minifyOptions, }, }) // 3. UMD build → dist/SandboxExec.umd.js await build({ root, build: { outDir: 'dist/umd', emptyOutDir: false, sourcemap: true, lib: { entry: resolve(root, 'src/SandboxExec.ts'), name: 'SandboxExec', formats: ['umd'], fileName: () => 'SandboxExec.min.js', }, rollupOptions: { output: { exports: 'named' }, }, ...minifyOptions, }, }) // 4. ESM build → dist/esm/ await build({ root, build: { outDir: 'dist/esm', minify: false, sourcemap: true, lib: { entry: entries, formats: ['es'], }, rollupOptions: { output: { preserveModules: true, entryFileNames: '[name].js', exports: 'named', }, }, }, plugins: [ dts({ outDir: 'dist/esm', include: ['src'], tsconfigPath: resolve(root, 'tsconfig.json'), }), ], }) writeModuleTypeManifest('dist/esm', 'module') ================================================ FILE: scripts/export-tests.ts ================================================ import * as fs from 'fs'; import * as path from 'path'; import { fileURLToPath } from 'url'; import { TestCase } from '../test/eval/testCases/types'; import * as allTestModules from '../test/eval/testCases/index'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // Collect all tests from imported modules async function extractTests() { const allTests: TestCase[] = []; // Get all test arrays from the imported modules for (const [moduleName, tests] of Object.entries(allTestModules)) { if (Array.isArray(tests)) { allTests.push(...tests); console.log(`Extracted ${tests.length} tests from ${moduleName}`); } } // Group tests by category, preserving order within each category const testsByCategory: Record = {}; const categoryOrder: string[] = []; allTests.forEach((test) => { const category = test.category; if (!testsByCategory[category]) { testsByCategory[category] = []; categoryOrder.push(category); } testsByCategory[category].push(test); }); // Build final array: Data Types first, Security second, rest in original order const sortedTests: TestCase[] = []; // Add Data Types first if (testsByCategory['Data Types']) { sortedTests.push(...testsByCategory['Data Types']); } // Add Security second if (testsByCategory['Security']) { sortedTests.push(...testsByCategory['Security']); } // Add all other categories in the order they appeared categoryOrder.forEach((category) => { if (category !== 'Data Types' && category !== 'Security') { sortedTests.push(...testsByCategory[category]); } }); // Write to tests.json const outputPath = path.join(__dirname, '../test/eval', 'tests.json'); const jsonContent = JSON.stringify(sortedTests, null, 2); // Ensure consistent LF line endings const normalizedContent = jsonContent.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); fs.writeFileSync(outputPath, normalizedContent + '\n', 'utf8'); console.log(`\nTotal tests: ${sortedTests.length}`); console.log(`Exported to: ${outputPath}`); // Show category distribution const categories: Record = {}; sortedTests.forEach((test) => { categories[test.category] = (categories[test.category] || 0) + 1; }); console.log('\nTests per category:'); Object.entries(categories).forEach(([cat, count]) => { console.log(` ${cat}: ${count}`); }); } extractTests().catch((err) => { console.error('Error:', err); process.exit(1); }); ================================================ FILE: src/Sandbox.ts ================================================ import { AsyncFunction, createExecContext, SandboxCapabilityError, sanitizeScopes } from './utils'; import type { IExecContext, IOptionParams, IScope } from './utils'; import { createEvalContext } from './eval'; import { ExecReturn } from './executor'; import parse from './parser'; import SandboxExec from './SandboxExec'; export { ParseError } from './parser'; export { LocalScope, SandboxExecutionTreeError, SandboxCapabilityError, SandboxAccessError, SandboxExecutionQuotaExceededError, SandboxError, delaySynchronousResult, } from './utils'; export type * from './utils'; export type * from './parser'; export type * from './executor'; export type * from './eval'; export class Sandbox extends SandboxExec { constructor(options?: IOptionParams) { super(options, createEvalContext()); } static audit(code: string, scopes: IScope[] = []): ExecReturn { const globals: Record = {}; for (const i of Object.getOwnPropertyNames(globalThis) as [keyof typeof globalThis]) { globals[i] = globalThis[i]; } const sandbox = new SandboxExec({ globals, audit: true, }); return sandbox.executeTree( createExecContext( sandbox, parse(code, true, false, sandbox.context.options.maxParserRecursionDepth), createEvalContext(), ), scopes, ); } static parse(code: string) { return parse(code, true); } get Function() { const context = createExecContext( this, { tree: [], constants: { strings: [], eager: true, literals: [], maxDepth: this.context.options.maxParserRecursionDepth, regexes: [], }, }, this.evalContext, ); return context.evals.get(Function)!; } get AsyncFunction() { const context = createExecContext( this, { tree: [], constants: { strings: [], eager: true, literals: [], maxDepth: this.context.options.maxParserRecursionDepth, regexes: [], }, }, this.evalContext, ); return context.evals.get(AsyncFunction)!; } get eval() { const context = createExecContext( this, { tree: [], constants: { strings: [], eager: true, literals: [], maxDepth: this.context.options.maxParserRecursionDepth, regexes: [], }, }, this.evalContext, ); return context.evals.get(eval)!; } compile( code: string, optimize = false, ): (...scopes: IScope[]) => { context: IExecContext; run: () => T } { if (this.context.options.nonBlocking) throw new SandboxCapabilityError( 'Non-blocking mode is enabled, use Sandbox.compileAsync() instead.', ); const parsed = parse(code, optimize, false, this.context.options.maxParserRecursionDepth); const context = createExecContext(this, parsed, this.evalContext); const exec = (...scopes: IScope[]) => { sanitizeScopes(scopes, context); return { context, run: () => this.executeTree(context, [...scopes]).result }; }; return exec; } compileAsync( code: string, optimize = false, ): (...scopes: IScope[]) => { context: IExecContext; run: () => Promise } { const parsed = parse(code, optimize, false, this.context.options.maxParserRecursionDepth); const context = createExecContext(this, parsed, this.evalContext); const exec = (...scopes: IScope[]) => { sanitizeScopes(scopes, context); return { context, run: () => this.executeTreeAsync(context, [...scopes]).then((ret) => ret.result), }; }; return exec; } compileExpression( code: string, optimize = false, ): (...scopes: IScope[]) => { context: IExecContext; run: () => T } { const parsed = parse(code, optimize, true, this.context.options.maxParserRecursionDepth); const context = createExecContext(this, parsed, this.evalContext); const exec = (...scopes: IScope[]) => { sanitizeScopes(scopes, context); return { context, run: () => this.executeTree(context, [...scopes]).result }; }; return exec; } compileExpressionAsync( code: string, optimize = false, ): (...scopes: IScope[]) => { context: IExecContext; run: () => Promise } { const parsed = parse(code, optimize, true, this.context.options.maxParserRecursionDepth); const context = createExecContext(this, parsed, this.evalContext); const exec = (...scopes: IScope[]) => { return { context, run: () => this.executeTreeAsync(context, [...scopes]).then((ret) => ret.result), }; }; return exec; } } export default Sandbox; ================================================ FILE: src/SandboxExec.ts ================================================ import type { IEvalContext } from './eval'; import { Change, ExecReturn, executeTree, executeTreeAsync } from './executor'; import { createContext, SandboxExecutionQuotaExceededError } from './utils'; import type { IContext, IExecContext, IGlobals, IOptionParams, IOptions, IScope, ISymbolWhitelist, SubscriptionSubject, HaltContext, } from './utils'; function subscribeSet( obj: object, name: string, callback: (modification: Change) => void, context: { setSubscriptions: WeakMap< SubscriptionSubject, Map void>> >; changeSubscriptions: WeakMap void>>; }, ): { unsubscribe: () => void } { const names = context.setSubscriptions.get(obj) || new Map void>>(); context.setSubscriptions.set(obj, names); const callbacks = names.get(name) || new Set(); names.set(name, callbacks); callbacks.add(callback); let changeCbs: Set<(modification: Change) => void>; const val = (obj as any)[name] as unknown; if (val instanceof Object) { changeCbs = context.changeSubscriptions.get(val) || new Set(); changeCbs.add(callback); context.changeSubscriptions.set(val, changeCbs); } return { unsubscribe: () => { callbacks.delete(callback); changeCbs?.delete(callback); }, }; } export class SandboxExec { public readonly context: IContext; public readonly setSubscriptions: WeakMap< SubscriptionSubject, Map void>> > = new WeakMap(); public readonly changeSubscriptions: WeakMap< SubscriptionSubject, Set<(modification: Change) => void> > = new WeakMap(); public readonly sandboxFunctions: WeakMap = new WeakMap(); private haltSubscriptions: Set<(context: HaltContext) => void> = new Set(); private resumeSubscriptions: Set<() => void> = new Set(); public halted = false; timeoutHandleCounter = 0; public readonly setTimeoutHandles = new Map< number, { handle: number; haltsub: { unsubscribe: () => void }; contsub: { unsubscribe: () => void }; } >(); public readonly setIntervalHandles = new Map< number, { handle: number; haltsub: { unsubscribe: () => void }; contsub: { unsubscribe: () => void }; } >(); constructor( options?: IOptionParams, public evalContext?: IEvalContext, ) { const opt: IOptions = Object.assign( { audit: false, forbidFunctionCalls: false, forbidFunctionCreation: false, globals: SandboxExec.SAFE_GLOBALS, symbolWhitelist: SandboxExec.SAFE_SYMBOLS, prototypeWhitelist: SandboxExec.SAFE_PROTOTYPES, maxParserRecursionDepth: 256, nonBlocking: false, functionReplacements: new Map< Function, (ctx: IExecContext, builtInReplacement?: Function) => Function >(), }, options || {}, ); this.context = createContext(this, opt); } static get SAFE_GLOBALS(): IGlobals { return { globalThis, Function, eval, console: { debug: console.debug, error: console.error, info: console.info, log: console.log, table: console.table, warn: console.warn, }, isFinite, isNaN, parseFloat, parseInt, decodeURI, decodeURIComponent, encodeURI, encodeURIComponent, escape, unescape, Boolean, Number, BigInt, String, Object, Array, Symbol, Error, EvalError, RangeError, ReferenceError, SyntaxError, TypeError, URIError, Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array, Map, Set, WeakMap, WeakSet, Promise, Intl, JSON, Math, Date, RegExp, }; } static get SAFE_SYMBOLS(): ISymbolWhitelist { const safeSymbols: ISymbolWhitelist = {}; for (const key of [ 'asyncIterator', 'iterator', 'match', 'matchAll', 'replace', 'search', 'split', ]) { const value = (Symbol as unknown as Record)[key]; if (typeof value === 'symbol') { safeSymbols[key] = value; } } return safeSymbols; } static get SAFE_PROTOTYPES(): Map> { const protos = [ Function, Boolean, Number, BigInt, String, Date, Error, Array, Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array, Map, Set, WeakMap, WeakSet, Promise, Symbol, Date, RegExp, ]; const map = new Map>(); protos.forEach((proto) => { map.set(proto, new Set()); }); map.set( Object, new Set([ 'constructor', 'name', 'entries', 'fromEntries', 'getOwnPropertyNames', 'is', 'keys', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable', 'toLocaleString', 'toString', 'valueOf', 'values', ]), ); return map; } subscribeGet( callback: (obj: SubscriptionSubject, name: string) => void, context: IExecContext, ): { unsubscribe: () => void } { context.getSubscriptions.add(callback); return { unsubscribe: () => context.getSubscriptions.delete(callback) }; } subscribeSet( obj: object, name: string, callback: (modification: Change) => void, context: SandboxExec | IExecContext, ): { unsubscribe: () => void } { return subscribeSet(obj, name, callback, context); } subscribeSetGlobal( obj: SubscriptionSubject, name: string, callback: (modification: Change) => void, ): { unsubscribe: () => void } { return subscribeSet(obj, name, callback, this); } subscribeHalt(cb: (context: HaltContext) => void) { this.haltSubscriptions.add(cb); return { unsubscribe: () => { this.haltSubscriptions.delete(cb); }, }; } subscribeResume(cb: () => void) { this.resumeSubscriptions.add(cb); return { unsubscribe: () => { this.resumeSubscriptions.delete(cb); }, }; } haltExecution(haltContext: HaltContext = { type: 'manual' }) { if (this.halted) return; this.halted = true; for (const cb of this.haltSubscriptions) { cb(haltContext); } } resumeExecution() { if (!this.halted) return; if ( this.context.ticks.tickLimit !== undefined && this.context.ticks.ticks >= this.context.ticks.tickLimit ) { throw new SandboxExecutionQuotaExceededError('Cannot resume execution: tick limit exceeded'); } this.halted = false; for (const cb of this.resumeSubscriptions) { cb(); } } getContext(fn: (...args: any[]) => any) { return this.sandboxFunctions.get(fn); } executeTree(context: IExecContext, scopes: IScope[] = []): ExecReturn { return executeTree(context.ctx.ticks, context, context.tree, scopes, undefined, false); } executeTreeAsync(context: IExecContext, scopes: IScope[] = []): Promise> { return executeTreeAsync(context.ctx.ticks, context, context.tree, scopes, undefined, false); } } export default SandboxExec; ================================================ FILE: src/eval/index.ts ================================================ import { createAsyncGeneratorFunction, createFunction, createFunctionAsync, createGeneratorFunction, } from '../executor'; import parse, { lispifyFunction } from '../parser'; import type { Lisp } from '../parser'; import { getSandboxSymbolCtor, LispType } from '../utils'; import type { IExecContext } from '../utils'; export interface IEvalContext { sandboxFunction: typeof sandboxFunction; sandboxAsyncFunction: typeof sandboxAsyncFunction; sandboxGeneratorFunction: typeof sandboxGeneratorFunction; sandboxAsyncGeneratorFunction: typeof sandboxAsyncGeneratorFunction; sandboxedSymbol: typeof sandboxedSymbol; sandboxedEval: (func: SandboxFunction, context: IExecContext) => SandboxEval; sandboxedSetTimeout: typeof sandboxedSetTimeout; sandboxedSetInterval: typeof sandboxedSetInterval; sandboxedClearTimeout: typeof sandboxedClearTimeout; sandboxedClearInterval: typeof sandboxedClearInterval; lispifyFunction: typeof lispifyFunction; } export type SandboxFunction = (code: string, ...args: string[]) => () => unknown; export type SandboxEval = (code: string) => unknown; export type SandboxSetTimeout = ( handler: TimerHandler, timeout?: number, ...args: unknown[] ) => any; export type SandboxSetInterval = ( handler: TimerHandler, timeout?: number, ...args: unknown[] ) => any; export type SandboxClearTimeout = (handle: number) => void; export type SandboxClearInterval = (handle: number) => void; export function createEvalContext(): IEvalContext { return { sandboxFunction, sandboxAsyncFunction, sandboxGeneratorFunction, sandboxAsyncGeneratorFunction, sandboxedSymbol, sandboxedEval, sandboxedSetTimeout, sandboxedSetInterval, sandboxedClearTimeout, sandboxedClearInterval, lispifyFunction, }; } export function sandboxedSymbol(context: IExecContext) { return getSandboxSymbolCtor(context.ctx.sandboxSymbols); } function SB() {} export function sandboxFunction(context: IExecContext): SandboxFunction { SandboxFunction.prototype = SB.prototype; return SandboxFunction; function SandboxFunction(...params: string[]) { const code = params.pop() || ''; const parsed = parse(code, false, false, context.ctx.options.maxParserRecursionDepth); return createFunction( params, parsed.tree, context.ctx.ticks, { ...context, constants: parsed.constants, tree: parsed.tree, }, undefined, 'anonymous', ); } } export type SandboxAsyncFunction = (code: string, ...args: string[]) => () => Promise; function SAF() {} export function sandboxAsyncFunction(context: IExecContext): SandboxAsyncFunction { SandboxAsyncFunction.prototype = SAF.prototype; return SandboxAsyncFunction; function SandboxAsyncFunction(...params: string[]) { const code = params.pop() || ''; const parsed = parse(code, false, false, context.ctx.options.maxParserRecursionDepth); return createFunctionAsync( params, parsed.tree, context.ctx.ticks, { ...context, constants: parsed.constants, tree: parsed.tree, }, undefined, 'anonymous', ); } } export type SandboxGeneratorFunction = ( code: string, ...args: string[] ) => () => Iterator & Iterable; function SGF() {} export function sandboxGeneratorFunction(context: IExecContext): SandboxGeneratorFunction { SandboxGeneratorFunction.prototype = SGF.prototype; return SandboxGeneratorFunction; function SandboxGeneratorFunction(...params: string[]) { const code = params.pop() || ''; const parsed = parse(code, false, false, context.ctx.options.maxParserRecursionDepth); return createGeneratorFunction( params, parsed.tree, context.ctx.ticks, { ...context, constants: parsed.constants, tree: parsed.tree, }, undefined, 'anonymous', ); } } export type SandboxAsyncGeneratorFunction = ( code: string, ...args: string[] ) => () => AsyncGenerator; function SAGF() {} export function sandboxAsyncGeneratorFunction( context: IExecContext, ): SandboxAsyncGeneratorFunction { SandboxAsyncGeneratorFunction.prototype = SAGF.prototype; return SandboxAsyncGeneratorFunction; function SandboxAsyncGeneratorFunction(...params: string[]) { const code = params.pop() || ''; const parsed = parse(code, false, false, context.ctx.options.maxParserRecursionDepth); return createAsyncGeneratorFunction( params, parsed.tree, context.ctx.ticks, { ...context, constants: parsed.constants, tree: parsed.tree, }, undefined, 'anonymous', ); } } function SE() {} export function sandboxedEval(func: SandboxFunction, context: IExecContext): SandboxEval { sandboxEval.prototype = SE.prototype; return sandboxEval; function sandboxEval(code: string) { // Parse the code and wrap last statement in return for completion value const parsed = parse(code, false, false, context.ctx.options.maxParserRecursionDepth); const tree = wrapLastStatementInReturn(parsed.tree); // Create and execute function with modified tree return createFunction( [], tree, context.ctx.ticks, { ...context, constants: parsed.constants, tree, }, undefined, 'anonymous', )(); } } function wrapLastStatementInReturn(tree: Lisp[]): Lisp[] { if (tree.length === 0) return tree; const newTree = [...tree]; const lastIndex = newTree.length - 1; const lastStmt = newTree[lastIndex]; // Only wrap if it's not already a return or throw if (Array.isArray(lastStmt) && lastStmt.length >= 1) { const op = lastStmt[0]; // Don't wrap Return (8) or Throw (47) - they already control flow if (op === LispType.Return || op === LispType.Throw) { return newTree; } // List of statement types that should have undefined completion value // These match JavaScript semantics where declarations and control structures // don't produce a completion value const statementTypes = [ LispType.Let, // 3 LispType.Const, // 4 LispType.Var, // 35 LispType.Function, // 38 LispType.If, // 14 LispType.Loop, // 39 LispType.Try, // 40 LispType.Switch, // 41 LispType.InternalBlock, // 43 LispType.Expression, // 44 ]; // If the last statement is a declaration or control structure, // don't wrap it (it will naturally return undefined) if (statementTypes.includes(op)) { return newTree; } // For all other types (expressions, operators, etc.), // wrap in return to capture the completion value newTree[lastIndex] = [LispType.Return, LispType.None, lastStmt]; } return newTree; } function sST() {} export function sandboxedSetTimeout( func: SandboxFunction, context: IExecContext, ): SandboxSetTimeout { sandboxSetTimeout.prototype = sST.prototype; return sandboxSetTimeout; function sandboxSetTimeout(handler: TimerHandler, timeout?: number, ...args: unknown[]) { const sandbox = context.ctx.sandbox; const exec = (...a: any[]) => { const h = typeof handler === 'string' ? func(handler) : handler; haltsub.unsubscribe(); contsub.unsubscribe(); sandbox.setTimeoutHandles.delete(sandBoxhandle); return h(...a); }; const sandBoxhandle = ++sandbox.timeoutHandleCounter; let start = Date.now(); let handle: number = setTimeout(exec, timeout, ...args); let elapsed = 0; const haltsub = sandbox.subscribeHalt(() => { elapsed = Date.now() - start + elapsed; clearTimeout(handle); }); const contsub = sandbox.subscribeResume(() => { start = Date.now(); const remaining = Math.floor((timeout || 0) - elapsed); handle = setTimeout(exec, remaining, ...args); sandbox.setTimeoutHandles.set(sandBoxhandle, { handle, haltsub, contsub, }); }); sandbox.setTimeoutHandles.set(sandBoxhandle, { handle, haltsub, contsub, }); return sandBoxhandle; } } function sCT() {} export function sandboxedClearTimeout(context: IExecContext): SandboxClearTimeout { sandboxClearTimeout.prototype = sCT.prototype; return sandboxClearTimeout; function sandboxClearTimeout(handle: number) { const sandbox = context.ctx.sandbox; const timeoutHandle = sandbox.setTimeoutHandles.get(handle); if (timeoutHandle) { clearTimeout(timeoutHandle.handle); timeoutHandle.haltsub.unsubscribe(); timeoutHandle.contsub.unsubscribe(); sandbox.setTimeoutHandles.delete(handle); } } } function sCI() {} export function sandboxedClearInterval(context: IExecContext): SandboxClearInterval { sandboxClearInterval.prototype = sCI.prototype; return sandboxClearInterval; function sandboxClearInterval(handle: number) { const sandbox = context.ctx.sandbox; const intervalHandle = sandbox.setIntervalHandles.get(handle); if (intervalHandle) { clearInterval(intervalHandle.handle); clearTimeout(intervalHandle.handle); intervalHandle.haltsub.unsubscribe(); intervalHandle.contsub.unsubscribe(); sandbox.setIntervalHandles.delete(handle); } } } function sSI() {} export function sandboxedSetInterval( func: SandboxFunction, context: IExecContext, ): SandboxSetInterval { sandboxSetInterval.prototype = sSI.prototype; return sandboxSetInterval; function sandboxSetInterval( handler: TimerHandler, timeout: number | undefined, ...args: unknown[] ) { const sandbox = context.ctx.sandbox; const h = typeof handler === 'string' ? func(handler) : handler; const exec = (...a: any[]) => { start = Date.now(); elapsed = 0; return h(...a); }; const sandBoxhandle = ++sandbox.timeoutHandleCounter; let start = Date.now(); let handle: number = setInterval(exec, timeout, ...args); let elapsed = 0; const haltsub = sandbox.subscribeHalt(() => { elapsed = Date.now() - start + elapsed; clearInterval(handle); clearTimeout(handle); }); const contsub = sandbox.subscribeResume(() => { start = Date.now(); handle = setTimeout( () => { start = Date.now(); elapsed = 0; handle = setInterval(exec, timeout, ...args); handlObj.handle = handle; exec(...args); }, Math.floor((timeout || 0) - elapsed), ...args, ); handlObj.handle = handle; }); const handlObj = { handle, haltsub, contsub, }; sandbox.setIntervalHandles.set(sandBoxhandle, handlObj); return sandBoxhandle; } } ================================================ FILE: src/executor/executorUtils.ts ================================================ import type { LispItem, Lisp, StatementLabel } from '../parser'; import { hasOwnProperty, isLisp, LispType, LocalScope, Prop, SandboxExecutionQuotaExceededError, SandboxError, SandboxExecutionTreeError, Scope, GeneratorFunction, AsyncGeneratorFunction, SandboxCapabilityError, SandboxAccessError, NON_BLOCKING_THRESHOLD, sanitizeProp, Unknown, } from '../utils'; import { IAuditReport, IExecContext, IScope, optional, Ticks } from '../utils'; export type Done = (err?: any, res?: T | typeof optional) => void; export type ControlFlowAction = 'break' | 'continue'; export interface ControlFlowSignal { type: ControlFlowAction; label?: string; } interface ControlFlowTarget { label?: string; acceptsBreak: boolean; acceptsContinue: boolean; acceptsUnlabeledBreak: boolean; acceptsUnlabeledContinue: boolean; } export type ControlFlowTargets = readonly ControlFlowTarget[] | undefined; export class ExecReturn { constructor( public auditReport: IAuditReport | undefined, public result: T, public returned: boolean, public controlFlow?: ControlFlowSignal, ) {} get breakLoop() { return this.controlFlow?.type === 'break'; } get continueLoop() { return this.controlFlow?.type === 'continue'; } } export interface IChange { type: string; } export interface ICreate extends IChange { type: 'create'; prop: number | string; } export interface IReplace extends IChange { type: 'replace'; } export interface IDelete extends IChange { type: 'delete'; prop: number | string; } export interface IReverse extends IChange { type: 'reverse'; } export interface ISort extends IChange { type: 'sort'; } export interface IPush extends IChange { type: 'push'; added: unknown[]; } export interface IPop extends IChange { type: 'pop'; removed: unknown[]; } export interface IShift extends IChange { type: 'shift'; removed: unknown[]; } export interface IUnShift extends IChange { type: 'unshift'; added: unknown[]; } export interface ISplice extends IChange { type: 'splice'; startIndex: number; deleteCount: number; added: unknown[]; removed: unknown[]; } export interface ICopyWithin extends IChange { type: 'copyWithin'; startIndex: number; endIndex: number; added: unknown[]; removed: unknown[]; } export type Change = | ICreate | IReplace | IDelete | IReverse | ISort | IPush | IPop | IUnShift | IShift | ISplice | ICopyWithin; const emptyControlFlowTargets: readonly ControlFlowTarget[] = []; export function normalizeStatementLabel(label: StatementLabel | undefined) { return label === undefined || label === LispType.None ? undefined : label; } export function normalizeStatementLabels(label: LispItem | StatementLabel | undefined) { if (label === undefined || label === LispType.None) return [] as string[]; if (Array.isArray(label) && !isLisp(label)) { return label.filter((item): item is string => typeof item === 'string'); } return [label as string]; } export function createLoopTarget(label?: string, acceptsUnlabeled = true): ControlFlowTarget { return { label, acceptsBreak: true, acceptsContinue: true, acceptsUnlabeledBreak: acceptsUnlabeled, acceptsUnlabeledContinue: acceptsUnlabeled, }; } export function createSwitchTarget(label?: string): ControlFlowTarget { return { label, acceptsBreak: true, acceptsContinue: false, acceptsUnlabeledBreak: true, acceptsUnlabeledContinue: false, }; } export function createLabeledStatementTarget(label?: string): ControlFlowTarget | undefined { if (!label) return undefined; return { label, acceptsBreak: true, acceptsContinue: false, acceptsUnlabeledBreak: false, acceptsUnlabeledContinue: false, }; } export function addControlFlowTarget( controlFlowTargets: ControlFlowTargets, target?: ControlFlowTarget, ): ControlFlowTargets { if (!target) return controlFlowTargets; return [...(controlFlowTargets || emptyControlFlowTargets), target]; } export function addControlFlowTargets( controlFlowTargets: ControlFlowTargets, targets: ControlFlowTarget[], ): ControlFlowTargets { return targets.reduce( (currentTargets, target) => addControlFlowTarget(currentTargets, target), controlFlowTargets, ); } export function matchesControlFlowTarget(signal: ControlFlowSignal, target: ControlFlowTarget) { if (signal.type === 'continue') { if (!target.acceptsContinue) return false; return signal.label ? target.label === signal.label : target.acceptsUnlabeledContinue; } if (!target.acceptsBreak) return false; return signal.label ? target.label === signal.label : target.acceptsUnlabeledBreak; } export function findControlFlowTarget( controlFlowTargets: ControlFlowTargets, type: ControlFlowAction, label?: string, ) { if (!controlFlowTargets) return undefined; for (let i = controlFlowTargets.length - 1; i >= 0; i--) { const target = controlFlowTargets[i]; if (label) { if (target.label !== label) continue; if (type === 'continue' ? target.acceptsContinue : target.acceptsBreak) { return target; } return null; } if (type === 'continue' ? target.acceptsUnlabeledContinue : target.acceptsUnlabeledBreak) { return target; } } return undefined; } function generateArgs(argNames: string[], args: unknown[]) { const vars: Record = {}; argNames.forEach((arg, i) => { if (arg.startsWith('...')) { vars[arg.substring(3)] = args.slice(i); } else { vars[arg] = args[i]; } }); return vars; } export function createFunction( argNames: string[], parsed: Lisp[], ticks: Ticks, context: IExecContext, scope?: Scope, name?: string, internal = false, ) { if (context.ctx.options.forbidFunctionCreation) { throw new SandboxCapabilityError('Function creation is forbidden'); } let func; if (name === undefined) { func = (...args: unknown[]) => { const vars = generateArgs(argNames, args); const res = executeTree( ticks, context, parsed, scope === undefined ? [] : [new Scope(scope, vars)], undefined, internal, ); return res.result; }; } else { func = function sandboxedObject(this: Unknown, ...args: unknown[]) { const vars = generateArgs(argNames, args); const res = executeTree( ticks, context, parsed, scope === undefined ? [] : [new Scope(scope, vars, this)], undefined, internal, ); return res.result; }; } context.registerSandboxFunction(func); context.ctx.sandboxedFunctions.add(func); return func; } export function createFunctionAsync( argNames: string[], parsed: Lisp[], ticks: Ticks, context: IExecContext, scope?: Scope, name?: string, internal = false, ) { if (context.ctx.options.forbidFunctionCreation) { throw new SandboxCapabilityError('Function creation is forbidden'); } if (!context.ctx.prototypeWhitelist?.has(Promise.prototype)) { throw new SandboxCapabilityError('Async/await not permitted'); } let func; if (name === undefined) { func = async (...args: unknown[]) => { const vars = generateArgs(argNames, args); const res = await executeTreeAsync( ticks, context, parsed, scope === undefined ? [] : [new Scope(scope, vars)], undefined, internal, ); return res.result; }; } else { func = async function sandboxedObject(this: Unknown, ...args: unknown[]) { const vars = generateArgs(argNames, args); const res = await executeTreeAsync( ticks, context, parsed, scope === undefined ? [] : [new Scope(scope, vars, this)], undefined, internal, ); return res.result; }; } context.registerSandboxFunction(func); context.ctx.sandboxedFunctions.add(func); return func; } // Sentinel class used to communicate yield values from the executor back to the generator. export class YieldValue { constructor( public value: unknown, public delegate: boolean, ) {} } // Unique sentinel thrown by captureYieldFn in executeGenBody's default case when a new // synchronous yield is encountered. Propagates through the call stack back to the restart loop. const syncYieldPauseSentinel = Symbol('syncYieldPause'); function asIterableIterator(value: unknown): Iterator & Iterable { const iterator = (value as { [Symbol.iterator]?: () => Iterator })?.[Symbol.iterator]?.() ?? (value as Iterator); if (!iterator || typeof iterator.next !== 'function') { throw new TypeError('yield* target is not iterable'); } if (typeof (iterator as Iterator & Iterable)[Symbol.iterator] === 'function') { return iterator as Iterator & Iterable; } return { next: iterator.next.bind(iterator), throw: iterator.throw?.bind(iterator), return: iterator.return?.bind(iterator), [Symbol.iterator]() { return this; }, }; } function asAsyncIterableIterator(value: unknown): AsyncIterator & AsyncIterable { const asyncIterator = (value as { [Symbol.asyncIterator]?: () => AsyncIterator })?.[ Symbol.asyncIterator ]?.(); if (asyncIterator) { return { next: asyncIterator.next.bind(asyncIterator), throw: asyncIterator.throw?.bind(asyncIterator), return: asyncIterator.return?.bind(asyncIterator), [Symbol.asyncIterator]() { return this; }, }; } const iterator = asIterableIterator(value); return { async next(nextValue?: unknown) { return iterator.next(nextValue); }, async throw(err?: unknown) { if (typeof iterator.throw === 'function') { return iterator.throw(err); } throw err; }, async return(valueToReturn?: unknown) { if (typeof iterator.return === 'function') { return iterator.return(valueToReturn); } return { value: valueToReturn, done: true }; }, [Symbol.asyncIterator]() { return this; }, }; } // executeGenBody: a native generator that lazily executes a generator function body. // It handles compound control-flow nodes (statements, if, loop, try, yield) with yield* // recursion, and falls back to the existing execSync for all leaf expressions. // Only used by createGeneratorFunction — nothing else in the executor is changed. function* executeGenBody( ticks: Ticks, tree: Lisp | Lisp[], scope: Scope, context: IExecContext, statementLabels: ControlFlowTargets, internal: boolean, ): Generator | unknown, unknown> { // ── Statement list ────────────────────────────────────────────────────────── if (!isLisp(tree as Lisp) && Array.isArray(tree)) { const stmts = tree as Lisp[]; if (stmts.length === 0 || (stmts[0] as unknown) === LispType.None) { return new ExecReturn(context.ctx.auditReport, undefined, false); } for (const stmt of stmts) { const res = (yield* executeGenBody( ticks, stmt, scope, context, statementLabels, internal, )) as ExecReturn; if (res instanceof ExecReturn && (res.returned || res.controlFlow)) return res; // Mirror _executeWithDoneSync: wrap the result of a return statement if (isLisp(stmt) && (stmt as Lisp)[0] === LispType.Return) { return new ExecReturn(context.ctx.auditReport, res.result, true); } } return new ExecReturn(context.ctx.auditReport, undefined, false); } const [op, a, b] = tree as Lisp; switch (op) { // ── yield expr ──────────────────────────────────────────────────────────── case LispType.Yield: { const valResult = (yield* executeGenBody( ticks, a as Lisp, scope, context, statementLabels, internal, )) as ExecReturn; const sanitized = sanitizeProp(valResult.result, context); const injected: unknown = yield sanitized; // ← real pause point return new ExecReturn(context.ctx.auditReport, injected, false); } // ── yield* expr ─────────────────────────────────────────────────────────── case LispType.YieldDelegate: { const iterResult = (yield* executeGenBody( ticks, a as Lisp, scope, context, statementLabels, internal, )) as ExecReturn; const delegatee = sanitizeProp(iterResult.result, context); const result: unknown = yield* asIterableIterator(delegatee); return new ExecReturn(context.ctx.auditReport, result, false); } // ── if / else ───────────────────────────────────────────────────────────── // LispType.If is NOT in unexecTypes — its `a` is the raw condition Lisp, // `b` is the raw IfCase node that evaluates to an If object with .t/.f. case LispType.If: { const condResult = (yield* executeGenBody( ticks, a as Lisp, scope, context, statementLabels, internal, )) as ExecReturn; const ifCase = syncDone((d) => execSync(ticks, b as Lisp, scope, context, d, statementLabels, internal, undefined), ).result as If; const branch = sanitizeProp(condResult.result, context) ? ifCase.t : ifCase.f; if (branch) { return (yield* executeGenBody( ticks, branch, scope, context, statementLabels, internal, )) as ExecReturn; } return new ExecReturn(context.ctx.auditReport, undefined, false); } // ── loops (while / for / for-of / for-in / do-while) ───────────────────── // Mirror the sync path of the existing Loop handler (executor.ts ~1421-1475) // but replace `executeTree(b, ...)` with `yield*`. case LispType.Loop: { const [ checkFirst, startInternal, getIterator, startStep, step, condition, beforeStep, isForAwait, label, ] = a as Lisp[]; if ((isForAwait as unknown as LispType) === LispType.True) { throw new SyntaxError('for-await-of loops are only allowed inside async functions'); } const loopStatementTargets = [ ...normalizeStatementLabels(label).map((loopLabel) => createLoopTarget(loopLabel, false)), createLoopTarget(), ]; const loopTargets = addControlFlowTargets(statementLabels, loopStatementTargets); const loopScope = new Scope(scope, {}); const internalVars: Record = { $$obj: undefined }; const interalScope = new Scope(loopScope, internalVars); syncDone((d) => execSync(ticks, startStep, loopScope, context, d, undefined, internal, undefined), ); internalVars['$$obj'] = syncDone((d) => execSync(ticks, getIterator, loopScope, context, d, undefined, internal, undefined), ).result; syncDone((d) => execSync(ticks, startInternal, interalScope, context, d, undefined, internal, undefined), ); let loop: unknown = true; if (checkFirst) { loop = syncDone((d) => execSync(ticks, condition, interalScope, context, d, undefined, internal, undefined), ).result; } while (loop) { const iterScope = new Scope(interalScope, {}); syncDone((d) => execSync(ticks, beforeStep, iterScope, context, d, undefined, internal, undefined), ); const res = (yield* executeGenBody( ticks, b as Lisp[], iterScope, context, loopTargets, internal, )) as ExecReturn; if (res.returned) return res; if (res.controlFlow) { if (!loopStatementTargets.some((t) => matchesControlFlowTarget(res.controlFlow!, t))) { return res; // break/continue targeting an outer labeled loop } if (res.breakLoop) break; // continueLoop: fall through to step + condition check } syncDone((d) => execSync(ticks, step, interalScope, context, d, undefined, internal, undefined), ); loop = syncDone((d) => execSync(ticks, condition, interalScope, context, d, undefined, internal, undefined), ).result; } return new ExecReturn(context.ctx.auditReport, undefined, false); } // ── try / catch / finally ───────────────────────────────────────────────── // Using real native try/catch/finally gives us correct gen.throw() and // gen.return() semantics for free (the native generator machinery handles them). case LispType.Try: { const [exception, catchBody, finallyBody] = b as [string, Lisp[], Lisp[]]; let result!: ExecReturn; let finalOverride: ExecReturn | undefined; try { result = (yield* executeGenBody( ticks, a as Lisp[], scope, context, statementLabels, internal, )) as ExecReturn; } catch (e) { if (exception && catchBody?.length > 0) { const catchScope = new Scope(scope, { [exception]: e }); result = (yield* executeGenBody( ticks, catchBody, catchScope, context, statementLabels, internal, )) as ExecReturn; } else { throw e; } } finally { if (finallyBody?.length > 0) { const fr = (yield* executeGenBody( ticks, finallyBody, scope, context, statementLabels, internal, )) as ExecReturn; // finally control flow (return/break/continue) overrides everything if (fr.returned || fr.controlFlow) { finalOverride = fr; } } } if (finalOverride) return finalOverride; return result; } // ── labeled statement ───────────────────────────────────────────────────── case LispType.Labeled: { const target = createLabeledStatementTarget(normalizeStatementLabel(a as StatementLabel)); const newTargets = addControlFlowTargets(statementLabels, target ? [target] : []); const res = (yield* executeGenBody( ticks, b as Lisp, scope, context, newTargets, internal, )) as ExecReturn; if (res.controlFlow && target && matchesControlFlowTarget(res.controlFlow, target)) { return new ExecReturn(context.ctx.auditReport, res.result, false); } return res; } // ── everything else ─────────────────────────────────────────────────────── // Arithmetic, property access, function calls, assignments, etc. are // delegated to execSync. However, a yield expression may be nested inside // a non-unexecType node (e.g. `const x = yield 1`). In that case the // existing sync yield handler throws syncYieldPauseSentinel. We restart // execSync from scratch on each such pause, skipping already-completed // yields by immediately calling their continuation with the stored result. default: { let completedYields = 0; const yieldResults: unknown[] = []; while (true) { let currentYieldIdx = 0; let capturedValue: unknown = undefined; let capturedDelegate = false; let yielded = false; const captureYieldFn = (yv: YieldValue, continueDone?: Done) => { if (currentYieldIdx < completedYields) { // This yield already happened in a prior iteration; fast-forward. continueDone!(undefined, yieldResults[currentYieldIdx]); currentYieldIdx++; return; } // New yield: capture the value and pause. capturedValue = yv.value; capturedDelegate = yv.delegate; yielded = true; currentYieldIdx++; throw syncYieldPauseSentinel; }; try { const result = syncDone((d) => execSync( ticks, tree as Lisp, scope, context, d, statementLabels, internal, captureYieldFn, ), ).result; if (result instanceof ExecReturn) return result; return new ExecReturn(context.ctx.auditReport, result, false); } catch (e) { if (!yielded || e !== syncYieldPauseSentinel) throw e; const resumedValue: unknown = capturedDelegate ? yield* asIterableIterator(capturedValue) : yield capturedValue; yieldResults.push(resumedValue); completedYields++; } } } } } export function createGeneratorFunction( argNames: string[], parsed: Lisp[], ticks: Ticks, context: IExecContext, scope?: Scope, name?: string, internal = false, ) { if (context.ctx.options.forbidFunctionCreation) { throw new SandboxCapabilityError('Function creation is forbidden'); } const makeGen = (thisArg: Unknown, args: unknown[]) => { const vars = generateArgs(argNames, args); const genScope = scope === undefined ? new Scope(null, vars, thisArg) : new Scope(scope, vars, thisArg); const executionGen = executeGenBody(ticks, parsed, genScope, context, undefined, internal); let isDone = false; function drive(action: () => IteratorResult): IteratorResult { if (isDone) return { value: undefined, done: true }; try { const r = action(); if (r.done) { isDone = true; return { value: r.value instanceof ExecReturn ? r.value.result : r.value, done: true, }; } return { value: r.value, done: false }; } catch (e) { isDone = true; throw e; } } const iterator: Iterator & Iterable = { next(value?: unknown) { return drive(() => executionGen.next(value)); }, return(value?: unknown) { return drive(() => executionGen.return(value)); }, throw(err?: unknown) { return drive(() => executionGen.throw(err)); }, [Symbol.iterator]() { return this; }, }; return iterator; }; const func = function sandboxedObject(this: Unknown, ...args: unknown[]) { return makeGen(this, args); }; Object.setPrototypeOf(func, GeneratorFunction.prototype); context.registerSandboxFunction(func); context.ctx.sandboxedFunctions.add(func); return func; } export function createAsyncGeneratorFunction( argNames: string[], parsed: Lisp[], ticks: Ticks, context: IExecContext, scope?: Scope, name?: string, internal = false, ) { if (context.ctx.options.forbidFunctionCreation) { throw new SandboxCapabilityError('Function creation is forbidden'); } if (!context.ctx.prototypeWhitelist?.has(Promise.prototype)) { throw new SandboxCapabilityError('Async/await not permitted'); } const makeGen = (thisArg: Unknown, args: unknown[]) => { const vars = generateArgs(argNames, args); const genScope = scope === undefined ? [new Scope(null, vars, thisArg)] : [new Scope(scope, vars, thisArg)]; return (async function* sandboxedAsyncGenerator(): AsyncGenerator { const yieldQueue: Array<{ yieldValue: YieldValue; continueDone?: Done }> = []; let resolveYield: (() => void) | null = null; const yieldFn = (yv: YieldValue, continueDone?: Done) => { yieldQueue.push({ yieldValue: yv, continueDone }); if (resolveYield) { resolveYield(); resolveYield = null; } }; const bodyPromise = executeTreeAsync( ticks, context, parsed, genScope, undefined, internal, yieldFn, ); let bodyDone = false; let bodyResult: ExecReturn | undefined; let bodyError: unknown; bodyPromise.then( (r) => { bodyDone = true; bodyResult = r; resolveYield?.(); }, (e) => { bodyDone = true; bodyError = e; resolveYield?.(); }, ); while (true) { if (yieldQueue.length === 0 && !bodyDone) { await new Promise((res) => { resolveYield = res; }); } while (yieldQueue.length > 0) { const { yieldValue, continueDone } = yieldQueue.shift()!; try { const resumedValue = yieldValue.delegate ? yield* asAsyncIterableIterator(yieldValue.value) : yield yieldValue.value; continueDone?.(undefined, resumedValue); } catch (err) { continueDone?.(err); } } if (bodyDone) break; } if (bodyError !== undefined) throw bodyError; return bodyResult?.result; })(); }; const func = function sandboxedObject(this: Unknown, ...args: unknown[]) { return makeGen(this, args) as unknown as AsyncGenerator; }; Object.setPrototypeOf(func, AsyncGeneratorFunction.prototype); context.registerSandboxFunction(func); context.ctx.sandboxedFunctions.add(func); return func; } export function assignCheck(obj: Prop, context: IExecContext, op = 'assign') { if (obj.context === undefined) { throw new ReferenceError(`Cannot ${op} value to undefined.`); } if (obj.isConst) { throw new TypeError(`Assignment to constant variable.`); } if (obj.isGlobal) { throw new SandboxAccessError( `Cannot ${op} property '${obj.prop.toString()}' of a global object`, ); } if (obj.context === null) { throw new TypeError('Cannot set properties of null'); } if ( typeof (obj.context as any)[obj.prop] === 'function' && !hasOwnProperty(obj.context, obj.prop) ) { throw new SandboxAccessError( `Override prototype property '${obj.prop.toString()}' not allowed`, ); } if (op === 'delete') { if (hasOwnProperty(obj.context, obj.prop)) { context.changeSubscriptions .get(obj.context) ?.forEach((cb) => cb({ type: 'delete', prop: obj.prop.toString() })); context.changeSubscriptionsGlobal .get(obj.context) ?.forEach((cb) => cb({ type: 'delete', prop: obj.prop.toString() })); } } else if (hasOwnProperty(obj.context, obj.prop)) { context.setSubscriptions .get(obj.context) ?.get(obj.prop.toString()) ?.forEach((cb) => cb({ type: 'replace', }), ); context.setSubscriptionsGlobal .get(obj.context) ?.get(obj.prop.toString()) ?.forEach((cb) => cb({ type: 'replace', }), ); } else { context.changeSubscriptions .get(obj.context) ?.forEach((cb) => cb({ type: 'create', prop: obj.prop.toString() })); context.changeSubscriptionsGlobal .get(obj.context) ?.forEach((cb) => cb({ type: 'create', prop: obj.prop.toString() })); } } export const arrayChange = new Set([ [].push, [].pop, [].shift, [].unshift, [].splice, [].reverse, [].sort, [].copyWithin, ]); export class KeyVal { constructor( public key: PropertyKey | SpreadObject, public val: unknown, ) {} } export class SpreadObject { constructor(public item: { [key: string]: unknown }) {} } export class SpreadArray { constructor(public item: unknown[]) {} } export class ArrayHole {} export class If { constructor( public t: Lisp, public f: Lisp, public label?: string, ) {} } export const literalRegex = /(\$\$)*(\$)?\${(\d+)}/g; export { ops, addOps } from './opsRegistry'; import { ops, Execution } from './opsRegistry'; export const prorptyKeyTypes = ['string', 'number', 'symbol']; export function isPropertyKey(val: unknown): val is PropertyKey { return prorptyKeyTypes.includes(typeof val); } export function hasPossibleProperties(val: unknown): val is {} { return val !== null && val !== undefined; } import './ops/index'; export function execMany( ticks: Ticks, exec: Execution, tree: Lisp[], done: Done, scope: Scope, context: IExecContext, statementLabels: ControlFlowTargets, internal: boolean, generatorYield: ((yv: YieldValue, done?: Done) => void) | undefined, ) { if (exec === execSync) { _execManySync(ticks, tree, done, scope, context, statementLabels, internal, generatorYield); } else { _execManyAsync( ticks, tree, done, scope, context, statementLabels, internal, generatorYield, ).catch(done); } } function _execManySync( ticks: Ticks, tree: Lisp[], done: Done, scope: Scope, context: IExecContext, statementLabels: ControlFlowTargets, internal: boolean, generatorYield: ((yv: YieldValue, done?: Done) => void) | undefined, ) { const ret: any[] = []; for (let i = 0; i < tree.length; i++) { let res = syncDone((d) => execSync(ticks, tree[i], scope, context, d, statementLabels, internal, generatorYield), ).result; if (res instanceof ExecReturn && (res.returned || res.breakLoop || res.continueLoop)) { done(undefined, res); return; } if (isLisp(tree[i]) && tree[i][0] === LispType.Return) { done(undefined, new ExecReturn(context.ctx.auditReport, res, true)); return; } ret.push(res); } done(undefined, ret); } async function _execManyAsync( ticks: Ticks, tree: Lisp[], done: Done, scope: Scope, context: IExecContext, statementLabels: ControlFlowTargets, internal: boolean, generatorYield: ((yv: YieldValue, done?: Done) => void) | undefined, ) { const ret: any[] = []; for (let i = 0; i < tree.length; i++) { let res; try { let ad: AsyncDoneRet; res = (ad = asyncDone((d) => execAsync(ticks, tree[i], scope, context, d, statementLabels, internal, generatorYield), )).isInstant === true ? ad.instant : (await ad.p).result; } catch (e) { done(e); return; } if (res instanceof ExecReturn && (res.returned || res.breakLoop || res.continueLoop)) { done(undefined, res); return; } if (isLisp(tree[i]) && tree[i][0] === LispType.Return) { done(undefined, new ExecReturn(context.ctx.auditReport, res, true)); return; } ret.push(res); } done(undefined, ret); } export interface AsyncDoneRet { isInstant: boolean; instant: any; p: Promise<{ result: any }>; } export function asyncDone(callback: (done: Done) => void): AsyncDoneRet { let isInstant = false; let instant: unknown; const p = new Promise((resolve, reject) => { callback((...args: unknown[]) => { if (args.length === 1) reject(args[0]); else { isInstant = true; instant = args[1]; resolve({ result: args[1] }); } }); }); return { isInstant, instant, p, }; } export function syncDone(callback: (done: Done) => void): { result: any } { let result; let err: { error: unknown } | undefined; callback((...args: unknown[]) => { err = args.length === 1 ? { error: args[0] } : undefined; result = args[1]; }); if (err) throw err.error; return { result }; } export async function execAsync( ticks: Ticks, tree: LispItem, scope: Scope, context: IExecContext, doneOriginal: Done, statementLabels: ControlFlowTargets, internal: boolean, generatorYield: ((yv: YieldValue, done?: Done) => void) | undefined, ): Promise { let done: Done = doneOriginal; const p = new Promise((resolve) => { done = (...args: unknown[]) => { doneOriginal(...args); resolve(); }; }); if ( !_execNoneRecurse( ticks, tree, scope, context, done, true, statementLabels, internal, generatorYield, ) && isLisp(tree) ) { let op = tree[0]; let obj; try { let ad: AsyncDoneRet; obj = (ad = asyncDone((d) => execAsync(ticks, tree[1], scope, context, d, statementLabels, internal, generatorYield), )).isInstant === true ? ad.instant : (await ad.p).result; } catch (e) { done(e); return; } let a = obj; try { a = obj instanceof Prop ? obj.get(context) : obj; } catch (e) { done(e); return; } if (op === LispType.PropOptional || op === LispType.CallOptional) { if (a === undefined || a === null) { done(undefined, optional); return; } op = op === LispType.PropOptional ? LispType.Prop : LispType.Call; } if (a === optional) { if (op === LispType.Prop || op === LispType.Call) { done(undefined, a); return; } else { a = undefined; } } // Short-circuit for nullish coalescing: if a is not null/undefined, return a without evaluating b if (op === LispType.NullishCoalescing && a !== undefined && a !== null) { done(undefined, a); return; } let bobj; try { let ad: AsyncDoneRet; bobj = (ad = asyncDone((d) => execAsync(ticks, tree[2], scope, context, d, statementLabels, internal, generatorYield), )).isInstant === true ? ad.instant : (await ad.p).result; } catch (e) { done(e); return; } let b = bobj; try { b = bobj instanceof Prop ? bobj.get(context) : bobj; } catch (e) { done(e); return; } if (b === optional) { b = undefined; } performOp({ op, exec: execAsync, done, ticks, a, b, obj, context, scope, bobj, statementLabels, internal, generatorYield, tree, }); } await p; } export function execSync( ticks: Ticks, tree: LispItem, scope: Scope, context: IExecContext, done: Done, statementLabels: ControlFlowTargets, internal: boolean, generatorYield: ((yv: YieldValue, done?: Done) => void) | undefined, ) { if ( !_execNoneRecurse( ticks, tree, scope, context, done, false, statementLabels, internal, generatorYield, ) && isLisp(tree) ) { let op = tree[0]; let obj = syncDone((d) => execSync(ticks, tree[1], scope, context, d, statementLabels, internal, generatorYield), ).result; let a = obj instanceof Prop ? obj.get(context) : obj; if (op === LispType.PropOptional || op === LispType.CallOptional) { if (a === undefined || a === null) { done(undefined, optional); return; } op = op === LispType.PropOptional ? LispType.Prop : LispType.Call; } if (a === optional) { if (op === LispType.Prop || op === LispType.Call) { done(undefined, a); return; } else { a = undefined; } } // Short-circuit for nullish coalescing: if a is not null/undefined, return a without evaluating b if (op === LispType.NullishCoalescing && a !== undefined && a !== null) { done(undefined, a); return; } let bobj = syncDone((d) => execSync(ticks, tree[2], scope, context, d, statementLabels, internal, generatorYield), ).result; let b = bobj instanceof Prop ? bobj.get(context) : bobj; if (b === optional) { b = undefined; } performOp({ op, exec: execSync, done, ticks, a, b, obj, context, scope, bobj, statementLabels, internal, generatorYield, tree, }); } } type OpsCallbackParams = { op: LispType; exec: Execution; a: a; b: b; obj: obj; bobj: bobj; ticks: Ticks; tree: LispItem; scope: Scope; context: IExecContext; done: Done; statementLabels: ControlFlowTargets; internal: boolean; generatorYield: ((yv: YieldValue, done?: Done) => void) | undefined; }; export function checkHaltExpectedTicks( params: OpsCallbackParams, expectTicks = 0n, ): boolean { const sandbox = params.context.ctx.sandbox; const { ticks, scope, context } = params; if (sandbox.halted) { const sub = sandbox.subscribeResume(() => { sub.unsubscribe(); performOp(params, false); }); return true; } else if (ticks.tickLimit !== undefined && ticks.tickLimit <= ticks.ticks + expectTicks) { const error = new SandboxExecutionQuotaExceededError('Execution quota exceeded'); if (context.ctx.options.haltOnSandboxError) { const sub = sandbox.subscribeResume(() => { sub.unsubscribe(); performOp(params); }); sandbox.haltExecution({ type: 'error', error, ticks, scope, context, }); } else { params.done(error); } return true; } else if (ticks.nextYield && ticks.ticks > ticks.nextYield) { const sub = sandbox.subscribeResume(() => { sub.unsubscribe(); performOp(params, false); }); ticks.nextYield += NON_BLOCKING_THRESHOLD; sandbox.haltExecution({ type: 'yield' }); setTimeout(() => sandbox.resumeExecution()); return true; } ticks.ticks += expectTicks; return false; } function performOp(params: OpsCallbackParams, count = true) { const { done, op, ticks, context, scope } = params; if (count) { ticks.ticks++; } const sandbox = context.ctx.sandbox; try { if (checkHaltExpectedTicks(params)) { return; } const o = ops.get(op); if (o === undefined) { done(new SandboxExecutionTreeError('Unknown operator: ' + op)); return; } o(params); } catch (err) { if (context.ctx.options.haltOnSandboxError && err instanceof SandboxError) { const sub = sandbox.subscribeResume(() => { sub.unsubscribe(); done(err); }); sandbox.haltExecution({ type: 'error', error: err, ticks, scope, context, }); } else { done(err); } } } const unexecTypes = new Set([ LispType.ArrowFunction, LispType.Function, LispType.InlineFunction, LispType.Loop, LispType.Try, LispType.Switch, LispType.IfCase, LispType.InlineIfCase, LispType.Labeled, LispType.Typeof, ]); function _execNoneRecurse( ticks: Ticks, tree: LispItem, scope: Scope, context: IExecContext, done: Done, isAsync: boolean, statementLabels: ControlFlowTargets, internal: boolean, generatorYield: ((yv: YieldValue, done?: Done) => void) | undefined, ): boolean { const exec = isAsync ? execAsync : execSync; if (tree instanceof Prop) { done(undefined, tree.get(context)); } else if (tree === optional) { done(); } else if (Array.isArray(tree) && !isLisp(tree)) { if (tree[0] === LispType.None) { done(); } else { execMany( ticks, exec, tree as Lisp[], done, scope, context, statementLabels, internal, generatorYield, ); } } else if (!isLisp(tree)) { done(undefined, tree); } else if (tree[0] === LispType.Block) { execMany( ticks, exec, tree[1] as Lisp[], done, new Scope(scope), context, statementLabels, internal, generatorYield, ); } else if (tree[0] === LispType.InternalBlock) { execMany( ticks, exec, tree[1] as Lisp[], done, scope, context, statementLabels, true, generatorYield, ); } else if (tree[0] === LispType.Await) { if (!isAsync) { done(new SyntaxError("Illegal use of 'await', must be inside async function")); } else if (context.ctx.prototypeWhitelist?.has(Promise.prototype)) { execAsync( ticks, tree[1], scope, context, async (...args: unknown[]) => { if (args.length === 1) done(args[0]); else try { done(undefined, (await sanitizeProp(args[1], context)) as any); } catch (err) { done(err); } }, statementLabels, internal, generatorYield, ).catch(done); } else { done(new SandboxCapabilityError('Async/await is not permitted')); } } else if (tree[0] === LispType.Yield || tree[0] === LispType.YieldDelegate) { const yieldFn = generatorYield; if (!yieldFn) { done(new SyntaxError("Illegal use of 'yield', must be inside a generator function")); return true; } const isDelegate = tree[0] === LispType.YieldDelegate; if (isAsync) { execAsync( ticks, tree[1], scope, context, async (...args: unknown[]) => { if (args.length === 1) { done(args[0]); return; } try { const val = await sanitizeProp(args[1], context); yieldFn(new YieldValue(val, isDelegate), done); } catch (err) { done(err); } }, statementLabels, internal, generatorYield, ).catch(done); } else { try { const val = syncDone((d) => execSync(ticks, tree[1], scope, context, d, statementLabels, internal, generatorYield), ).result; const sanitized = sanitizeProp(val, context); // Pass `done` as second arg so the yieldFn can call it with the injected value. // For capture-mode yieldFns (executeGenBody default case), this enables the restart loop. // For plain yieldFns (eager mode), done? is ignored and done() is called below. yieldFn(new YieldValue(sanitized, isDelegate), done); // If yieldFn did not call done (it threw syncYieldPauseSentinel instead), we fall through // to the catch which re-throws the sentinel. Otherwise yieldFn called done itself. } catch (err) { if (err === syncYieldPauseSentinel) throw err; // propagate pause up to restart loop done(err); } } } else if (unexecTypes.has(tree[0])) { performOp({ op: tree[0], exec, done, ticks, a: tree[1], b: tree[2], obj: tree, tree, context, scope, bobj: undefined, statementLabels, internal, generatorYield, }); } else { return false; } return true; } export function executeTree( ticks: Ticks, context: IExecContext, executionTree: Lisp[], scopes: IScope[] = [], statementLabels: ControlFlowTargets, internal: boolean, generatorYield?: ((yv: YieldValue, done?: Done) => void) | undefined, ): ExecReturn { return syncDone((done) => executeTreeWithDone( execSync, done, ticks, context, executionTree, scopes, statementLabels, internal, generatorYield, ), ).result; } export async function executeTreeAsync( ticks: Ticks, context: IExecContext, executionTree: Lisp[], scopes: IScope[] = [], statementLabels: ControlFlowTargets, internal: boolean, generatorYield?: ((yv: YieldValue, done?: Done) => void) | undefined, ): Promise> { let ad: AsyncDoneRet; return (ad = asyncDone((done) => executeTreeWithDone( execAsync, done, ticks, context, executionTree, scopes, statementLabels, internal, generatorYield, ), )).isInstant === true ? ad.instant : (await ad.p).result; } export function executeTreeWithDone( exec: Execution, done: Done, ticks: Ticks, context: IExecContext, executionTree: Lisp[], scopes: IScope[] = [], statementLabels: ControlFlowTargets, internal: boolean, generatorYield?: ((yv: YieldValue, done?: Done) => void) | undefined, ) { if (!executionTree) { done(); return; } if (!(executionTree instanceof Array)) { throw new SyntaxError('Bad execution tree'); } let scope = context.ctx.globalScope; let s; while ((s = scopes.shift())) { if (typeof s !== 'object') continue; if (s instanceof Scope) { scope = s; } else { scope = new Scope(scope, s, s instanceof LocalScope ? undefined : null); } } if (context.ctx.options.audit && !context.ctx.auditReport) { context.ctx.auditReport = { globalsAccess: new Set(), prototypeAccess: {}, }; } if (exec === execSync) { _executeWithDoneSync( done, ticks, context, executionTree, scope, statementLabels, internal, generatorYield, ); } else { _executeWithDoneAsync( done, ticks, context, executionTree, scope, statementLabels, internal, generatorYield, ).catch(done); } } function _executeWithDoneSync( done: Done, ticks: Ticks, context: IExecContext, executionTree: Lisp[], scope: Scope, statementLabels: ControlFlowTargets, internal: boolean, generatorYield: ((yv: YieldValue, done?: Done) => void) | undefined, ) { if (!(executionTree instanceof Array)) throw new SyntaxError('Bad execution tree'); let i = 0; for (i = 0; i < executionTree.length; i++) { let res: unknown; let err: { error: unknown } | undefined; const current = executionTree[i]; try { execSync( ticks, current, scope, context, (...args: unknown[]) => { if (args.length === 1) err = { error: args[0] }; else res = args[1]; }, statementLabels, internal, generatorYield, ); } catch (e) { err = { error: e }; } if (err) { done(err.error); return; } if (res instanceof ExecReturn) { done(undefined, res); return; } if (isLisp(current) && current[0] === LispType.Return) { done(undefined, new ExecReturn(context.ctx.auditReport, res, true)); return; } } done(undefined, new ExecReturn(context.ctx.auditReport, undefined, false)); } async function _executeWithDoneAsync( done: Done, ticks: Ticks, context: IExecContext, executionTree: Lisp[], scope: Scope, statementLabels: ControlFlowTargets, internal: boolean, generatorYield: ((yv: YieldValue, done?: Done) => void) | undefined, ) { if (!(executionTree instanceof Array)) throw new SyntaxError('Bad execution tree'); let i = 0; for (i = 0; i < executionTree.length; i++) { let res: unknown; let err: { error: unknown } | undefined; const current = executionTree[i]; try { await execAsync( ticks, current, scope, context, (...args: unknown[]) => { if (args.length === 1) err = { error: args[0] }; else res = args[1]; }, statementLabels, internal, generatorYield, ); } catch (e) { err = { error: e }; } if (err) { done(err.error); return; } if (res instanceof ExecReturn) { done(undefined, res); return; } if (isLisp(current) && current[0] === LispType.Return) { done(undefined, new ExecReturn(context.ctx.auditReport, res, true)); return; } } done(undefined, new ExecReturn(context.ctx.auditReport, undefined, false)); } ================================================ FILE: src/executor/index.ts ================================================ export * from './executorUtils'; ================================================ FILE: src/executor/ops/assignment.ts ================================================ import { addOps, assignCheck, checkHaltExpectedTicks } from '../executorUtils'; import { LispType, Prop } from '../../utils'; addOps, Prop>(LispType.Assign, (params) => { const { done, b, obj, context, scope, bobj, internal } = params; assignCheck(obj, context); obj.isGlobal = bobj?.isGlobal || false; if (obj.isVariable) { const s = scope.getWhereValScope(obj.prop as string, obj.prop === 'this', internal); if (s === null) { throw new ReferenceError(`Cannot assign to undeclared variable '${obj.prop.toString()}'`); } s.set(obj.prop as string, b, internal); if (obj.isGlobal) { s.globals[obj.prop.toString()] = true; } else { delete s.globals[obj.prop.toString()]; } done(undefined, b); return; } if (obj.prop === 'length' && Array.isArray(obj.context) && typeof b === 'number') { const delta = BigInt(Math.abs(b - obj.context.length)); if (delta > 0n && checkHaltExpectedTicks(params, delta)) return; } done(undefined, (obj.context[obj.prop] = b)); }); addOps>(LispType.AddEquals, (params) => { const { done, b, obj, context } = params; assignCheck(obj, context); const result = (obj.context[obj.prop] as any) + (b as any); if (typeof result === 'string' && checkHaltExpectedTicks(params, BigInt(result.length))) return; done(undefined, (obj.context[obj.prop] = result)); }); addOps>(LispType.SubractEquals, ({ done, b, obj, context }) => { assignCheck(obj, context); done(undefined, (obj.context[obj.prop] -= b)); }); addOps>(LispType.DivideEquals, ({ done, b, obj, context }) => { assignCheck(obj, context); done(undefined, (obj.context[obj.prop] /= b)); }); addOps>(LispType.MultiplyEquals, ({ done, b, obj, context }) => { assignCheck(obj, context); done(undefined, (obj.context[obj.prop] *= b)); }); addOps>(LispType.PowerEquals, ({ done, b, obj, context }) => { assignCheck(obj, context); done(undefined, (obj.context[obj.prop] **= b)); }); addOps>(LispType.ModulusEquals, ({ done, b, obj, context }) => { assignCheck(obj, context); done(undefined, (obj.context[obj.prop] %= b)); }); addOps>(LispType.BitNegateEquals, ({ done, b, obj, context }) => { assignCheck(obj, context); done(undefined, (obj.context[obj.prop] ^= b)); }); addOps>(LispType.BitAndEquals, ({ done, b, obj, context }) => { assignCheck(obj, context); done(undefined, (obj.context[obj.prop] &= b)); }); addOps>(LispType.BitOrEquals, ({ done, b, obj, context }) => { assignCheck(obj, context); done(undefined, (obj.context[obj.prop] |= b)); }); addOps>(LispType.ShiftLeftEquals, ({ done, b, obj, context }) => { assignCheck(obj, context); done(undefined, (obj.context[obj.prop] <<= b)); }); addOps>(LispType.ShiftRightEquals, ({ done, b, obj, context }) => { assignCheck(obj, context); done(undefined, (obj.context[obj.prop] >>= b)); }); addOps>( LispType.UnsignedShiftRightEquals, ({ done, b, obj, context }) => { assignCheck(obj, context); done(undefined, (obj.context[obj.prop] >>>= b)); }, ); addOps>(LispType.AndEquals, ({ done, b, obj, context }) => { assignCheck(obj, context); done(undefined, (obj.context[obj.prop] &&= b)); }); addOps>(LispType.OrEquals, ({ done, b, obj, context }) => { assignCheck(obj, context); done(undefined, (obj.context[obj.prop] ||= b)); }); addOps>( LispType.NullishCoalescingEquals, ({ done, b, obj, context }) => { assignCheck(obj, context); done(undefined, (obj.context[obj.prop] ??= b)); }, ); ================================================ FILE: src/executor/ops/call.ts ================================================ import { addOps, arrayChange, Change, checkHaltExpectedTicks, SpreadArray } from '../executorUtils'; import { checkTicksAndThrow, typedArrayProtos as _typedArrayProtos, } from '../../utils/functionReplacements'; import type { Lisp } from '../../parser'; import { DelayedSynchronousResult, getReplacementReceiver, LispType, SandboxAccessError, SandboxCapabilityError, sanitizeProp, } from '../../utils'; addOps(LispType.Call, (params) => { const { done, a, b, obj, context } = params; if (context.ctx.options.forbidFunctionCalls) throw new SandboxCapabilityError('Function invocations are not allowed'); if (typeof a !== 'function') { throw new TypeError( `${typeof obj?.prop === 'symbol' ? 'Symbol' : obj?.prop} is not a function`, ); } const vals = new Array(b.length); let valsLen = 0; for (let i = 0; i < b.length; i++) { const item = b[i]; if (item instanceof SpreadArray) { const expanded = Array.isArray(item.item) ? item.item : [...(item.item as Iterable)]; if (checkHaltExpectedTicks(params, BigInt(expanded.length))) return; for (let j = 0; j < expanded.length; j++) vals[valsLen++] = sanitizeProp(expanded[j], context); } else { vals[valsLen++] = sanitizeProp(item, context); } } vals.length = valsLen; if (a === String) { const result = String(vals[0]); checkTicksAndThrow(context, BigInt(result.length)); done(undefined, result); return; } if (typeof obj === 'function') { // Direct function call (not a method): obj is the function itself const evl = context.evals.get(obj); const receiver = getReplacementReceiver(obj); let ret = evl ? evl(obj, ...vals) : receiver === undefined ? obj(...vals) : obj.call(receiver, ...vals); ret = sanitizeProp(ret, context); if (ret !== null && typeof ret === 'object' && ret instanceof DelayedSynchronousResult) { Promise.resolve(ret.result).then( (res) => done(undefined, res), (err) => done(err), ); } else { done(undefined, ret); } return; } // Method call: obj is a Prop. `a` is already the replacement (from Prop.get()). // The original function is still accessible via obj.context[obj.prop] for subscription checks. const originalFn: unknown = obj.context[obj.prop]; if (originalFn === JSON.stringify && context.getSubscriptions.size) { const cache = new WeakSet(); let ticks = 0n; const recurse = (x: unknown) => { if (!x || !(typeof x === 'object') || cache.has(x)) return; cache.add(x); const keys = Object.keys(x) as (keyof typeof x)[]; ticks += BigInt(keys.length); for (const y of keys) { context.getSubscriptions.forEach((cb) => cb(x, y)); recurse(x[y]); } }; recurse(vals[0]); checkTicksAndThrow(context, ticks); } if ( obj.context instanceof Array && arrayChange.has(originalFn as any) && (context.changeSubscriptions.get(obj.context) || context.changeSubscriptionsGlobal.get(obj.context)) ) { let change: Change = undefined!; let changed = false; if (obj.prop === 'push') { change = { type: 'push', added: vals }; changed = !!vals.length; } else if (obj.prop === 'pop') { change = { type: 'pop', removed: obj.context.slice(-1) }; changed = !!change.removed.length; } else if (obj.prop === 'shift') { change = { type: 'shift', removed: obj.context.slice(0, 1) }; changed = !!change.removed.length; } else if (obj.prop === 'unshift') { change = { type: 'unshift', added: vals }; changed = !!vals.length; } else if (obj.prop === 'splice') { change = { type: 'splice', startIndex: vals[0] as number, deleteCount: vals[1] === undefined ? obj.context.length : vals[1], added: vals.slice(2), removed: obj.context.slice( vals[0], vals[1] === undefined ? undefined : (vals[0] as number) + (vals[1] as number), ), }; changed = !!change.added.length || !!change.removed.length; } else if (obj.prop === 'reverse' || obj.prop === 'sort') { change = { type: obj.prop }; changed = !!obj.context.length; } else if (obj.prop === 'copyWithin') { const len = vals[2] === undefined ? obj.context.length - (vals[1] as number) : Math.min(obj.context.length, (vals[2] as number) - (vals[1] as number)); change = { type: 'copyWithin', startIndex: vals[0] as number, endIndex: (vals[0] as number) + len, added: obj.context.slice(vals[1] as number, (vals[1] as number) + len), removed: obj.context.slice(vals[0] as number, (vals[0] as number) + len), }; changed = !!change.added.length || !!change.removed.length; } if (changed) { const subs = context.changeSubscriptions.get(obj.context); if (subs !== undefined) for (const cb of subs) cb(change); const subsG = context.changeSubscriptionsGlobal.get(obj.context); if (subsG !== undefined) for (const cb of subsG) cb(change); } } // Trigger get-subscriptions, then call via `a` (which may be a replacement from evals). // Sandboxed wrappers for globals (Function, eval, etc.) must be called without `this`; // tick-checking replacements must be called with `this`. obj.get(context); const evl = context.evals.get(originalFn as Function); const receiver = getReplacementReceiver(originalFn as Function); const thisArg = obj.isVariable && receiver !== undefined ? receiver : obj.context; let ret = evl ? evl.call(thisArg, ...vals) : (a as Function).call(thisArg, ...vals); ret = sanitizeProp(ret, context); if (ret !== null && typeof ret === 'object' && ret instanceof DelayedSynchronousResult) { Promise.resolve(ret.result).then( (res) => done(undefined, res), (err) => done(err), ); } else { done(undefined, ret); } }); addOps void, unknown[]>(LispType.New, (params) => { const { done, a, b, context } = params; if (!context.ctx.globalsWhitelist.has(a) && !context.ctx.sandboxedFunctions.has(a)) { throw new SandboxAccessError(`Object construction not allowed: ${a.constructor.name}`); } const vals = b.map((item) => sanitizeProp(item, context)); const replacement = context.evals.get(a); if (replacement) { const ret = new (replacement as new (...args: unknown[]) => unknown)(...vals); done(undefined, ret); return; } const expectedTicks = getNewTicks(a, vals); if (expectedTicks > 0n && checkHaltExpectedTicks(params, expectedTicks)) return; const ret = new a(...vals); done(undefined, ret); }); function getNewTicks(ctor: Function, args: unknown[]): bigint { // new Array(n) or new TypedArray(n) — allocates n elements if (ctor === Array) { const n = args[0]; if (typeof n === 'number' && args.length === 1) return BigInt(n); return BigInt(args.length); } if (_typedArrayProtos.has(Object.getPrototypeOf(ctor.prototype))) { const n = args[0]; if (typeof n === 'number') return BigInt(n); if (Array.isArray(n) || ArrayBuffer.isView(n)) return BigInt((n as ArrayLike).length); return 0n; } // new Map(iterable) or new Set(iterable) — O(n) of iterable length if (ctor === Map || ctor === Set) { const iterable = args[0]; if (Array.isArray(iterable)) return BigInt(iterable.length); return 0n; } // new String(s) or new RegExp(pattern) — O(n) of string length if (ctor === String || ctor === RegExp) { const s = args[0]; if (typeof s === 'string') return BigInt(s.length); return 0n; } return 0n; } ================================================ FILE: src/executor/ops/comparison.ts ================================================ import { addOps, checkHaltExpectedTicks } from '../executorUtils'; import { LispType } from '../../utils'; addOps(LispType.LargerThan, ({ done, a, b }) => done(undefined, a > b)); addOps(LispType.SmallerThan, ({ done, a, b }) => done(undefined, a < b)); addOps(LispType.LargerEqualThan, ({ done, a, b }) => done(undefined, a >= b)); addOps(LispType.SmallerEqualThan, ({ done, a, b }) => done(undefined, a <= b)); addOps(LispType.Equal, ({ done, a, b }) => done(undefined, a == b)); addOps(LispType.StrictEqual, ({ done, a, b }) => done(undefined, a === b)); addOps(LispType.NotEqual, ({ done, a, b }) => done(undefined, a != b)); addOps(LispType.StrictNotEqual, ({ done, a, b }) => done(undefined, a !== b)); addOps(LispType.And, ({ done, a, b }) => done(undefined, a && b)); addOps(LispType.Or, ({ done, a, b }) => done(undefined, a || b)); addOps(LispType.NullishCoalescing, ({ done, a, b }) => done(undefined, a ?? b)); addOps(LispType.BitAnd, ({ done, a, b }) => done(undefined, a & b)); addOps(LispType.BitOr, ({ done, a, b }) => done(undefined, a | b)); addOps(LispType.Plus, (params) => { const { done, a, b } = params; const result = (a as any) + (b as any); if (typeof result === 'string' && checkHaltExpectedTicks(params, BigInt(result.length))) return; done(undefined, result); }); addOps(LispType.Minus, ({ done, a, b }) => done(undefined, a - b)); addOps(LispType.Divide, ({ done, a, b }) => done(undefined, a / b)); addOps(LispType.Power, ({ done, a, b }) => done(undefined, a ** b)); addOps(LispType.BitNegate, ({ done, a, b }) => done(undefined, a ^ b)); addOps(LispType.Multiply, ({ done, a, b }) => done(undefined, a * b)); addOps(LispType.Modulus, ({ done, a, b }) => done(undefined, a % b)); addOps(LispType.BitShiftLeft, ({ done, a, b }) => done(undefined, a << b)); addOps(LispType.BitShiftRight, ({ done, a, b }) => done(undefined, a >> b)); addOps(LispType.BitUnsignedShiftRight, ({ done, a, b }) => done(undefined, a >>> b), ); addOps(LispType.Instanceof, ({ done, a, b }) => done(undefined, a instanceof b), ); addOps(LispType.In, ({ done, a, b }) => done(undefined, a in b)); ================================================ FILE: src/executor/ops/control.ts ================================================ import { addOps, execAsync, execSync, asyncDone, executeTreeWithDone, normalizeStatementLabels, addControlFlowTargets, createLoopTarget, createSwitchTarget, createLabeledStatementTarget, matchesControlFlowTarget, If, ExecReturn, executeTreeAsync, syncDone, executeTree, normalizeStatementLabel, addControlFlowTarget, } from '../executorUtils'; import type { AsyncDoneRet } from '../executorUtils'; import type { Lisp, LispItem, StatementLabel, SwitchCase } from '../../parser'; import { LispType, Scope, SandboxError, sanitizeProp } from '../../utils'; addOps( LispType.Loop, ({ exec, done, ticks, a, b, context, scope, statementLabels, internal, generatorYield }) => { const [ checkFirst, startInternal, getIterator, startStep, step, condition, beforeStep, isForAwait, label, ] = a; const loopStatementTargets = [ ...normalizeStatementLabels(label).map((loopLabel) => createLoopTarget(loopLabel, false)), createLoopTarget(), ]; const loopTargets = addControlFlowTargets(statementLabels, loopStatementTargets); if ((isForAwait as unknown as LispType) === LispType.True && exec !== execAsync) { done(new SyntaxError('for-await-of loops are only allowed inside async functions')); return; } let loop = true; const loopScope = new Scope(scope, {}); const internalVars: Record = { $$obj: undefined, }; const interalScope = new Scope(loopScope, internalVars); if (exec === execAsync) { (async () => { let ad: AsyncDoneRet; ad = asyncDone((d) => exec(ticks, startStep, loopScope, context, d, undefined, internal, generatorYield), ); internalVars['$$obj'] = (ad = asyncDone((d) => exec(ticks, getIterator, loopScope, context, d, undefined, internal, generatorYield), )).isInstant === true ? ad.instant : (await ad.p).result; // for-await-of: override $$obj with the correct async iterator if ((isForAwait as unknown as LispType) === LispType.True) { const obj = internalVars['$$obj'] as any; internalVars['$$obj'] = obj[Symbol.asyncIterator] ? obj[Symbol.asyncIterator]() : obj[Symbol.iterator] ? obj[Symbol.iterator]() : obj; } ad = asyncDone((d) => exec(ticks, startInternal, interalScope, context, d, undefined, internal, generatorYield), ); // for-await-of: await the $$next promise after startInternal sets it if ((isForAwait as unknown as LispType) === LispType.True) { internalVars['$$next'] = await internalVars['$$next']; } if (checkFirst) loop = (ad = asyncDone((d) => exec(ticks, condition, interalScope, context, d, undefined, internal, generatorYield), )).isInstant === true ? ad.instant : (await ad.p).result; while (loop) { const innerLoopVars = {}; const iterScope = new Scope(interalScope, innerLoopVars); ad = asyncDone((d) => exec(ticks, beforeStep, iterScope, context, d, undefined, internal, generatorYield), ); ad.isInstant === true ? ad.instant : (await ad.p).result; const res = await executeTreeAsync( ticks, context, b, [iterScope], loopTargets, internal, generatorYield, ); if (res instanceof ExecReturn && res.returned) { done(undefined, res); return; } if (res instanceof ExecReturn && res.controlFlow) { if ( !loopStatementTargets.some((target) => matchesControlFlowTarget(res.controlFlow!, target), ) ) { done(undefined, res); return; } if (res.breakLoop) { break; } } ad = asyncDone((d) => exec(ticks, step, interalScope, context, d, undefined, internal, generatorYield), ); // for-await-of: await the $$next promise after step updates it if ((isForAwait as unknown as LispType) === LispType.True) { internalVars['$$next'] = await internalVars['$$next']; } loop = (ad = asyncDone((d) => exec(ticks, condition, interalScope, context, d, undefined, internal, generatorYield), )).isInstant === true ? ad.instant : (await ad.p).result; } done(); })().catch(done); } else { syncDone((d) => exec(ticks, startStep, loopScope, context, d, undefined, internal, generatorYield), ); internalVars['$$obj'] = syncDone((d) => exec(ticks, getIterator, loopScope, context, d, undefined, internal, generatorYield), ).result; syncDone((d) => exec(ticks, startInternal, interalScope, context, d, undefined, internal, generatorYield), ); if (checkFirst) loop = syncDone((d) => exec(ticks, condition, interalScope, context, d, undefined, internal, generatorYield), ).result; while (loop) { const innerLoopVars = {}; const iterScope = new Scope(interalScope, innerLoopVars); syncDone((d) => exec(ticks, beforeStep, iterScope, context, d, undefined, internal, generatorYield), ); const res = executeTree( ticks, context, b, [iterScope], loopTargets, internal, generatorYield, ); if (res instanceof ExecReturn && res.returned) { done(undefined, res); return; } if (res instanceof ExecReturn && res.controlFlow) { if ( !loopStatementTargets.some((target) => matchesControlFlowTarget(res.controlFlow!, target), ) ) { done(undefined, res); return; } if (res.breakLoop) { break; } } syncDone((d) => exec(ticks, step, interalScope, context, d, undefined, internal, generatorYield), ); loop = syncDone((d) => exec(ticks, condition, interalScope, context, d, undefined, internal, generatorYield), ).result; } done(); } }, ); addOps( LispType.If, ({ exec, done, ticks, a, b, context, scope, statementLabels, internal, generatorYield }) => { exec( ticks, sanitizeProp(a, context) ? b.t : b.f, scope, context, done, statementLabels, internal, generatorYield, ); }, ); addOps( LispType.InlineIf, ({ exec, done, ticks, a, b, context, scope, internal, generatorYield }) => { exec( ticks, sanitizeProp(a, context) ? b.t : b.f, scope, context, done, undefined, internal, generatorYield, ); }, ); addOps(LispType.InlineIfCase, ({ done, a, b }) => done(undefined, new If(a, b))); addOps(LispType.IfCase, ({ done, a, b }) => done(undefined, new If(a, b))); addOps( LispType.Labeled, ({ exec, done, ticks, a, b, context, scope, statementLabels, internal, generatorYield }) => { const target = createLabeledStatementTarget(normalizeStatementLabel(a)); exec( ticks, b, scope, context, (...args: unknown[]) => { if (args.length === 1) { done(args[0]); return; } const res = args[1]; if (res instanceof ExecReturn && res.controlFlow && target) { if (matchesControlFlowTarget(res.controlFlow, target)) { done(); return; } } done(undefined, res as any); }, addControlFlowTarget(statementLabels, target), internal, generatorYield, ); }, ); addOps( LispType.Switch, ({ exec, done, ticks, a, b, context, scope, statementLabels, internal, generatorYield }) => { const switchTarget = createSwitchTarget(); const switchTargets = addControlFlowTarget(statementLabels, switchTarget); exec( ticks, a, scope, context, (...args: unknown[]) => { if (args.length === 1) { done(args[0]); return; } let toTest = args[1]; toTest = sanitizeProp(toTest, context); if (exec === execSync) { let res: ExecReturn; let isTrue = false; for (const caseItem of b) { if ( isTrue || (isTrue = !caseItem[1] || toTest === sanitizeProp( syncDone((d) => exec( ticks, caseItem[1], scope, context, d, undefined, internal, generatorYield, ), ).result, context, )) ) { if (!caseItem[2]) continue; res = executeTree( ticks, context, caseItem[2], [scope], switchTargets, internal, generatorYield, ); if (res.controlFlow) { if (!matchesControlFlowTarget(res.controlFlow, switchTarget)) { done(undefined, res); return; } if (res.breakLoop) break; } if (res.returned) { done(undefined, res); return; } if (!caseItem[1]) { // default case break; } } } done(); } else { (async () => { let res: ExecReturn; let isTrue = false; for (const caseItem of b) { let ad: AsyncDoneRet; if ( isTrue || (isTrue = !caseItem[1] || toTest === sanitizeProp( (ad = asyncDone((d) => exec( ticks, caseItem[1], scope, context, d, undefined, internal, generatorYield, ), )).isInstant === true ? ad.instant : (await ad.p).result, context, )) ) { if (!caseItem[2]) continue; res = await executeTreeAsync( ticks, context, caseItem[2], [scope], switchTargets, internal, generatorYield, ); if (res.controlFlow) { if (!matchesControlFlowTarget(res.controlFlow, switchTarget)) { done(undefined, res); return; } if (res.breakLoop) break; } if (res.returned) { done(undefined, res); return; } if (!caseItem[1]) { // default case break; } } } done(); })().catch(done); } }, undefined, internal, generatorYield, ); }, ); addOps( LispType.Try, ({ exec, done, ticks, a, b, context, scope, statementLabels, internal, generatorYield }) => { const [exception, catchBody, finallyBody] = b; // Execute try block executeTreeWithDone( exec, (...tryArgs: unknown[]) => { const tryHadError = tryArgs.length === 1; const tryError = tryHadError ? tryArgs[0] : undefined; const tryResult = !tryHadError && tryArgs.length > 1 ? tryArgs[1] : undefined; // Handler to execute finally and complete const executeFinallyAndComplete = (hadError: boolean, errorOrResult: unknown) => { if (finallyBody && finallyBody.length > 0) { // Execute finally block executeTreeWithDone( exec, (...finallyArgs: unknown[]) => { const finallyHadError = finallyArgs.length === 1; const finallyResult = !finallyHadError && finallyArgs.length > 1 ? finallyArgs[1] : undefined; // If finally throws an error, it overrides everything if (finallyHadError) { done(finallyArgs[0]); return; } // If finally has a control flow statement (return/break/continue), it overrides everything if ( finallyResult instanceof ExecReturn && (finallyResult.returned || finallyResult.breakLoop || finallyResult.continueLoop) ) { done(undefined, finallyResult); return; } // Otherwise, return the original try/catch result/error if (hadError) { done(errorOrResult); } else if (errorOrResult instanceof ExecReturn) { // If try/catch returned or has some other control flow, pass that through if ( errorOrResult.returned || errorOrResult.breakLoop || errorOrResult.continueLoop ) { done(undefined, errorOrResult); } else { // Normal completion - don't return a value done(); } } else { // Try/catch completed normally, just signal completion with no return value done(); } }, ticks, context, finallyBody, [new Scope(scope, {})], statementLabels, internal, generatorYield, ); } else { // No finally block, just return result/error if (hadError) { done(errorOrResult); } else if (errorOrResult instanceof ExecReturn) { // If try/catch returned or has some other control flow, pass that through if (errorOrResult.returned || errorOrResult.breakLoop || errorOrResult.continueLoop) { done(undefined, errorOrResult); } else { // Normal completion - don't return a value done(); } } else { done(); } } }; // SandboxErrors bypass both catch and finally — propagate immediately. if (tryHadError && tryError instanceof SandboxError) { done(tryError); return; } // If try had an error and there's a catch block, execute catch. if (tryHadError && catchBody && catchBody.length > 0) { const sc: Record = {}; if (exception) sc[exception] = tryError; executeTreeWithDone( exec, (...catchArgs: unknown[]) => { const catchHadError = catchArgs.length === 1; const catchErrorOrResult = catchHadError ? catchArgs[0] : catchArgs.length > 1 ? catchArgs[1] : undefined; // Execute finally with catch result executeFinallyAndComplete(catchHadError, catchErrorOrResult); }, ticks, context, catchBody, [new Scope(scope, sc)], statementLabels, internal, generatorYield, ); } else { // No catch or no error, execute finally with try result executeFinallyAndComplete(tryHadError, tryHadError ? tryError : tryResult); } }, ticks, context, a, [new Scope(scope)], statementLabels, internal, generatorYield, ); }, ); addOps(LispType.Expression, ({ done, a }) => done(undefined, a.pop())); ================================================ FILE: src/executor/ops/functions.ts ================================================ import { addOps, createAsyncGeneratorFunction, createFunction, createFunctionAsync, createGeneratorFunction, } from '../executorUtils'; import type { Lisp } from '../../parser'; import { LispType, SandboxCapabilityError, CodeString, Scope, VarType } from '../../utils'; addOps( LispType.ArrowFunction, ({ done, ticks, a, b, obj, context, scope, internal }) => { if (typeof obj[2] === 'string' || obj[2] instanceof CodeString) { if (context.allowJit && context.evalContext) { obj[2] = b = context.evalContext.lispifyFunction(new CodeString(obj[2]), context.constants); } else { throw new SandboxCapabilityError('Unevaluated code detected, JIT not allowed'); } } const argNames = a.slice(1); if (a[0]) { done(undefined, createFunctionAsync(argNames, b, ticks, context, scope, undefined, internal)); } else { done(undefined, createFunction(argNames, b, ticks, context, scope, undefined, internal)); } }, ); addOps<(string | LispType)[], Lisp[], Lisp>( LispType.Function, ({ done, ticks, a, b, obj, context, scope, internal }) => { if (typeof obj[2] === 'string' || obj[2] instanceof CodeString) { if (context.allowJit && context.evalContext) { obj[2] = b = context.evalContext.lispifyFunction( new CodeString(obj[2]), context.constants, false, { generatorDepth: a[1] === LispType.True ? 1 : 0, asyncDepth: a[0] === LispType.True ? 1 : 0, lispDepth: 0, }, ); } else { throw new SandboxCapabilityError('Unevaluated code detected, JIT not allowed'); } } const isAsync = a[0]; const isGenerator = a[1]; const name = a[2] as string; const argNames = a.slice(3) as string[]; let func; if (isAsync === LispType.True && isGenerator === LispType.True) { func = createAsyncGeneratorFunction(argNames, b, ticks, context, scope, name, internal); } else if (isGenerator === LispType.True) { func = createGeneratorFunction(argNames, b, ticks, context, scope, name, internal); } else if (isAsync === LispType.True) { func = createFunctionAsync(argNames, b, ticks, context, scope, name, internal); } else { func = createFunction(argNames, b, ticks, context, scope, name, internal); } if (name) { scope.declare(name, VarType.var, func, false, internal); } done(undefined, func); }, ); addOps<(string | LispType)[], Lisp[], Lisp>( LispType.InlineFunction, ({ done, ticks, a, b, obj, context, scope, internal }) => { if (typeof obj[2] === 'string' || obj[2] instanceof CodeString) { if (context.allowJit && context.evalContext) { obj[2] = b = context.evalContext.lispifyFunction( new CodeString(obj[2]), context.constants, false, { generatorDepth: a[1] === LispType.True ? 1 : 0, asyncDepth: a[0] === LispType.True ? 1 : 0, lispDepth: 0, }, ); } else { throw new SandboxCapabilityError('Unevaluated code detected, JIT not allowed'); } } const isAsync = a[0]; const isGenerator = a[1]; const name = a[2] as string; const argNames = a.slice(3) as string[]; if (name) { scope = new Scope(scope, {}); } let func; if (isAsync === LispType.True && isGenerator === LispType.True) { func = createAsyncGeneratorFunction(argNames, b, ticks, context, scope, name, internal); } else if (isGenerator === LispType.True) { func = createGeneratorFunction(argNames, b, ticks, context, scope, name, internal); } else if (isAsync === LispType.True) { func = createFunctionAsync(argNames, b, ticks, context, scope, name, internal); } else { func = createFunction(argNames, b, ticks, context, scope, name, internal); } if (name) { scope.declare(name, VarType.let, func, false, internal); } done(undefined, func); }, ); ================================================ FILE: src/executor/ops/index.ts ================================================ import './prop'; import './call'; import './object'; import './literals'; import './unary'; import './assignment'; import './comparison'; import './variables'; import './misc'; import './functions'; import './control'; ================================================ FILE: src/executor/ops/literals.ts ================================================ import { addOps, literalRegex, checkHaltExpectedTicks } from '../executorUtils'; import type { Lisp, IRegEx } from '../../parser'; import { LispType, SandboxCapabilityError, sanitizeProp } from '../../utils'; addOps(LispType.Number, ({ done, b }) => done(undefined, Number(b.replace(/_/g, ''))), ); addOps(LispType.BigInt, ({ done, b }) => done(undefined, BigInt(b.replace(/_/g, ''))), ); addOps(LispType.RegexIndex, ({ done, b, context }) => { const reg: IRegEx = context.constants.regexes[parseInt(b)]; if (!context.ctx.globalsWhitelist.has(RegExp)) { throw new SandboxCapabilityError('Regex not permitted'); } else { const RegExpCtor = (context.evals.get(RegExp) as | (new (pattern: string, flags?: string) => unknown) | undefined) ?? RegExp; done(undefined, new RegExpCtor(reg.regex, reg.flags)); } }); addOps(LispType.LiteralIndex, (params) => { const { exec, done, ticks, b, context, scope, internal, generatorYield } = params; const item = context.constants.literals[parseInt(b)]; const [, name, js] = item; const found: Lisp[] = []; let f: RegExpExecArray | null; const resnums: string[] = []; while ((f = literalRegex.exec(name))) { if (!f[2]) { found.push(js[parseInt(f[3], 10)]); resnums.push(f[3]); } } exec( ticks, found, scope, context, (...args: unknown[]) => { const reses: Record = {}; if (args.length === 1) { done(args[0]); return; } const processed = args[1]; for (const i of Object.keys(processed!) as (keyof typeof processed)[]) { const num = resnums[i]; reses[num] = processed![i]; } const result = name.replace(/(\\\\)*(\\)?\${(\d+)}/g, (match, $$, $, num) => { if ($) return match; const res = reses[num]; return ($$ ? $$ : '') + `${sanitizeProp(res, context)}`; }); if (checkHaltExpectedTicks(params, BigInt(result.length))) return; done(undefined, result); }, undefined, internal, generatorYield, ); }); ================================================ FILE: src/executor/ops/misc.ts ================================================ import { addOps, findControlFlowTarget, ExecReturn, normalizeStatementLabel, } from '../executorUtils'; import type { ControlFlowAction } from '../executorUtils'; import type { LispItem, StatementLabel } from '../../parser'; import { LispType, Prop, VarType, SandboxCapabilityError } from '../../utils'; addOps( LispType.Internal, ({ done, a, b, scope, bobj, internal }) => { if (!internal) { throw new SandboxCapabilityError('Internal variables are not accessible'); } done(undefined, scope.declare(a, VarType.internal, b, bobj?.isGlobal || false, internal)); }, ); addOps( LispType.LoopAction, ({ done, a, b, context, statementLabels }) => { const label = normalizeStatementLabel(b); const target = findControlFlowTarget(statementLabels, a as ControlFlowAction, label); if (target === null) { throw new TypeError('Illegal continue statement'); } if (!target) { throw new TypeError(label ? `Undefined label '${label}'` : 'Illegal ' + a + ' statement'); } done( undefined, new ExecReturn(context.ctx.auditReport, undefined, false, { type: a as ControlFlowAction, label, }), ); }, ); addOps(LispType.Throw, ({ done, b }) => { done(b); }); addOps(LispType.None, ({ done }) => done()); ================================================ FILE: src/executor/ops/object.ts ================================================ import { addOps, checkHaltExpectedTicks, SpreadArray, SpreadObject, KeyVal, ArrayHole, } from '../executorUtils'; import type { Lisp, LispItem } from '../../parser'; import { LispType, sanitizeProp } from '../../utils'; addOps(LispType.CreateObject, (params) => { const { done, b } = params; let res = {} as any; for (const item of b) { if (item.key instanceof SpreadObject) { const keys = Object.keys(item.key.item); if (checkHaltExpectedTicks(params, BigInt(keys.length))) return; res = { ...res, ...item.key.item }; } else { res[item.key] = item.val; } } done(undefined, res); }); addOps(LispType.KeyVal, ({ done, a, b }) => done(undefined, new KeyVal(a, b)), ); addOps(LispType.CreateArray, (params) => { const { done, b, context } = params; const items: unknown[] = []; for (const item of b) { if (item instanceof SpreadArray) { const expanded = Array.isArray(item.item) ? item.item : [...(item.item as Iterable)]; if (checkHaltExpectedTicks(params, BigInt(expanded.length))) return; for (const v of expanded) items.push(sanitizeProp(v, context)); } else if (item instanceof ArrayHole) { items.length++; } else { items.push(sanitizeProp(item, context)); } } done(undefined, items); }); addOps(LispType.Hole, ({ done }) => done(undefined, new ArrayHole())); addOps(LispType.Group, ({ done, b }) => done(undefined, b)); addOps(LispType.GlobalSymbol, ({ done, b }) => { switch (b) { case 'true': return done(undefined, true); case 'false': return done(undefined, false); case 'null': return done(undefined, null); case 'undefined': return done(undefined, undefined); case 'NaN': return done(undefined, NaN); case 'Infinity': return done(undefined, Infinity); } done(new Error('Unknown symbol: ' + b)); }); addOps(LispType.SpreadArray, ({ done, b }) => { done(undefined, new SpreadArray(b)); }); addOps>(LispType.SpreadObject, ({ done, b }) => { done(undefined, new SpreadObject(b)); }); ================================================ FILE: src/executor/ops/prop.ts ================================================ import { addOps, hasPossibleProperties, isPropertyKey } from '../executorUtils'; import { LispType, Prop, SandboxAccessError, resolveSandboxProp, hasOwnProperty, } from '../../utils'; addOps(LispType.Prop, ({ done, a, b, obj, context, scope, internal }) => { if (a === null) { throw new TypeError(`Cannot read properties of null (reading '${b?.toString()}')`); } if (!isPropertyKey(b)) { b = `${b}`; } if (a === undefined && obj === undefined && typeof b === 'string') { // is variable access const prop = scope.get(b, internal); if (prop.context === undefined) { throw new ReferenceError(`${b} is not defined`); } if (prop.context === context.ctx.sandboxGlobal) { if (context.ctx.options.audit) { context.ctx.auditReport?.globalsAccess.add(b); } } const val = (prop.context as any)[prop.prop]; const p = resolveSandboxProp(val, context, prop) || prop; done(undefined, p); return; } else if (a === undefined) { throw new TypeError(`Cannot read properties of undefined (reading '${b.toString()}')`); } if (!hasPossibleProperties(a)) { done(undefined, new Prop(undefined, b)); return; } const prototypeAccess = typeof a === 'function' || !hasOwnProperty(a, b); if (context.ctx.options.audit && prototypeAccess) { let prot: {} = Object.getPrototypeOf(a); do { if (hasOwnProperty(prot, b)) { if ( context.ctx.auditReport && !context.ctx.auditReport.prototypeAccess[prot.constructor.name] ) { context.ctx.auditReport.prototypeAccess[prot.constructor.name] = new Set(); } context.ctx.auditReport?.prototypeAccess[prot.constructor.name].add(b); } } while ((prot = Object.getPrototypeOf(prot))); } if (prototypeAccess) { if (typeof a === 'function') { if (hasOwnProperty(a, b)) { const whitelist = context.ctx.prototypeWhitelist.get(a.prototype); if ( !(whitelist && (!whitelist.size || whitelist.has(b))) && !context.ctx.sandboxedFunctions.has(a) ) { throw new SandboxAccessError( `Static method or property access not permitted: ${a.name}.${b.toString()}`, ); } } } let prot: {} = a; while ((prot = Object.getPrototypeOf(prot))) { if (hasOwnProperty(prot, b) || b === '__proto__') { const whitelist = context.ctx.prototypeWhitelist.get(prot); if ( (whitelist && (!whitelist.size || whitelist.has(b))) || context.ctx.sandboxedFunctions.has(prot.constructor) ) { break; } if (b === '__proto__') { throw new SandboxAccessError(`Access to prototype of global object is not permitted`); } throw new SandboxAccessError( `Method or property access not permitted: ${prot.constructor.name}.${b.toString()}`, ); } } } if (typeof a === 'function') { if (b === 'prototype' && !context.ctx.sandboxedFunctions.has(a)) { throw new SandboxAccessError(`Access to prototype of global object is not permitted`); } if (['caller', 'callee', 'arguments'].includes(b as string)) { throw new SandboxAccessError(`Access to '${b as string}' property is not permitted`); } } const val = a[b as keyof typeof a] as unknown; if (b === '__proto__' && !context.ctx.sandboxedFunctions.has(val?.constructor as any)) { throw new SandboxAccessError(`Access to prototype of global object is not permitted`); } const p = resolveSandboxProp(val, context, new Prop(a, b, false, false)); if (p) { done(undefined, p); return; } const isSandboxGlobal = a === context.ctx.sandboxGlobal; const g = (!isSandboxGlobal && obj instanceof Prop && obj.isGlobal) || (typeof a === 'function' && !context.ctx.sandboxedFunctions.has(a)) || context.ctx.globalsWhitelist.has(a) || (isSandboxGlobal && typeof b === 'string' && hasOwnProperty(context.ctx.globalScope.globals, b)); done(undefined, new Prop(a, b, false, g, false)); }); addOps(LispType.StringIndex, ({ done, b, context }) => done(undefined, context.constants.strings[parseInt(b)]), ); ================================================ FILE: src/executor/ops/unary.ts ================================================ import { addOps, assignCheck } from '../executorUtils'; import type { LispItem } from '../../parser'; import { LispType, Prop, sanitizeProp } from '../../utils'; addOps(LispType.Not, ({ done, b }) => done(undefined, !b)); addOps(LispType.Inverse, ({ done, b }) => done(undefined, ~b)); addOps>(LispType.IncrementBefore, ({ done, obj, context }) => { assignCheck(obj, context); done(undefined, ++obj.context[obj.prop]); }); addOps>(LispType.IncrementAfter, ({ done, obj, context }) => { assignCheck(obj, context); done(undefined, obj.context[obj.prop]++); }); addOps>(LispType.DecrementBefore, ({ done, obj, context }) => { assignCheck(obj, context); done(undefined, --obj.context[obj.prop]); }); addOps>(LispType.DecrementAfter, ({ done, obj, context }) => { assignCheck(obj, context); done(undefined, obj.context[obj.prop]--); }); addOps(LispType.Positive, ({ done, b }) => done(undefined, +b)); addOps(LispType.Negative, ({ done, b }) => done(undefined, -b)); addOps( LispType.Typeof, ({ exec, done, ticks, b, context, scope, internal, generatorYield }) => { exec( ticks, b, scope, context, (e, prop) => { done(undefined, typeof sanitizeProp(prop, context)); }, undefined, internal, generatorYield, ); }, ); addOps(LispType.Delete, ({ done, context, bobj }) => { if (!(bobj instanceof Prop)) { done(undefined, true); return; } assignCheck(bobj, context, 'delete'); if (bobj.isVariable) { done(undefined, false); return; } done(undefined, delete (bobj.context as any)?.[bobj.prop]); }); addOps(LispType.Void, ({ done }) => { done(); }); ================================================ FILE: src/executor/ops/variables.ts ================================================ import { addOps } from '../executorUtils'; import { LispType, Prop, VarType } from '../../utils'; addOps(LispType.Return, ({ done, b }) => done(undefined, b)); addOps(LispType.Var, ({ done, a, b, scope, bobj, internal }) => { done(undefined, scope.declare(a, VarType.var, b, bobj?.isGlobal || false, internal)); }); addOps(LispType.Let, ({ done, a, b, scope, bobj, internal }) => { done(undefined, scope.declare(a, VarType.let, b, bobj?.isGlobal || false, internal)); }); addOps(LispType.Const, ({ done, a, b, scope, bobj, internal }) => { done(undefined, scope.declare(a, VarType.const, b, bobj?.isGlobal || false, internal)); }); ================================================ FILE: src/executor/opsRegistry.ts ================================================ /** * Ops registry — kept separate from executorUtils.ts so that ops/*.ts files * can import addOps without creating a circular initialization problem. * * executorUtils.ts imports from here; ops/*.ts imports from here too. * No imports from executorUtils.ts allowed in this file. */ import { LispType } from '../utils'; import type { LispItem } from '../parser'; import type { IExecContext, Ticks } from '../utils'; import type { Scope } from '../utils'; import type { YieldValue, Done } from './executorUtils'; export type ControlFlowAction = 'break' | 'continue'; export interface ControlFlowTarget { label?: string; acceptsBreak: boolean; acceptsContinue: boolean; acceptsUnlabeledBreak: boolean; acceptsUnlabeledContinue: boolean; } export type ControlFlowTargets = readonly ControlFlowTarget[] | undefined; export type Execution = ( ticks: Ticks, tree: LispItem, scope: Scope, context: IExecContext, done: Done, statementLabels: ControlFlowTargets, internal: boolean, generatorYield: ((yv: YieldValue, done?: Done) => void) | undefined, ) => void; export type OpsCallbackParams = { op: LispType; exec: Execution; a: a; b: b; obj: obj; bobj: bobj; ticks: Ticks; tree: LispItem; scope: Scope; context: IExecContext; done: Done; statementLabels: ControlFlowTargets; internal: boolean; generatorYield: ((yv: YieldValue, done?: Done) => void) | undefined; }; type OpCallback = (params: OpsCallbackParams) => void; export const ops = new Map>(); export function addOps( type: LispType, cb: OpCallback, ) { ops.set(type, cb); } ================================================ FILE: src/parser/index.ts ================================================ export * from './lisp'; export * from './parserUtils'; export { default } from './parserUtils'; ================================================ FILE: src/parser/lisp.ts ================================================ import type { CodeString, LispType } from '../utils'; export type DefineLisp< op extends LispType, a extends LispItem | LispItem, b extends LispItem | LispItem, > = [op, a, b]; export type ExtractLispOp = L extends DefineLisp ? i : never; export type ExtractLispA = L extends DefineLisp ? i : never; export type ExtractLispB = L extends DefineLisp ? i : never; export type LispItemSingle = LispType.None | LispType.True | string | Lisp; export type LispItem = LispItemSingle | LispItemSingle[]; export type Lisp = [LispType, LispItem, LispItem]; export type Literal = DefineLisp & { tempJsStrings?: string[] }; export type IfLisp = DefineLisp; export type InlineIf = DefineLisp; export type StatementLabel = string | LispType.None; export type IfCase = DefineLisp; export type InlineIfCase = DefineLisp; export type Labeled = DefineLisp; export type KeyValLisp = DefineLisp; export type SpreadObjectLisp = DefineLisp; export type SpreadArrayLisp = DefineLisp; export type ArrayProp = DefineLisp; export type PropLisp = DefineLisp; export type PropOptional = DefineLisp; export type Call = DefineLisp; export type CallOptional = DefineLisp; export type CreateArray = DefineLisp; export type CreateObject = DefineLisp; export type Group = DefineLisp; export type Inverse = DefineLisp; export type Not = DefineLisp; export type Negative = DefineLisp; export type Positive = DefineLisp; export type Typeof = DefineLisp; export type Delete = DefineLisp; export type IncrementBefore = DefineLisp; export type IncrementAfter = DefineLisp; export type DecrementBefore = DefineLisp; export type DecrementAfter = DefineLisp; export type And = DefineLisp; export type Or = DefineLisp; export type NullishCoalescing = DefineLisp; export type Instanceof = DefineLisp; export type In = DefineLisp; export type Assigns = DefineLisp; export type SubractEquals = DefineLisp; export type AddEquals = DefineLisp; export type DivideEquals = DefineLisp; export type PowerEquals = DefineLisp; export type MultiplyEquals = DefineLisp; export type ModulusEquals = DefineLisp; export type BitNegateEquals = DefineLisp; export type BitAndEquals = DefineLisp; export type BitOrEquals = DefineLisp; export type UnsignedShiftRightEquals = DefineLisp; export type ShiftLeftEquals = DefineLisp; export type ShiftRightEquals = DefineLisp; export type AndEquals = DefineLisp; export type OrEquals = DefineLisp; export type NullishCoalescingEquals = DefineLisp; export type BitAnd = DefineLisp; export type BitOr = DefineLisp; export type BitNegate = DefineLisp; export type BitShiftLeft = DefineLisp; export type BitShiftRight = DefineLisp; export type BitUnsignedShiftRight = DefineLisp; export type SmallerEqualThan = DefineLisp; export type LargerEqualThan = DefineLisp; export type SmallerThan = DefineLisp; export type LargerThan = DefineLisp; export type StrictNotEqual = DefineLisp; export type NotEqual = DefineLisp; export type StrictEqual = DefineLisp; export type Equal = DefineLisp; export type Plus = DefineLisp; export type Minus = DefineLisp; export type Divide = DefineLisp; export type Power = DefineLisp; export type Multiply = DefineLisp; export type Modulus = DefineLisp; export type InternalCode = DefineLisp; export type Block = DefineLisp; export type Expression = DefineLisp; export type Return = DefineLisp; export type Throw = DefineLisp; export type Switch = DefineLisp; export type SwitchCase = DefineLisp; export type Var = DefineLisp; export type Let = DefineLisp; export type Const = DefineLisp; export type Internal = DefineLisp; export type Number = DefineLisp; export type BigInt = DefineLisp; export type GlobalSymbol = DefineLisp; export type LiteralIndex = DefineLisp; export type StringIndex = DefineLisp; export type RegexIndex = DefineLisp; export type Function = DefineLisp< LispType.Function, (string | LispType.None | LispType.True)[], string | Lisp[] >; export type InlineFunction = DefineLisp; export type ArrowFunction = DefineLisp; export type Loop = DefineLisp; export type LoopAction = DefineLisp; export type Try = DefineLisp; export type Void = DefineLisp; export type Await = DefineLisp; export type Yield = DefineLisp; export type YieldDelegate = DefineLisp; export type New = DefineLisp; export type None = DefineLisp; export type LispFamily = | Literal | IfLisp | InlineIf | IfCase | InlineIfCase | Labeled | KeyValLisp | SpreadObjectLisp | SpreadArrayLisp | ArrayProp | PropLisp | PropOptional | Call | CallOptional | CreateArray | CreateObject | Group | Inverse | Not | Negative | Positive | Typeof | Delete | IncrementBefore | IncrementAfter | DecrementBefore | DecrementAfter | And | Or | NullishCoalescing | Instanceof | In | Assigns | SubractEquals | AddEquals | DivideEquals | PowerEquals | MultiplyEquals | ModulusEquals | BitNegateEquals | BitAndEquals | BitOrEquals | UnsignedShiftRightEquals | ShiftLeftEquals | ShiftRightEquals | AndEquals | OrEquals | NullishCoalescingEquals | BitAnd | BitOr | BitNegate | BitShiftLeft | BitShiftRight | BitUnsignedShiftRight | SmallerEqualThan | LargerEqualThan | SmallerThan | LargerThan | StrictNotEqual | NotEqual | StrictEqual | Equal | Plus | Minus | Divide | Power | Multiply | Modulus | InternalCode | Expression | Return | Throw | Switch | SwitchCase | Var | Let | Const | Number | BigInt | GlobalSymbol | LiteralIndex | StringIndex | RegexIndex | Function | InlineFunction | ArrowFunction | Loop | LoopAction | Try | Void | Await | Yield | YieldDelegate | New | Block | Internal | None; export interface IRegEx { regex: string; flags: string; length: number; } export interface IConstants { strings: string[]; literals: Literal[]; regexes: IRegEx[]; eager: boolean; maxDepth: number; } export interface IExecutionTree { tree: Lisp[]; constants: IConstants; } export interface LispDepthCtx { generatorDepth: number; asyncDepth: number; lispDepth: number; } export interface LispCallbackCtx extends LispDepthCtx { constants: IConstants; type: string; part: CodeString; res: string[]; expect: string; lispTree: Lisp; } export type LispCallback = (ctx: LispCallbackCtx & { type: T }) => any; ================================================ FILE: src/parser/lispTypes/conditionals.ts ================================================ import { CodeString, LispType, SandboxCapabilityError } from '../../utils'; import type { IConstants, IfLisp, IfCase, Lisp, Switch, SwitchCase } from '../lisp'; import type { RegisterLispTypesDeps } from './shared'; export function registerConditionalLispTypes({ createLisp, emptyString, extractStatementLabels, insertSemicolons, lispifyBlock, lispifyExpr, restOfExp, semiColon, setLispType, wrapLabeledStatement, }: RegisterLispTypesDeps) { const inlineIfElse = /^:/; const elseIf = /^else(?![\w$])/; const ifElse = /^if(?![\w$])/; function extractIfElse(constants: IConstants, part: CodeString, depth = 0) { if (depth > constants.maxDepth) { throw new SandboxCapabilityError('Maximum expression depth exceeded'); } let count = 0; let found = part.substring(0, 0); let foundElse = emptyString; let foundTrue: CodeString | undefined; let first = true; let elseReg: RegExpExecArray | null; let details: any = {}; while ( (found = restOfExp( constants, part.substring(found.end - part.start), [elseIf, ifElse, semiColon], undefined, undefined, undefined, details, )).length || first ) { first = false; const f = part.substring(found.end - part.start).toString(); if (f.startsWith('if')) { found.end++; count++; } else if (f.startsWith('else')) { foundTrue = part.substring(0, found.end - part.start); found.end++; count--; if (!count) { found.end--; } } else if ((elseReg = /^;?\s*else(?![\w$])/.exec(f))) { foundTrue = part.substring(0, found.end - part.start); found.end += elseReg[0].length - 1; count--; if (!count) { found.end -= elseReg[0].length - 1; } } else { foundTrue = foundElse.length ? foundTrue : part.substring(0, found.end - part.start); break; } if (!count) { const ie = extractIfElse( constants, part.substring(found.end - part.start + (/^;?\s*else(?![\w$])/.exec(f)?.[0].length || 0)), depth + 1, ); foundElse = ie.all; break; } details = {}; } foundTrue = foundTrue || part.substring(0, found.end - part.start); return { all: part.substring(0, Math.max(foundTrue.end, foundElse.end) - part.start), true: foundTrue, false: foundElse, }; } setLispType(['if'] as const, (ctx) => { const { constants, part, res } = ctx; const labels = extractStatementLabels(res[1]); let condition = restOfExp(constants, part.substring(res[0].length), [], '('); const ie = extractIfElse(constants, part.substring(res[1].length)); const startTrue = res[0].length - res[1].length + condition.length + 1; let trueBlock = ie.true.substring(startTrue); let elseBlock = ie.false; condition = condition.trim(); trueBlock = trueBlock.trim(); elseBlock = elseBlock.trim(); if (!trueBlock.length || /^else(?![\w$])/.test(trueBlock.toString())) { throw new SyntaxError('Unexpected token'); } if (trueBlock.char(0) === '{') trueBlock = trueBlock.slice(1, -1); if (elseBlock.char(0) === '{') elseBlock = elseBlock.slice(1, -1); ctx.lispTree = wrapLabeledStatement( labels, createLisp({ op: LispType.If, a: lispifyExpr(constants, condition, undefined, ctx), b: createLisp({ op: LispType.IfCase, a: lispifyBlock(trueBlock, constants, false, ctx), b: lispifyBlock(elseBlock, constants, false, ctx), }), }), ) as Lisp; }); setLispType(['switch'] as const, (ctx) => { const { constants, part, res } = ctx; const labels = extractStatementLabels(res[1]); const test = restOfExp(constants, part.substring(res[0].length), [], '('); let start = part.toString().indexOf('{', res[0].length + test.length + 1); if (start === -1) throw new SyntaxError('Invalid switch'); let statement = insertSemicolons( constants, restOfExp(constants, part.substring(start + 1), [], '{'), ); let caseFound: RegExpExecArray | null; const caseTest = /^\s*(case\s|default)\s*/; const caseNoTestReg = /^\s*case\s*:/; const cases: SwitchCase[] = []; let defaultFound = false; while ( (caseFound = caseTest.exec(statement.toString())) || caseNoTestReg.test(statement.toString()) ) { if (!caseFound) { throw new SyntaxError('Unexpected end of expression'); } if (caseFound[1] === 'default') { if (defaultFound) throw new SyntaxError('Only one default switch case allowed'); defaultFound = true; } const cond = restOfExp(constants, statement.substring(caseFound[0].length), [/^:/]); if (caseFound[1] !== 'default' && !cond.trimStart().length) { throw new SyntaxError('Unexpected end of expression'); } let found = emptyString; let i = (start = caseFound[0].length + cond.length + 1); const bracketFound = /^\s*\{/.exec(statement.substring(i).toString()); let exprs: Lisp[] = []; if (bracketFound) { i += bracketFound[0].length; found = restOfExp(constants, statement.substring(i), [], '{'); i += found.length + 1; exprs = lispifyBlock(found, constants, false, ctx); } else { const notEmpty = restOfExp(constants, statement.substring(i), [caseTest]); if (!notEmpty.trim().length) { exprs = []; i += notEmpty.length; } else { while ((found = restOfExp(constants, statement.substring(i), [semiColon])).length) { i += found.length + (statement.char(i + found.length) === ';' ? 1 : 0); if (caseTest.test(statement.substring(i).toString())) { break; } } exprs = lispifyBlock( statement.substring(start, found.end - statement.start), constants, false, ctx, ); } } statement = statement.substring(i); cases.push( createLisp({ op: LispType.SwitchCase, a: caseFound[1] === 'default' ? LispType.None : lispifyExpr(constants, cond, undefined, ctx), b: exprs, }), ); } ctx.lispTree = wrapLabeledStatement( labels, createLisp({ op: LispType.Switch, a: lispifyExpr(constants, test, undefined, ctx), b: cases, }), ) as Lisp; }); void inlineIfElse; } ================================================ FILE: src/parser/lispTypes/control.ts ================================================ import { CodeString, LispType } from '../../utils'; import type { Block, InternalCode, Lisp, LispItem, Loop, LoopAction, StatementLabel, Try, } from '../lisp'; import type { RegisterLispTypesDeps } from './shared'; export function registerControlLispTypes({ ParseError, createLisp, emptyString, expandDestructure, extractStatementLabels, insertSemicolons, lispify, lispifyBlock, lispifyExpr, lispifyReturnExpr, restOfExp, semiColon, setLispType, startingExecpted, wrapLabeledStatement, }: RegisterLispTypesDeps) { const iteratorRegex = /^((let|var|const|internal)\s+)?\s*([a-zA-Z$_][a-zA-Z\d$_]*)\s+(in|of)(?![\w$])/; const iteratorDestructureRegex = /^((let|var|const|internal)\s+)\s*([[{])/; const ofStart2 = lispify( { maxDepth: 10 } as any, new CodeString('let $$iterator = $$obj[Symbol.iterator]()'), ['initialize'], ); const ofStart3 = lispify( { maxDepth: 10 } as any, new CodeString('let $$next = $$iterator.next()'), ['initialize'], ); const ofCondition = lispify({ maxDepth: 10 } as any, new CodeString('return !$$next.done'), [ 'initialize', ]); const ofStep = lispify({ maxDepth: 10 } as any, new CodeString('$$next = $$iterator.next()')); const asyncOfStart2 = lispify({ maxDepth: 10 } as any, new CodeString('let $$iterator = $$obj'), [ 'initialize', ]); const asyncOfStart3 = lispify( { maxDepth: 10 } as any, new CodeString('let $$next = $$iterator.next()'), ['initialize'], ); const asyncOfStep = lispify( { maxDepth: 10 } as any, new CodeString('$$next = $$iterator.next()'), ); const inStart2 = lispify( { maxDepth: 10 } as any, new CodeString('let $$keys = Object.keys($$obj)'), ['initialize'], ); const inStart3 = lispify({ maxDepth: 10 } as any, new CodeString('let $$keyIndex = 0'), [ 'initialize', ]); const inStep = lispify({ maxDepth: 10 } as any, new CodeString('$$keyIndex++')); const inCondition = lispify( { maxDepth: 10 } as any, new CodeString('return $$keyIndex < $$keys.length'), ['initialize'], ); setLispType(['for', 'do', 'while'] as const, (ctx) => { const { constants, type, part, res } = ctx; const labels = extractStatementLabels(res[1]); let i = 0; let startStep: LispItem = LispType.True; let startInternal: Lisp[] = []; let getIterator: Lisp | LispType.None = LispType.None; let beforeStep: LispItem = LispType.None; let checkFirst = LispType.True; let condition: LispItem; let step: LispItem = LispType.True; let body: CodeString; const isForAwait: LispItem = type === 'for' && res[2] ? LispType.True : LispType.None; switch (type) { case 'while': { i = part.toString().indexOf('(') + 1; const extract = restOfExp(constants, part.substring(i), [], '('); if (!extract.trimStart().length) { throw new SyntaxError('Unexpected end of expression'); } condition = lispifyReturnExpr(constants, extract); body = restOfExp(constants, part.substring(i + extract.length + 1)).trim(); if (body.char(0) === '{') body = body.slice(1, -1); break; } case 'for': { i = part.toString().indexOf('(') + 1; const args: CodeString[] = []; let extract2 = emptyString; for (let k = 0; k < 3; k++) { extract2 = restOfExp(constants, part.substring(i), [/^[;)]/]); args.push(extract2.trim()); i += extract2.length + 1; if (part.char(i - 1) === ')') break; } let iterator: RegExpExecArray | null; let iteratorDestructure: RegExpExecArray | null; if (args.length === 1 && (iterator = iteratorRegex.exec(args[0].toString()))) { const iterableExpr = args[0].substring(iterator[0].length); if (!iterableExpr.trimStart().length) { throw new SyntaxError('Unexpected end of expression'); } if (iterator[4] === 'of') { getIterator = lispifyReturnExpr(constants, iterableExpr); startInternal = isForAwait ? [asyncOfStart2, asyncOfStart3] : [ofStart2, ofStart3]; condition = ofCondition; step = isForAwait ? asyncOfStep : ofStep; beforeStep = lispify( constants, new CodeString((iterator[1] || 'let ') + iterator[3] + ' = $$next.value'), ['initialize'], ); } else { if (isForAwait) { throw new SyntaxError("Unexpected token 'in'"); } getIterator = lispifyReturnExpr(constants, iterableExpr); startInternal = [inStart2, inStart3]; step = inStep; condition = inCondition; beforeStep = lispify( constants, new CodeString((iterator[1] || 'let ') + iterator[3] + ' = $$keys[$$keyIndex]'), ['initialize'], ); } } else if ( args.length === 1 && (iteratorDestructure = iteratorDestructureRegex.exec(args[0].toString())) ) { const keyword = iteratorDestructure[1].trim(); const openBracket = iteratorDestructure[3]; const closeBracket = openBracket === '[' ? ']' : '}'; const keywordPrefixLen = iteratorDestructure[0].length - 1; const patternContent = restOfExp( constants, args[0].substring(keywordPrefixLen + 1), [], openBracket, ); const patternStr = openBracket + patternContent.toString() + closeBracket; const afterClose = args[0] .substring(keywordPrefixLen + 1 + patternContent.length + 1) .trimStart(); const inOfMatch = /^(in|of)(?![\w$])\s*/.exec(afterClose.toString()); if (!inOfMatch) throw new SyntaxError('Invalid for loop definition'); const inOf = inOfMatch[1]; const iterExpr = afterClose.substring(inOfMatch[0].length); if (!iterExpr.trimStart().length) { throw new SyntaxError('Unexpected end of expression'); } const tempVarName = '$$_fv'; if (inOf === 'of') { getIterator = lispifyReturnExpr(constants, iterExpr); startInternal = isForAwait ? [asyncOfStart2, asyncOfStart3] : [ofStart2, ofStart3]; condition = ofCondition; step = isForAwait ? asyncOfStep : ofStep; const expandedCode = expandDestructure(keyword, patternStr, tempVarName); const stmts = lispifyBlock(new CodeString(expandedCode), constants, false, ctx); beforeStep = createLisp({ op: LispType.InternalBlock, a: [ lispify(constants, new CodeString(`${keyword} ${tempVarName} = $$next.value`), [ 'initialize', ]), ...stmts, ], b: LispType.None, }); } else { getIterator = lispifyReturnExpr(constants, iterExpr); startInternal = [inStart2, inStart3]; step = inStep; condition = inCondition; const expandedCode = expandDestructure(keyword, patternStr, tempVarName); const stmts = lispifyBlock(new CodeString(expandedCode), constants, false, ctx); beforeStep = createLisp({ op: LispType.InternalBlock, a: [ lispify( constants, new CodeString(`${keyword} ${tempVarName} = $$keys[$$keyIndex]`), ['initialize'], ), ...stmts, ], b: LispType.None, }); } } else if (args.length === 3) { const [startArg, conditionArg, stepArg] = args; if (startArg.length) { startStep = lispifyExpr(constants, startArg, startingExecpted, ctx); } condition = conditionArg.length ? lispifyReturnExpr(constants, conditionArg) : LispType.True; if (stepArg.length) { step = lispifyExpr(constants, stepArg, undefined, ctx); } } else { throw new SyntaxError('Invalid for loop definition'); } body = restOfExp(constants, part.substring(i)).trim(); if (body.char(0) === '{') body = body.slice(1, -1); break; } case 'do': { checkFirst = LispType.None; const isBlock = !!res[2]; body = restOfExp(constants, part.substring(res[0].length), isBlock ? [/^\}/] : [semiColon]); condition = lispifyReturnExpr( constants, restOfExp( constants, part.substring(part.toString().indexOf('(', res[0].length + body.length) + 1), [], '(', ), ); break; } } const a = [ checkFirst, startInternal, getIterator, startStep, step, condition, beforeStep, isForAwait, labels, ] as LispItem; ctx.lispTree = createLisp({ op: LispType.Loop, a, b: lispifyBlock(body, constants, false, ctx), }); }); setLispType(['block'] as const, (ctx) => { const { constants, part, res } = ctx; const labels = extractStatementLabels(res[1]); ctx.lispTree = wrapLabeledStatement( labels, createLisp({ op: LispType.Block, a: lispifyBlock( restOfExp(constants, part.substring(res[0].length), [], '{'), constants, false, ctx, ), b: LispType.None, }), ) as Lisp; }); setLispType(['loopAction'] as const, (ctx) => { const { part, res } = ctx; const remaining = part.substring(res[0].length).trimStart(); if (remaining.length && !res[2]) { throw new SyntaxError(`Unexpected token '${remaining.char(0)}'`); } ctx.lispTree = createLisp({ op: LispType.LoopAction, a: res[1], b: (res[2] || LispType.None) as StatementLabel, }); }); const catchReg = /^\s*(catch\s*(\(\s*([a-zA-Z$_][a-zA-Z\d$_]*)\s*\))?|finally)\s*\{/; const catchEmptyBindingReg = /^\s*catch\s*\(\s*\)/; const catchReservedBindingReg = /^\s*catch\s*\(\s*(break|case|catch|continue|debugger|default|delete|do|else|finally|for|function|if|in|instanceof|new|return|switch|this|throw|try|typeof|var|void|while|with|class|const|enum|export|extends|implements|import|interface|let|package|private|protected|public|static|super|yield|await)\s*\)/; const finallyKeywordReg = /^\s*finally\b/; setLispType(['try'] as const, (ctx) => { const { constants, part, res } = ctx; const body = restOfExp(constants, part.substring(res[0].length), [], '{'); const afterBody = part.substring(res[0].length + body.length + 1).toString(); if (catchEmptyBindingReg.test(afterBody)) { throw new ParseError("Unexpected token ')'", part.toString()); } const reservedMatch = catchReservedBindingReg.exec(afterBody); if (reservedMatch) { throw new ParseError(`Unexpected token '${reservedMatch[1]}'`, part.toString()); } const finallyMatch = finallyKeywordReg.exec(afterBody); if (finallyMatch && !/^\s*\{/.test(afterBody.substring(finallyMatch[0].length))) { throw new ParseError('Unexpected token', part.toString()); } let catchRes = catchReg.exec(afterBody); let finallyBody; let exception = ''; let catchBody; let offset = 0; if (!catchRes) { throw new ParseError('Missing catch or finally after try', part.toString()); } if (catchRes[1].startsWith('catch')) { catchRes = catchReg.exec(part.substring(res[0].length + body.length + 1).toString()); exception = catchRes![3] || ''; catchBody = restOfExp( constants, part.substring(res[0].length + body.length + 1 + catchRes![0].length), [], '{', ); offset = res[0].length + body.length + 1 + catchRes![0].length + catchBody.length + 1; if ( (catchRes = catchReg.exec(part.substring(offset).toString())) && catchRes[1].startsWith('finally') ) { finallyBody = restOfExp(constants, part.substring(offset + catchRes[0].length), [], '{'); } } else { finallyBody = restOfExp( constants, part.substring(res[0].length + body.length + 1 + catchRes![0].length), [], '{', ); } const b = [ exception, lispifyBlock(insertSemicolons(constants, catchBody || emptyString), constants, false, ctx), lispifyBlock(insertSemicolons(constants, finallyBody || emptyString), constants, false, ctx), ] as LispItem; ctx.lispTree = wrapLabeledStatement( extractStatementLabels(res[1]), createLisp({ op: LispType.Try, a: lispifyBlock(insertSemicolons(constants, body), constants, false, ctx), b, }), ) as Lisp; }); } ================================================ FILE: src/parser/lispTypes/declarations.ts ================================================ import { CodeString, LispType, reservedWords } from '../../utils'; import type { ArrowFunction, Const, Function as FunctionLisp, InlineFunction, Internal, InternalCode, Let, Var, } from '../lisp'; import type { RegisterLispTypesDeps } from './shared'; export function registerDeclarationLispTypes({ createLisp, expectTypes, expandDestructure, expandFunctionParamDestructure, lispify, lispifyBlock, lispifyFunction, restOfExp, semiColon, setLispType, splitByCommasDestructure, }: RegisterLispTypesDeps) { setLispType(['initializeDestructure'] as const, (ctx) => { const { constants, part, res } = ctx; const keyword = res[1] as string; const openBracket = res[2] as string; const closeBracket = openBracket === '[' ? ']' : '}'; const patternContent = restOfExp(constants, part.substring(res[0].length), [], openBracket); const patternStr = openBracket + patternContent.toString() + closeBracket; const afterClose = part.substring(res[0].length + patternContent.length + 1).trimStart(); if (!afterClose.length || afterClose.char(0) !== '=') { throw new SyntaxError('Destructuring declaration requires an initializer'); } const rhsCode = restOfExp(constants, afterClose.substring(1).trimStart(), [semiColon]); const rhsStr = rhsCode.toString(); const expandedCode = expandDestructure(keyword, patternStr, rhsStr); const stmts = lispifyBlock(new CodeString(expandedCode), constants, false, ctx); ctx.lispTree = createLisp({ op: LispType.InternalBlock, a: stmts, b: LispType.None, }); }); setLispType(['initialize'] as const, (ctx) => { const { constants, part, res, expect } = ctx; const lt = res[1] === 'var' ? LispType.Var : res[1] === 'let' ? LispType.Let : res[1] === 'const' ? LispType.Const : LispType.Internal; if (!res[3]) { ctx.lispTree = lispify( constants, part.substring(res[0].length), expectTypes[expect].next, createLisp({ op: lt, a: res[2], b: LispType.None, }), false, ctx, ); } else { const initExpr = part.substring(res[0].length); if (!initExpr.trimStart().length) { throw new SyntaxError('Unexpected end of expression'); } ctx.lispTree = createLisp({ op: lt, a: res[2], b: lispify(constants, initExpr, expectTypes[expect].next, undefined, false, ctx), }); } }); setLispType( ['function', 'inlineFunction', 'arrowFunction', 'arrowFunctionSingle'] as const, (ctx) => { const { constants, type, part, res, expect, generatorDepth, asyncDepth } = ctx; const isArrow = type !== 'function' && type !== 'inlineFunction'; const isReturn = isArrow && !res[res.length - 1]; const isGenerator = !isArrow && res[2] && res[2].trimStart().startsWith('*') ? LispType.True : LispType.None; const isAsync = res[1] ? LispType.True : LispType.None; let rawArgStr: string; let bodyOffset: number; if (type === 'function' || type === 'inlineFunction') { const argsCode = restOfExp(constants, part.substring(res[0].length), [], '('); rawArgStr = argsCode.toString().trim(); bodyOffset = res[0].length + argsCode.length + 1; const afterParen = part.substring(bodyOffset).trimStart(); bodyOffset = part.length - afterParen.length + 1; } else { rawArgStr = res[2] ?? ''; bodyOffset = 0; } const rawArgs: string[] = rawArgStr ? splitByCommasDestructure(rawArgStr) .map((a) => a.trim()) .filter(Boolean) : []; const args: string[] = [...rawArgs]; if (!isArrow) { args.unshift((res[3] || '').trimStart()); } let ended = false; args.forEach((arg: string) => { if (ended) throw new SyntaxError('Rest parameter must be last formal parameter'); if (arg.startsWith('...')) ended = true; }); const f = restOfExp( constants, isArrow ? part.substring(res[0].length) : part.substring(bodyOffset), !isReturn ? [/^}/] : [/^[,)}\]]/, semiColon], ); let funcBody = isReturn ? 'return ' + f : f.toString(); const funcArgs = isArrow ? args : args.slice(1); const expanded = expandFunctionParamDestructure(funcArgs, funcBody); const finalArgs = isArrow ? expanded.args : [args[0], ...expanded.args]; funcBody = expanded.body; finalArgs.forEach((arg: string) => { if (reservedWords.has(arg.replace(/^\.\.\./, ''))) { throw new SyntaxError(`Unexpected token '${arg}'`); } }); const afterFunc = isArrow ? res[0].length + f.length + 1 : bodyOffset + f.length + 1; ctx.lispTree = lispify( constants, part.substring(afterFunc), expectTypes[expect].next, createLisp({ op: isArrow ? LispType.ArrowFunction : type === 'function' ? LispType.Function : LispType.InlineFunction, a: isArrow ? [isAsync, ...finalArgs] : [isAsync, isGenerator, ...finalArgs], b: constants.eager ? lispifyFunction( new CodeString(funcBody), constants, false, isArrow ? { generatorDepth: 0, asyncDepth: 0, lispDepth: 0 } : { generatorDepth: isGenerator === LispType.True ? generatorDepth + 1 : 0, asyncDepth: isAsync === LispType.True ? asyncDepth + 1 : 0, lispDepth: 0, }, ) : funcBody, }), false, ctx, ); }, ); } ================================================ FILE: src/parser/lispTypes/index.ts ================================================ import type { RegisterLispTypesDeps } from './shared'; import { registerConditionalLispTypes } from './conditionals'; import { registerControlLispTypes } from './control'; import { registerDeclarationLispTypes } from './declarations'; import { registerOperatorLispTypes } from './operators'; import { registerStructureLispTypes } from './structures'; import { registerValueLispTypes } from './values'; export function registerLispTypes(deps: RegisterLispTypesDeps) { registerStructureLispTypes(deps); registerOperatorLispTypes(deps); registerConditionalLispTypes(deps); registerValueLispTypes(deps); registerDeclarationLispTypes(deps); registerControlLispTypes(deps); } export type { RegisterLispTypesDeps } from './shared'; ================================================ FILE: src/parser/lispTypes/operators.ts ================================================ import { CodeString, isLisp, LispType } from '../../utils'; import type { AddEquals, And, AndEquals, Assigns, BigInt as BigIntLisp, BitAnd, BitAndEquals, BitNegate, BitNegateEquals, BitOr, BitOrEquals, BitShiftLeft, BitShiftRight, BitUnsignedShiftRight, Call, CreateArray, DecrementAfter, DecrementBefore, Delete, Divide, DivideEquals, Equal, GlobalSymbol, In, IncrementAfter, IncrementBefore, InlineIf, InlineIfCase, Instanceof, InternalCode, Inverse, LargerEqualThan, LargerThan, Lisp, Minus, Modulus, ModulusEquals, Multiply, MultiplyEquals, Negative, Not, NotEqual, NullishCoalescing, NullishCoalescingEquals, Number as NumberLisp, Or, OrEquals, Plus, Positive, Power, PowerEquals, ShiftLeftEquals, ShiftRightEquals, SmallerEqualThan, SmallerThan, StrictEqual, StrictNotEqual, StringIndex, SubractEquals, Typeof, UnsignedShiftRightEquals, } from '../lisp'; import type { RegisterLispTypesDeps } from './shared'; export function registerOperatorLispTypes({ createLisp, expandDestructure, expectTypes, getDestructurePatternSource, lispifyBlock, restOfExp, lispify, lispifyExpr, setLispType, }: RegisterLispTypesDeps) { const inlineIfElse = /^:/; const modifierTypes = { inverse: LispType.Inverse, not: LispType.Not, positive: LispType.Positive, negative: LispType.Negative, typeof: LispType.Typeof, delete: LispType.Delete, } as const; setLispType(['inverse', 'not', 'negative', 'positive', 'typeof', 'delete'] as const, (ctx) => { const { constants, type, part, res, expect } = ctx; const extract = restOfExp(constants, part.substring(res[0].length), [/^([^\s.?\w$]|\?[^.])/]); const remainingAfterOperand = part.substring(extract.length + res[0].length); const remainingStr = remainingAfterOperand.trim().toString(); if (remainingStr.startsWith('**')) { throw new SyntaxError( 'Unary operator used immediately before exponentiation expression. Parenthesis must be used to disambiguate operator precedence', ); } ctx.lispTree = lispify( constants, part.substring(extract.length + res[0].length), restOfExp.next, createLisp({ op: modifierTypes[type], a: ctx.lispTree, b: lispify(constants, extract, expectTypes[expect].next), }), false, ctx, ); }); setLispType(['taggedTemplate'] as const, (ctx) => { const { constants, part, res, expect } = ctx; const literalIndex = res[1]; const literal = constants.literals[parseInt(literalIndex)]; const [, templateStr, jsExprs] = literal; const stringParts: string[] = []; const expressions: Lisp[] = []; let currentStr = ''; let i = 0; while (i < templateStr.length) { if (templateStr.substring(i, i + 2) === '${') { let j = i + 2; let exprIndex = ''; let isValidPlaceholder = false; while (j < templateStr.length && templateStr[j] !== '}') { exprIndex += templateStr[j]; j++; } if (j < templateStr.length && templateStr[j] === '}' && /^\d+$/.test(exprIndex)) { isValidPlaceholder = true; } if (isValidPlaceholder) { stringParts.push(currentStr); currentStr = ''; expressions.push(jsExprs[parseInt(exprIndex)]); i = j + 1; } else { currentStr += templateStr[i]; i++; } } else { currentStr += templateStr[i]; i++; } } stringParts.push(currentStr); const stringsArray = stringParts.map((str) => createLisp({ op: LispType.StringIndex, a: LispType.None, b: String(constants.strings.push(str) - 1), }), ); const stringsArrayLisp = createLisp({ op: LispType.CreateArray, a: createLisp({ op: LispType.None, a: LispType.None, b: LispType.None, }), b: stringsArray, }); ctx.lispTree = lispify( constants, part.substring(res[0].length), expectTypes[expect].next, createLisp({ op: LispType.Call, a: ctx.lispTree, b: [stringsArrayLisp, ...expressions], }), false, ctx, ); }); const incrementTypes = { '++$': LispType.IncrementBefore, '--$': LispType.DecrementBefore, '$++': LispType.IncrementAfter, '$--': LispType.DecrementAfter, } as any; setLispType(['incrementerBefore'] as const, (ctx) => { const { constants, part, res } = ctx; const extract = restOfExp(constants, part.substring(2), [/^[^\s.\w$]/]); ctx.lispTree = lispify( constants, part.substring(extract.length + 2), restOfExp.next, createLisp({ op: incrementTypes[res[0] + '$'], a: lispify(constants, extract, expectTypes.incrementerBefore.next), b: LispType.None, }), false, ctx, ); }); setLispType(['incrementerAfter'] as const, (ctx) => { const { constants, part, res, expect } = ctx; if ( ctx.lispTree[0] === LispType.Number || ctx.lispTree[0] === LispType.BigInt || ctx.lispTree[0] === LispType.GlobalSymbol || ctx.lispTree[0] === LispType.StringIndex || ctx.lispTree[0] === LispType.LiteralIndex || ctx.lispTree[0] === LispType.RegexIndex ) { throw new SyntaxError('Invalid left-hand side expression in postfix operation'); } ctx.lispTree = lispify( constants, part.substring(res[0].length), expectTypes[expect].next, createLisp({ op: incrementTypes['$' + res[0]], a: ctx.lispTree, b: LispType.None, }), false, ctx, ); }); const adderTypes = { '&&': LispType.And, '||': LispType.Or, '??': LispType.NullishCoalescing, instanceof: LispType.Instanceof, in: LispType.In, '=': LispType.Assign, '-=': LispType.SubractEquals, '+=': LispType.AddEquals, '/=': LispType.DivideEquals, '**=': LispType.PowerEquals, '*=': LispType.MultiplyEquals, '%=': LispType.ModulusEquals, '^=': LispType.BitNegateEquals, '&=': LispType.BitAndEquals, '|=': LispType.BitOrEquals, '>>>=': LispType.UnsignedShiftRightEquals, '<<=': LispType.ShiftLeftEquals, '>>=': LispType.ShiftRightEquals, '&&=': LispType.AndEquals, '||=': LispType.OrEquals, '??=': LispType.NullishCoalescingEquals, } as any; setLispType(['assign', 'assignModify', 'nullishCoalescing'] as const, (ctx) => { const { constants, type, part, res } = ctx; if ( type !== 'nullishCoalescing' && isLisp(ctx.lispTree) && (ctx.lispTree[0] === LispType.PropOptional || ctx.lispTree[0] === LispType.CallOptional) ) { throw new SyntaxError('Invalid left-hand side in assignment'); } if (res[0] === '=') { const patternStr = getDestructurePatternSource(ctx.lispTree); if (patternStr) { const rhsStr = part.substring(res[0].length).toString(); const tempName = `$$_da${Math.random().toString(36).slice(2)}`; const expandedCode = `internal ${tempName} = (${rhsStr}); ${expandDestructure('', patternStr, tempName)}; ${tempName}`; ctx.lispTree = createLisp({ op: LispType.InternalBlock, a: lispifyBlock(new CodeString(expandedCode), constants, false, ctx), b: LispType.None, }); return; } } ctx.lispTree = createLisp< | NullishCoalescing | Assigns | SubractEquals | AddEquals | DivideEquals | PowerEquals | MultiplyEquals | ModulusEquals | BitNegateEquals | BitAndEquals | BitOrEquals | UnsignedShiftRightEquals | ShiftLeftEquals | ShiftRightEquals | AndEquals | OrEquals | NullishCoalescingEquals >({ op: adderTypes[res[0]], a: ctx.lispTree, b: lispify( constants, part.substring(res[0].length), expectTypes.assignment.next, undefined, false, { ...ctx, lispDepth: ctx.lispDepth + 1 }, ), }); }); const opTypes = { '&': LispType.BitAnd, '|': LispType.BitOr, '^': LispType.BitNegate, '<<': LispType.BitShiftLeft, '>>': LispType.BitShiftRight, '>>>': LispType.BitUnsignedShiftRight, '<=': LispType.SmallerEqualThan, '>=': LispType.LargerEqualThan, '<': LispType.SmallerThan, '>': LispType.LargerThan, '!==': LispType.StrictNotEqual, '!=': LispType.NotEqual, '===': LispType.StrictEqual, '==': LispType.Equal, '+': LispType.Plus, '-': LispType.Minus, '/': LispType.Divide, '**': LispType.Power, '*': LispType.Multiply, '%': LispType.Modulus, '&&': LispType.And, '||': LispType.Or, instanceof: LispType.Instanceof, in: LispType.In, } as any; setLispType( [ 'power', 'opHigh', 'op', 'comparitor', 'bitwiseShift', 'bitwiseAnd', 'bitwiseXor', 'bitwiseOr', 'boolOpAnd', 'boolOpOr', ] as const, (ctx) => { const { constants, type, part, res } = ctx; const next = [expectTypes.inlineIf.types.inlineIf, inlineIfElse]; switch (type) { case 'power': break; case 'opHigh': next.push(expectTypes.splitter.types.opHigh); case 'op': next.push(expectTypes.splitter.types.op); case 'comparitor': next.push(expectTypes.splitter.types.comparitor); case 'bitwiseShift': next.push(expectTypes.splitter.types.bitwiseShift); case 'bitwiseAnd': next.push(expectTypes.splitter.types.bitwiseAnd); case 'bitwiseXor': next.push(expectTypes.splitter.types.bitwiseXor); case 'bitwiseOr': next.push(expectTypes.splitter.types.bitwiseOr); case 'boolOpAnd': next.push(expectTypes.splitter.types.boolOpAnd); case 'boolOpOr': next.push(expectTypes.splitter.types.boolOpOr); } const extract = restOfExp(constants, part.substring(res[0].length), next); ctx.lispTree = lispify( constants, part.substring(extract.length + res[0].length), restOfExp.next, createLisp< | BitAnd | BitOr | BitNegate | BitShiftLeft | BitShiftRight | BitUnsignedShiftRight | SmallerEqualThan | LargerEqualThan | SmallerThan | LargerThan | StrictNotEqual | NotEqual | StrictEqual | Equal | Plus | Minus | Divide | Power | Multiply | Modulus | And | Or | Instanceof | In >({ op: opTypes[res[0]], a: ctx.lispTree, b: lispify(constants, extract, expectTypes.splitter.next), }), false, ctx, ); }, ); setLispType(['inlineIf'] as const, (ctx) => { const { constants, part, res } = ctx; let found = false; const extract = part.substring(0, 0); let quoteCount = 1; while (!found && extract.length < part.length) { extract.end = restOfExp(constants, part.substring(extract.length + 1), [ expectTypes.inlineIf.types.inlineIf, inlineIfElse, ]).end; if (part.char(extract.length) === '?') { quoteCount++; } else { quoteCount--; } if (!quoteCount) { found = true; } } extract.start = part.start + 1; const falseExpr = part.substring(res[0].length + extract.length + 1); if (!falseExpr.trimStart().length) { throw new SyntaxError('Unexpected end of expression'); } ctx.lispTree = createLisp({ op: LispType.InlineIf, a: ctx.lispTree, b: createLisp({ op: LispType.InlineIfCase, a: lispifyExpr(constants, extract, undefined, { ...ctx, lispDepth: ctx.lispDepth + 1 }), b: lispifyExpr(constants, falseExpr, undefined, { ...ctx, lispDepth: ctx.lispDepth + 1 }), }), }); }); } ================================================ FILE: src/parser/lispTypes/shared.ts ================================================ import type { CodeString } from '../../utils'; import type { ExtractLispA, ExtractLispB, ExtractLispOp, IConstants, Lisp, LispCallback, LispDepthCtx, LispItem, None, Return, } from '../lisp'; export interface RegisterLispTypesDeps { NullLisp: None; ParseError: new (message: string, code: string) => Error; createLisp: (obj: { op: ExtractLispOp; a: ExtractLispA; b: ExtractLispB; }) => L; emptyString: CodeString; expectTypes: Record; next: string[] }>; expandDestructure: (keyword: string, patternStr: string, rhsStr: string) => string; expandFunctionParamDestructure: ( args: string[], funcBody: string, ) => { args: string[]; body: string }; extractStatementLabels: (prefix?: string) => string[]; findPatternEndIdx: (s: string) => number; getDestructurePatternSource: (tree: LispItem) => string | null; insertSemicolons: (constants: IConstants, str: CodeString) => CodeString; lispify: ( constants: IConstants, part: CodeString, expected?: readonly string[], lispTree?: Lisp, topLevel?: boolean, depthCtx?: LispDepthCtx, ) => Lisp; lispifyBlock: ( str: CodeString, constants: IConstants, expression?: boolean, depthCtx?: LispDepthCtx, ) => Lisp[]; lispifyExpr: ( constants: IConstants, str: CodeString, expected?: readonly string[], depthCtx?: LispDepthCtx, ) => Lisp; lispifyFunction: ( str: CodeString, constants: IConstants, expression?: boolean, depthCtx?: LispDepthCtx, ) => Lisp[]; lispifyReturnExpr: (constants: IConstants, str: CodeString) => Return; restOfExp: (( constants: IConstants, part: CodeString, tests?: RegExp[], quote?: string, firstOpening?: string, closingsTests?: RegExp[], details?: any, depth?: number, ) => CodeString) & { next: string[] }; semiColon: RegExp; setLispType: (types: T, fn: LispCallback) => void; splitByCommasDestructure: (s: string) => string[]; startingExecpted: readonly string[]; wrapLabeledStatement: (labels: string[], statement: T) => Lisp; } ================================================ FILE: src/parser/lispTypes/structures.ts ================================================ import { CodeString, isLisp, LispType, reservedWords } from '../../utils'; import type { ArrayProp, Call, CallOptional, CreateArray, CreateObject, Group, InternalCode, KeyValLisp, Lisp, PropLisp, PropOptional, SpreadArrayLisp, SpreadObjectLisp, } from '../lisp'; import type { RegisterLispTypesDeps } from './shared'; export function registerStructureLispTypes({ NullLisp, createLisp, emptyString, expectTypes, expandDestructure, findPatternEndIdx, lispify, lispifyBlock, lispifyExpr, restOfExp, setLispType, }: RegisterLispTypesDeps) { type LispWithSource = Lisp & { source?: string }; const closingsCreate: { [type: string]: RegExp } = { createArray: /^\]/, createObject: /^\}/, group: /^\)/, arrayProp: /^\]/, call: /^\)/, }; const typesCreate = { createArray: LispType.CreateArray, createObject: LispType.CreateObject, group: LispType.Group, arrayProp: LispType.ArrayProp, call: LispType.Call, prop: LispType.Prop, '?prop': LispType.PropOptional, '?call': LispType.CallOptional, } as any; setLispType(['createArray', 'createObject', 'group', 'arrayProp', 'call'] as const, (ctx) => { const { constants, type, part, res, expect } = ctx; let extract = emptyString; const arg: CodeString[] = []; const argIsHole: boolean[] = []; let end = false; let i = res[0].length; const start = i; while (i < part.length && !end) { extract = restOfExp(constants, part.substring(i), [closingsCreate[type], /^,/]); i += extract.length; if (extract.trim().length) { arg.push(extract); argIsHole.push(false); } else if (type === 'createArray' && part.char(i) === ',') { arg.push(extract); argIsHole.push(true); } if (part.char(i) !== ',') { end = true; } else { if (!extract.trim().length && type === 'call') { throw new SyntaxError('Unexpected end of expression'); } if (!extract.trim().length && type === 'createObject') { throw new SyntaxError('Unexpected token ,'); } i++; } } const next = type === 'createArray' || type === 'createObject' || type === 'group' ? ['value', 'modifier', 'prop', 'incrementerBefore', 'assignment', 'expEnd'] : ['value', 'modifier', 'prop', 'incrementerBefore', 'expEnd']; let l: Lisp | Lisp[]; let funcFound: RegExpExecArray | null; switch (type) { case 'group': { const groupContent = part.substring(start, i).trim(); const groupContentStr = groupContent.toString(); if (groupContentStr.startsWith('{') || groupContentStr.startsWith('[')) { const patternEnd = findPatternEndIdx(groupContentStr); const patternStr = groupContentStr.slice(0, patternEnd).trim(); const afterPattern = groupContentStr.slice(patternEnd).trimStart(); if (afterPattern.startsWith('=')) { const rhsStr = afterPattern.slice(1).trim(); const tempName = `$$_da${Math.random().toString(36).slice(2)}`; const expandedCode = `internal ${tempName} = (${rhsStr}); ${expandDestructure('', patternStr, tempName)}; ${tempName}`; l = createLisp({ op: LispType.InternalBlock, a: lispifyBlock(new CodeString(expandedCode), constants, false, ctx), b: LispType.None, }); break; } } } case 'arrayProp': { const arrayPropExpr = part.substring(start, i); if (!arrayPropExpr.trimStart().length) { throw new SyntaxError('Unexpected end of expression'); } l = lispifyExpr(constants, arrayPropExpr, undefined, { ...ctx, lispDepth: ctx.lispDepth + 1, }); break; } case 'call': l = arg.map((e) => lispify(constants, e, [...next, 'spreadArray'], undefined, false, ctx)); break; case 'createArray': l = arg.map((e, idx) => argIsHole[idx] ? createLisp({ op: LispType.Hole, a: LispType.None, b: LispType.None }) : lispify(constants, e, [...next, 'spreadArray'], undefined, false, ctx), ); break; case 'createObject': l = arg.map((str) => { str = str.trimStart(); let value: Lisp; let key: string | Lisp = ''; if (str.char(0) === '[') { const innerExpr = restOfExp(constants, str.substring(1), [], '['); const afterBracket = str.substring(1 + innerExpr.length + 1).trimStart(); key = lispify(constants, innerExpr, next); if (afterBracket.length > 0 && afterBracket.char(0) === ':') { value = lispify(constants, afterBracket.substring(1)); } else if (afterBracket.length > 0 && afterBracket.char(0) === '(') { value = lispify(constants, new CodeString('function' + afterBracket.toString())); } else { throw new SyntaxError('Unexpected token in computed property'); } return createLisp({ op: LispType.KeyVal, a: key, b: value, }); } funcFound = expectTypes.expFunction.types.function.exec('function ' + str); if (funcFound) { key = funcFound[3].trimStart(); value = lispify( constants, new CodeString('function ' + str.toString().replace(key, '')), ); } else { const extract = restOfExp(constants, str, [/^:/]); key = lispify(constants, extract, [...next, 'spreadObject']) as PropLisp; if (isLisp(key) && key[0] === LispType.SpreadObject) { value = NullLisp; } else { if (key[0] === LispType.Prop) { key = (key as PropLisp)[2]; } if (str.length > extract.length && str.char(extract.length) === ':') { value = lispify(constants, str.substring(extract.length + 1)); } else { value = lispify(constants, extract, next); } } } return createLisp({ op: LispType.KeyVal, a: key, b: value, }); }); break; } const lisptype = ( type === 'arrayProp' ? res[1] ? LispType.PropOptional : LispType.Prop : type === 'call' ? res[1] ? LispType.CallOptional : LispType.Call : typesCreate[type] ) as (typeof typesCreate)[keyof typeof typesCreate]; const currentTree = createLisp< ArrayProp | PropLisp | Call | CreateObject | CreateArray | Group | PropOptional | CallOptional >({ op: lisptype, a: ctx.lispTree, b: l, }) as LispWithSource; if ( lisptype === LispType.Group || lisptype === LispType.CreateArray || lisptype === LispType.CreateObject ) { currentTree.source = part.substring(start, i).toString(); } ctx.lispTree = lispify( constants, part.substring(i + 1), expectTypes[expect].next, currentTree, false, ctx, ); }); setLispType(['dot', 'prop'] as const, (ctx) => { const { constants, type, part, res, expect } = ctx; let prop = res[0]; let index = res[0].length; let op = 'prop'; if (type === 'dot') { if (res[1]) { op = '?prop'; } const matches = part.substring(res[0].length).toString().match(expectTypes.prop.types.prop); if (matches && matches.length) { prop = matches[0]; index = prop.length + res[0].length; } else { throw new SyntaxError('Hanging dot'); } } else if (reservedWords.has(prop) && prop !== 'this') { throw new SyntaxError(`Unexpected token '${prop}'`); } ctx.lispTree = lispify( constants, part.substring(index), expectTypes[expect].next, createLisp({ op: typesCreate[op], a: ctx.lispTree, b: prop, }), false, ctx, ); }); setLispType(['spreadArray', 'spreadObject'] as const, (ctx) => { const { constants, type, part, res, expect } = ctx; ctx.lispTree = createLisp({ op: type === 'spreadArray' ? LispType.SpreadArray : LispType.SpreadObject, a: LispType.None, b: lispify(constants, part.substring(res[0].length), expectTypes[expect].next), }); }); } ================================================ FILE: src/parser/lispTypes/values.ts ================================================ import { LispType } from '../../utils'; import type { Await, BigInt as BigIntLisp, GlobalSymbol, LiteralIndex, Number as NumberLisp, RegexIndex, Return, StringIndex, Throw, Void, Yield, YieldDelegate, } from '../lisp'; import type { RegisterLispTypesDeps } from './shared'; export function registerValueLispTypes({ createLisp, expectTypes, lispify, lispifyExpr, restOfExp, semiColon, setLispType, }: RegisterLispTypesDeps) { setLispType(['return', 'throw'] as const, (ctx) => { const { constants, type, part, res } = ctx; const expr = part.substring(res[0].length); if (type === 'throw' && !expr.trimStart().length) { throw new SyntaxError('Unexpected end of expression'); } ctx.lispTree = createLisp({ op: type === 'return' ? LispType.Return : LispType.Throw, a: LispType.None, b: lispifyExpr(constants, expr, undefined, ctx), }); }); setLispType(['number', 'boolean', 'null', 'und', 'NaN', 'Infinity'] as const, (ctx) => { const { constants, type, part, res, expect } = ctx; ctx.lispTree = lispify( constants, part.substring(res[0].length), expectTypes[expect].next, createLisp({ op: type === 'number' ? (res[12] ? LispType.BigInt : LispType.Number) : LispType.GlobalSymbol, a: LispType.None, b: res[12] ? res[1] : res[0], }), false, ctx, ); }); setLispType(['string', 'literal', 'regex'] as const, (ctx) => { const { constants, type, part, res, expect } = ctx; ctx.lispTree = lispify( constants, part.substring(res[0].length), expectTypes[expect].next, createLisp({ op: type === 'string' ? LispType.StringIndex : type === 'literal' ? LispType.LiteralIndex : LispType.RegexIndex, a: LispType.None, b: res[1], }), false, ctx, ); }); setLispType(['void', 'await'] as const, (ctx) => { const { constants, type, part, res, expect } = ctx; const extract = restOfExp(constants, part.substring(res[0].length), [/^([^\s.?\w$]|\?[^.])/]); if (!extract.trimStart().length) { throw new SyntaxError('Unexpected end of expression'); } ctx.lispTree = lispify( constants, part.substring(res[0].length + extract.length), expectTypes[expect].next, createLisp({ op: type === 'void' ? LispType.Void : LispType.Await, a: lispify(constants, extract), b: LispType.None, }), false, ctx, ); }); setLispType(['yield'] as const, (ctx) => { const { constants, part, res, expect, generatorDepth } = ctx; if (generatorDepth === 0) { throw new SyntaxError('Unexpected token'); } const isDelegate = res[0].trimEnd().endsWith('*'); const extract = restOfExp(constants, part.substring(res[0].length), [/^([^\s.?\w$]|\?[^.])/]); if (isDelegate && !extract.trimStart().length) { throw new SyntaxError('Unexpected end of expression'); } ctx.lispTree = lispify( constants, part.substring(res[0].length + extract.length), expectTypes[expect].next, createLisp({ op: isDelegate ? LispType.YieldDelegate : LispType.Yield, a: lispify(constants, extract), b: LispType.None, }), false, ctx, ); }); setLispType(['new'] as const, (ctx) => { const { constants, part, res } = ctx; let i = res[0].length; const obj = restOfExp(constants, part.substring(i), [], undefined, '('); if (!obj.trimStart().length) { throw new SyntaxError('Unexpected end of expression'); } i += obj.length + 1; const args = []; if (part.char(i - 1) === '(') { const argsString = restOfExp(constants, part.substring(i), [], '('); i += argsString.length + 1; let found; let j = 0; while ((found = restOfExp(constants, argsString.substring(j), [/^,/])).length) { j += found.length + 1; args.push(found.trim()); } } ctx.lispTree = lispify( constants, part.substring(i), expectTypes.expEdge.next, createLisp({ op: LispType.New, a: lispify(constants, obj, expectTypes.initialize.next), b: args.map((arg) => lispify(constants, arg, expectTypes.initialize.next)), }), false, ctx, ); }); void semiColon; } ================================================ FILE: src/parser/parserUtils.ts ================================================ import { unraw } from '../utils/unraw'; import { CodeString, isLisp, LispType, SandboxCapabilityError } from '../utils'; import { registerLispTypes } from './lispTypes'; import type { Expression, ExtractLispA, ExtractLispB, ExtractLispOp, IConstants, IExecutionTree, IRegEx, InternalCode, Labeled, Lisp, LispCallback, LispCallbackCtx, LispDepthCtx, LispFamily, LispItem, LispItemSingle, Literal, None, Return, } from './lisp'; type LispWithSource = Lisp & { source?: string }; function createLisp(obj: { op: ExtractLispOp; a: ExtractLispA; b: ExtractLispB; }) { return [obj.op, obj.a, obj.b] as L; } const NullLisp = createLisp({ op: LispType.None, a: LispType.None, b: LispType.None }); const statementLabelRegex = /([a-zA-Z$_][\w$]*)\s*:/g; function extractStatementLabels(prefix = '') { return [...prefix.matchAll(statementLabelRegex)].map((match) => match[1]); } function wrapLabeledStatement(labels: string[], statement: T): Lisp { return labels.reduceRight( (current, label) => createLisp({ op: LispType.Labeled, a: label, b: current, }), statement as Lisp, ); } const lispTypes: Map> = new Map(); export class ParseError extends Error { constructor( message: string, public code: string, ) { super(message + ': ' + code.substring(0, 40)); } } let lastType: CodeString | string; let lastPart: CodeString | string; let lastLastPart: CodeString | string; let lastLastLastPart: CodeString | string; let lastLastLastLastPart: CodeString | string; const inlineIfElse = /^:/; const elseIf = /^else(?![\w$])/; const ifElse = /^if(?![\w$])/; const space = /^\s/; export const expectTypes = { splitter: { types: { power: /^(\*\*)(?!=)/, opHigh: /^(\/|\*(?!\*)|%)(?!=)/, op: /^(\+(?!(\+))|-(?!(-)))(?!=)/, comparitor: /^(<=|>=|<(?!<)|>(?!>)|!==|!=(?!=)|===|==)/, bitwiseShift: /^(<<|>>(?!>)|>>>)(?!=)/, bitwiseAnd: /^(&(?!&))(?!=)/, bitwiseXor: /^(\^)(?!=)/, bitwiseOr: /^(\|(?!\|))(?!=)/, boolOpAnd: /^(&&)(?!=)/, boolOpOr: /^(\|\|(?!=)|instanceof(?![\w$])|in(?![\w$]))/, nullishCoalescing: /^\?\?(?!=)/, }, next: ['modifier', 'value', 'prop', 'incrementerBefore'], }, inlineIf: { types: { inlineIf: /^\?(?!\.(?!\d))/, }, next: ['expEnd'], }, assignment: { types: { assignModify: /^(-=|\+=|\/=|\*\*=|\*=|%=|\^=|&=|\|=|>>>=|>>=|<<=|&&=|\|\|=|\?\?=)/, assign: /^(=)(?!=)/, }, next: ['modifier', 'value', 'prop', 'incrementerBefore'], }, incrementerBefore: { types: { incrementerBefore: /^(\+\+|--)/ }, next: ['prop'], }, expEdge: { types: { call: /^(\?\.)?[(]/, incrementerAfter: /^(\+\+|--)/, taggedTemplate: /^`(\d+)`/, }, next: ['splitter', 'assignment', 'expEdge', 'dot', 'inlineIf', 'expEnd'], }, modifier: { types: { not: /^!/, inverse: /^~/, negative: /^-(?!-)/, positive: /^\+(?!\+)/, typeof: /^typeof(?![\w$])/, delete: /^delete(?![\w$])/, }, next: ['modifier', 'value', 'prop', 'incrementerBefore'], }, dot: { types: { arrayProp: /^(\?\.)?\[/, dot: /^(\?)?\.(?=\s*[a-zA-Z$_])/, }, next: ['splitter', 'assignment', 'expEdge', 'dot', 'inlineIf', 'expEnd'], }, prop: { types: { prop: /^[a-zA-Z$_][a-zA-Z\d$_]*/, }, next: ['splitter', 'assignment', 'expEdge', 'dot', 'inlineIf', 'expEnd'], }, value: { types: { createObject: /^\{/, createArray: /^\[/, number: /^(0b[01]+(_[01]+)*|0o[0-7]+(_[0-7]+)*|0x[\da-f]+(_[\da-f]+)*|(\d+(_\d+)*(\.\d+(_\d+)*)?|\.\d+(_\d+)*))(e[+-]?\d+(_\d+)*)?(n)?(?!\d)/i, string: /^"(\d+)"/, literal: /^`(\d+)`/, regex: /^\/(\d+)\/r(?![\w$])/, boolean: /^(true|false)(?![\w$])/, null: /^null(?![\w$])/, und: /^undefined(?![\w$])/, arrowFunctionSingle: /^(async\s+)?([a-zA-Z$_][a-zA-Z\d$_]*)\s*=>\s*({)?/, arrowFunction: /^(async\s*)?\(\s*([^)(]*?)\s*\)\s*=>\s*({)?/, inlineFunction: /^(async\s+)?function(\*\s*|\s*)([a-zA-Z$_][a-zA-Z\d$_]*)?\s*\(\s*/, yield: /^yield\*(?![\w$])\s*|^yield(?![\w$])\s*/, group: /^\(/, NaN: /^NaN(?![\w$])/, Infinity: /^Infinity(?![\w$])/, void: /^void(?![\w$])\s*/, await: /^await(?![\w$])\s*/, new: /^new(?![\w$])\s*/, }, next: ['splitter', 'expEdge', 'dot', 'inlineIf', 'expEnd'], }, initialize: { types: { initializeDestructure: /^(var|let|const|internal)\s+([{[])/, initialize: /^(var|let|const|internal)\s+([a-zA-Z$_][a-zA-Z\d$_]*)\s*(=)?/, return: /^return(?![\w$])/, throw: /^throw(?![\w$])\s*/, }, next: ['modifier', 'value', 'prop', 'incrementerBefore', 'expEnd'], }, spreadObject: { types: { spreadObject: /^\.\.\./, }, next: ['value', 'prop'], }, spreadArray: { types: { spreadArray: /^\.\.\./, }, next: ['value', 'prop'], }, expEnd: { types: {}, next: [] }, expFunction: { types: { function: /^(async\s+)?function(\*\s*|\s+)([a-zA-Z$_][a-zA-Z\d$_]*)\s*\(\s*/, }, next: ['expEdge', 'expEnd'], }, expSingle: { types: { for: /^((?:[a-zA-Z$_][\w$]*\s*:\s*)*)\s*for(\s+await)?\s*\(/, do: /^((?:[a-zA-Z$_][\w$]*\s*:\s*)*)\s*do(?![\w$])\s*(\{)?/, while: /^((?:[a-zA-Z$_][\w$]*\s*:\s*)*)\s*while\s*\(/, loopAction: /^(break|continue)(?![\w$])\s*([a-zA-Z$_][\w$]*)?/, if: /^((?:[a-zA-Z$_][\w$]*\s*:\s*)*)\s*if\s*\(/, try: /^((?:[a-zA-Z$_][\w$]*\s*:\s*)*)\s*try\s*{/, block: /^((?:[a-zA-Z$_][\w$]*\s*:\s*)*)\s*{/, switch: /^((?:[a-zA-Z$_][\w$]*\s*:\s*)*)\s*switch\s*\(/, }, next: ['expEnd'], }, } as Record; next: string[] }>; const closings = { '(': ')', '[': ']', '{': '}', "'": "'", '"': '"', '`': '`', } as Record; export function testMultiple(str: string, tests: RegExp[]) { let found: RegExpExecArray | null = null; for (let i = 0; i < tests.length; i++) { const test = tests[i]; found = test.exec(str); if (found) break; } return found; } const emptyString = new CodeString(''); const okFirstChars = /^[+\-~ !]/; const aNumber = expectTypes.value.types.number; const wordReg = /^((if|for|else|while|do|function)(?![\w$])|[\w$]+)/; const semiColon = /^;/; const insertedSemicolons: WeakMap<{ str: string }, Array> = new WeakMap(); const quoteCache: WeakMap<{ str: string }, Map> = new WeakMap(); export interface restDetails { oneliner?: boolean; words?: string[]; lastWord?: string; lastAnyWord?: string; regRes?: RegExpExecArray; bodyContentAfterKeyword?: boolean; } export function restOfExp( constants: IConstants, part: CodeString, tests?: RegExp[], quote?: string, firstOpening?: string, closingsTests?: RegExp[], details: restDetails = {}, depth = 0, ): CodeString { if (!part.length) { return part; } if (depth > constants.maxDepth) { throw new SandboxCapabilityError('Maximum expression depth exceeded'); } details.words = details.words || []; let isStart = true; tests = tests || []; const hasSemiTest = tests.includes(semiColon); if (hasSemiTest) { tests = tests.filter((a) => a !== semiColon); } const insertedSemis = insertedSemicolons.get(part.ref) || []; const cache = quoteCache.get(part.ref) || new Map(); quoteCache.set(part.ref, cache); if (quote && cache.has(part.start - 1)) { return part.substring(0, cache.get(part.start - 1)! - part.start); } let escape = false; let done = false; let lastChar = ''; let isOneLiner = false; let i; let lastInertedSemi = false; let seenKeyword = false; let skipNextWord = false; for (i = 0; i < part.length && !done; i++) { let char = part.char(i)!; if (quote === '"' || quote === "'" || quote === '`') { if (quote === '`' && char === '$' && part.char(i + 1) === '{' && !escape) { const skip = restOfExp( constants, part.substring(i + 2), [], '{', undefined, undefined, {}, depth + 1, ); i += skip.length + 2; } else if (char === quote && !escape) { return part.substring(0, i); } escape = !escape && char === '\\'; } else if (closings[char]) { if (!lastInertedSemi && insertedSemis[i + part.start]) { lastInertedSemi = true; if (hasSemiTest) { break; } i--; lastChar = ';'; continue; } if (isOneLiner && char === '{') { isOneLiner = false; } if (char === firstOpening) { done = true; break; } else { const skip = restOfExp( constants, part.substring(i + 1), [], char, undefined, undefined, {}, depth + 1, ); cache.set(skip.start - 1, skip.end); i += skip.length + 1; isStart = false; if (closingsTests) { const sub = part.substring(i); let found: RegExpExecArray | null; if ((found = testMultiple(sub.toString(), closingsTests))) { details.regRes = found; done = true; } } } } else if (!quote) { let sub = part.substring(i).toString(); let foundWord: RegExpExecArray | null; let foundNumber: RegExpExecArray | null; if (closingsTests) { let found: RegExpExecArray | null; if ((found = testMultiple(sub, closingsTests))) { details.regRes = found; i++; done = true; break; } } if ((foundNumber = aNumber.exec(sub))) { i += foundNumber[0].length - 1; sub = part.substring(i).toString(); if (closingsTests) { let found: RegExpExecArray | null; if ((found = testMultiple(sub, closingsTests))) { details.regRes = found; i++; done = true; break; } } } else if (lastChar != char) { let found: [string] | RegExpExecArray | null = null; if (char === ';' || (insertedSemis[i + part.start] && !isStart && !lastInertedSemi)) { if (hasSemiTest) { found = [';']; } else if (insertedSemis[i + part.start]) { lastInertedSemi = true; i--; lastChar = ';'; continue; } char = sub = ';'; } else { lastInertedSemi = false; } if (!found) { found = testMultiple(sub, tests); } if (found) { done = true; } if (!done && (foundWord = wordReg.exec(sub))) { isOneLiner = true; if (foundWord[2]) { seenKeyword = true; skipNextWord = true; } else if (seenKeyword) { if (skipNextWord) { skipNextWord = false; } else { details.bodyContentAfterKeyword = true; } } if (foundWord[0].length > 1) { details.words.push(foundWord[1]); details.lastAnyWord = foundWord[1]; if (foundWord[2]) { details.lastWord = foundWord[2]; } } if (foundWord[0].length > 2) { i += foundWord[0].length - 2; } } } if (isStart) { if (okFirstChars.test(sub)) { done = false; } else { isStart = false; } } if (done) break; } else if (char === closings[quote]) { return part.substring(0, i); } lastChar = char; } if (quote) { throw new SyntaxError("Unclosed '" + quote + "'"); } if (details) { details.oneliner = isOneLiner; } return part.substring(0, i); } restOfExp.next = ['splitter', 'expEnd', 'inlineIf']; const startingExecpted = [ 'initialize', 'expSingle', 'expFunction', 'value', 'modifier', 'prop', 'incrementerBefore', 'expEnd', ]; export const setLispType = (types: T, fn: LispCallback) => { types.forEach((type) => { lispTypes.set(type, fn); }); }; registerLispTypes({ NullLisp, ParseError, createLisp, emptyString, expectTypes, expandDestructure, expandFunctionParamDestructure, extractStatementLabels, findPatternEndIdx, getDestructurePatternSource, insertSemicolons, lispify, lispifyBlock, lispifyExpr, lispifyFunction, lispifyReturnExpr, restOfExp, semiColon, setLispType, splitByCommasDestructure, startingExecpted, wrapLabeledStatement, }); function splitByCommasDestructure(s: string): string[] { const parts: string[] = []; let depth = 0; let cur = ''; for (let i = 0; i < s.length; i++) { const c = s[i]; if (c === '[' || c === '{' || c === '(') depth++; else if (c === ']' || c === '}' || c === ')') depth--; if (c === ',' && depth === 0) { parts.push(cur); cur = ''; } else { cur += c; } } parts.push(cur); return parts; } function findFirstAtTopLevel(s: string, ch: string): number { let depth = 0; for (let i = 0; i < s.length; i++) { const c = s[i]; if (c === '[' || c === '{' || c === '(') depth++; else if (c === ']' || c === '}' || c === ')') depth--; if (c === ch && depth === 0) return i; } return -1; } function findPatternEndIdx(s: string): number { let depth = 0; for (let i = 0; i < s.length; i++) { const c = s[i]; if (c === '[' || c === '{') depth++; else if (c === ']' || c === '}') { depth--; if (depth === 0) return i + 1; } } return s.length; } const validIdentifier = /^[a-zA-Z$_][a-zA-Z\d$_]*$/; function assertIdentifier(name: string): string { if (!validIdentifier.test(name)) throw new SyntaxError(`Invalid destructuring target: '${name}'`); return name; } function expandDestructure(keyword: string, patternStr: string, rhsStr: string): string { const stmts: string[] = []; function genTemp(): string { return `$$_d${Math.random().toString(36).slice(2)}`; } function processPattern(pattern: string, src: string) { pattern = pattern.trim(); if (pattern.startsWith('[')) { processArrayPattern(pattern.slice(1, -1), src); } else if (pattern.startsWith('{')) { processObjectPattern(pattern.slice(1, -1), src); } } function processArrayPattern(content: string, src: string) { const elements = splitByCommasDestructure(content); for (let i = 0; i < elements.length; i++) { const elem = elements[i].trim(); if (!elem) continue; if (elem.startsWith('...')) { const rest = elem.slice(3).trim(); if (rest.startsWith('[') || rest.startsWith('{')) { const t = genTemp(); stmts.push(`internal ${t} = ${src}.slice(${i})`); processPattern(rest, t); } else { stmts.push(`${keyword} ${assertIdentifier(rest)} = ${src}.slice(${i})`); } break; } const eqIdx = findFirstAtTopLevel(elem, '='); const target = eqIdx !== -1 ? elem.slice(0, eqIdx).trim() : elem.trim(); const defaultVal = eqIdx !== -1 ? elem.slice(eqIdx + 1).trim() : undefined; if (target.startsWith('[') || target.startsWith('{')) { const t = genTemp(); stmts.push( defaultVal !== undefined ? `internal ${t} = ${src}[${i}] !== undefined ? ${src}[${i}] : (${defaultVal})` : `internal ${t} = ${src}[${i}]`, ); processPattern(target, t); } else { stmts.push( defaultVal !== undefined ? `${keyword} ${assertIdentifier(target)} = ${src}[${i}] !== undefined ? ${src}[${i}] : (${defaultVal})` : `${keyword} ${assertIdentifier(target)} = ${src}[${i}]`, ); } } } function processObjectPattern(content: string, src: string) { const props = splitByCommasDestructure(content); const usedKeys: string[] = []; for (const prop of props) { const p = prop.trim(); if (!p) continue; if (p.startsWith('...')) { // Check that rest is the last element const restIdx = props.indexOf(prop); const hasMore = props.slice(restIdx + 1).some((pp) => pp.trim().length > 0); if (hasMore) { throw new SyntaxError('Rest element must be last element'); } const rest = p.slice(3).trim(); const exclTemp = genTemp(); const keyTemp = genTemp(); const resTemp = genTemp(); const exclEntries = usedKeys.map((k) => `${assertIdentifier(k)}:1`).join(','); stmts.push(`internal ${exclTemp} = {${exclEntries}}`); stmts.push(`internal ${resTemp} = {}`); stmts.push( `for (internal ${keyTemp} in ${src}) { if (!(${keyTemp} in ${exclTemp})) { ${resTemp}[${keyTemp}] = ${src}[${keyTemp}] } }`, ); if (rest.startsWith('[') || rest.startsWith('{')) { processPattern(rest, resTemp); } else { stmts.push(`${keyword} ${assertIdentifier(rest)} = ${resTemp}`); } break; } if (p.startsWith('[')) { // Computed property name: [expr]: target let closeBracket = -1; let depth = 1; for (let ci = 1; ci < p.length; ci++) { if (p[ci] === '[') depth++; else if (p[ci] === ']') { depth--; if (depth === 0) { closeBracket = ci; break; } } } if (closeBracket !== -1) { const computedExpr = p.slice(1, closeBracket); const after = p.slice(closeBracket + 1).trim(); if (after.startsWith(':')) { const valueStr = after.slice(1).trim(); const accessor = `${src}[${computedExpr}]`; const eqIdx = findFirstAtTopLevel(valueStr, '='); const tgt = eqIdx !== -1 && !valueStr.slice(0, eqIdx).trim().match(/^[[{]/) ? valueStr.slice(0, eqIdx).trim() : valueStr.trim(); const defVal = eqIdx !== -1 && !valueStr.slice(0, eqIdx).trim().match(/^[[{]/) ? valueStr.slice(eqIdx + 1).trim() : undefined; if (tgt.startsWith('[') || tgt.startsWith('{')) { const t = genTemp(); stmts.push( defVal !== undefined ? `internal ${t} = ${accessor} !== undefined ? ${accessor} : (${defVal})` : `internal ${t} = ${accessor}`, ); processPattern(tgt, t); } else { stmts.push( defVal !== undefined ? `${keyword} ${assertIdentifier(tgt)} = ${accessor} !== undefined ? ${accessor} : (${defVal})` : `${keyword} ${assertIdentifier(tgt)} = ${accessor}`, ); } } } continue; } const colonIdx = findFirstAtTopLevel(p, ':'); if (colonIdx !== -1) { const key = assertIdentifier(p.slice(0, colonIdx).trim()); const valueStr = p.slice(colonIdx + 1).trim(); usedKeys.push(key); const accessor = `${src}.${key}`; const eqIdx = findFirstAtTopLevel(valueStr, '='); const beforeEq = eqIdx !== -1 ? valueStr.slice(0, eqIdx).trim() : valueStr.trim(); const isNestedPattern = beforeEq.startsWith('[') || beforeEq.startsWith('{'); const tgt = isNestedPattern ? valueStr.trim() : beforeEq; const defVal = !isNestedPattern && eqIdx !== -1 ? valueStr.slice(eqIdx + 1).trim() : undefined; if (tgt.startsWith('[') || tgt.startsWith('{')) { const patEnd = findPatternEndIdx(tgt); const finalPattern = tgt.slice(0, patEnd); const afterPat = tgt.slice(patEnd).trim(); const nestedDef = afterPat.startsWith('=') ? afterPat.slice(1).trim() : undefined; const t = genTemp(); stmts.push( nestedDef !== undefined ? `internal ${t} = ${accessor} !== undefined ? ${accessor} : (${nestedDef})` : `internal ${t} = ${accessor}`, ); processPattern(finalPattern, t); } else { stmts.push( defVal !== undefined ? `${keyword} ${assertIdentifier(tgt)} = ${accessor} !== undefined ? ${accessor} : (${defVal})` : `${keyword} ${assertIdentifier(tgt)} = ${accessor}`, ); } continue; } const eqIdx = findFirstAtTopLevel(p, '='); if (eqIdx !== -1) { const name = assertIdentifier(p.slice(0, eqIdx).trim()); const defaultVal = p.slice(eqIdx + 1).trim(); usedKeys.push(name); stmts.push( `${keyword} ${name} = ${src}.${name} !== undefined ? ${src}.${name} : (${defaultVal})`, ); } else { assertIdentifier(p); usedKeys.push(p); stmts.push(`${keyword} ${p} = ${src}.${p}`); } } } const rootTemp = genTemp(); stmts.unshift(`var ${rootTemp} = (${rhsStr})`); processPattern(patternStr, rootTemp); return stmts.join('; '); } function getDestructurePatternSource(tree: LispItem): string | null { if (!isLisp(tree)) return null; const source = (tree as LispWithSource).source?.trim(); if (source && (source.startsWith('[') || source.startsWith('{'))) { return source; } if (tree[0] === LispType.Group) { return getDestructurePatternSource(tree[2]); } return null; } function expandFunctionParamDestructure( args: string[], funcBody: string, ): { args: string[]; body: string } { const injected: string[] = []; const newArgs = args.map((arg, i) => { const a = arg.trim(); if (a.startsWith('[') || a.startsWith('{')) { const tempName = `$$_p${i}`; // Check for a parameter default: {pattern} = defaultVal or [pattern] = defaultVal const patEnd = findPatternEndIdx(a); const patternOnly = a.slice(0, patEnd); const afterPat = a.slice(patEnd).trim(); if (afterPat.startsWith('=')) { const defaultVal = afterPat.slice(1).trim(); // Use temp var that applies the default, then destructure from it injected.push( expandDestructure( 'const', patternOnly, `${tempName} !== undefined ? ${tempName} : (${defaultVal})`, ), ); } else { injected.push(expandDestructure('const', patternOnly, tempName)); } return tempName; } // Handle simple default: a = defaultVal or ...rest (no default) const eqIdx = findFirstAtTopLevel(a, '='); if (eqIdx !== -1 && !a.startsWith('...')) { const paramName = a.slice(0, eqIdx).trim(); const defaultVal = a.slice(eqIdx + 1).trim(); injected.push(`if (${paramName} === undefined) ${paramName} = (${defaultVal})`); return paramName; } return a; }); if (injected.length === 0) return { args: newArgs, body: funcBody }; return { args: newArgs, body: injected.join('; ') + '; ' + funcBody }; } function lispify( constants: IConstants, part: CodeString, expected?: readonly string[], lispTree?: Lisp, topLevel = false, depthCtx: LispDepthCtx = { generatorDepth: 0, asyncDepth: 0, lispDepth: 0 }, ): Lisp { if (depthCtx.lispDepth > constants.maxDepth) { throw new SandboxCapabilityError('Maximum expression depth exceeded'); } const { generatorDepth, asyncDepth, lispDepth } = depthCtx; lispTree = lispTree || NullLisp; expected = expected || expectTypes.initialize.next; if (part === undefined) return lispTree; part = part.trimStart(); const str = part.toString(); if (!part.length && !expected.includes('expEnd')) { throw new SyntaxError('Unexpected end of expression'); } if (!part.length) return lispTree; const ctx: LispCallbackCtx = { constants, type: '', part, res: [], expect: '', lispTree, generatorDepth, asyncDepth, lispDepth, }; let res: any; for (const expect of expected) { if (expect === 'expEnd') { continue; } for (const type in expectTypes[expect].types) { if (type === 'expEnd') { continue; } if ((res = expectTypes[expect].types[type].exec(str))) { lastType = type; lastLastLastLastPart = lastLastLastPart; lastLastLastPart = lastLastPart; lastLastPart = lastPart; lastPart = part; ctx.type = type; ctx.part = part; ctx.res = res; ctx.expect = expect; try { lispTypes.get(type)?.(ctx as LispCallbackCtx & { type: string }); } catch (e) { if (topLevel && e instanceof SyntaxError) { throw new ParseError(e.message, str); } throw e; } break; } } if (res) break; } if (!res && part.length) { if (topLevel) { throw new ParseError(`Unexpected token after ${lastType}: ${part.char(0)}`, str); } throw new SyntaxError(`Unexpected token after ${lastType}: ${part.char(0)}`); } return ctx.lispTree; } const startingExpectedWithoutSingle = startingExecpted.filter((r) => r !== 'expSingle'); function lispifyExpr( constants: IConstants, str: CodeString, expected?: readonly string[], depthCtx: LispDepthCtx = { generatorDepth: 0, asyncDepth: 0, lispDepth: 0 }, ): Lisp { if (depthCtx.lispDepth > constants.maxDepth) { throw new SandboxCapabilityError('Maximum expression depth exceeded'); } if (!str.trimStart().length) return NullLisp; const subExpressions: CodeString[] = []; let sub: CodeString; let pos = 0; expected = expected || expectTypes.initialize.next; if (expected.includes('expSingle')) { if (testMultiple(str.toString(), Object.values(expectTypes.expSingle.types))) { return lispify(constants, str, ['expSingle'], undefined, true, depthCtx); } } if (expected === startingExecpted) expected = startingExpectedWithoutSingle; while ((sub = restOfExp(constants, str.substring(pos), [/^,/])).length) { subExpressions.push(sub.trimStart()); pos += sub.length + 1; } if (subExpressions.length === 1) { return lispify(constants, str, expected, undefined, true, depthCtx); } if (expected.includes('initialize')) { const defined = expectTypes.initialize.types.initialize.exec(subExpressions[0].toString()); if (defined) { return createLisp({ op: LispType.InternalBlock, a: subExpressions.map((str, i) => lispify( constants, i ? new CodeString(defined![1] + ' ' + str) : str, ['initialize'], undefined, true, depthCtx, ), ), b: LispType.None, }); } else if (expectTypes.initialize.types.return.exec(subExpressions[0].toString())) { return lispify(constants, str, expected, undefined, true, depthCtx); } } const exprs = subExpressions.map((str) => lispify(constants, str, expected, undefined, true, depthCtx), ); return createLisp({ op: LispType.Expression, a: exprs, b: LispType.None }); } export function lispifyReturnExpr(constants: IConstants, str: CodeString) { return createLisp({ op: LispType.Return, a: LispType.None, b: lispifyExpr(constants, str), }); } export function lispifyBlock( str: CodeString, constants: IConstants, expression = false, depthCtx: LispDepthCtx = { generatorDepth: 0, asyncDepth: 0, lispDepth: 0 }, ): Lisp[] { str = insertSemicolons(constants, str); if (!str.trim().length) return []; const parts: CodeString[] = []; let part: CodeString; let pos = 0; let start = 0; let details: restDetails = {}; let skipped = false; let isInserted = false; while ( (part = restOfExp( constants, str.substring(pos), [semiColon], undefined, undefined, undefined, details, )).length ) { isInserted = !!(str.char(pos + part.length) && str.char(pos + part.length) !== ';'); pos += part.length + (isInserted ? 0 : 1); if (/^\s*else(?![\w$])/.test(str.substring(pos).toString())) { skipped = true; } else if ( details['words']?.includes('do') && /^\s*while(?![\w$])/.test(str.substring(pos).toString()) ) { skipped = true; } else { skipped = false; parts.push(str.substring(start, pos - (isInserted ? 0 : 1))); start = pos; } details = {}; if (expression) break; } if (skipped) { parts.push(str.substring(start, pos - (isInserted ? 0 : 1))); } return parts .map((str) => str.trimStart()) .filter((str) => str.length) .map((str) => { return lispifyExpr(constants, str.trimStart(), startingExecpted, depthCtx); }); } export function lispifyFunction( str: CodeString, constants: IConstants, expression = false, depthCtx: LispDepthCtx = { generatorDepth: 0, asyncDepth: 0, lispDepth: 0 }, ): Lisp[] { if (!str.trim().length) return []; const tree = lispifyBlock(str, constants, expression, depthCtx); hoist(tree); return tree; } function hoist(item: LispItem, res: Lisp[] = []): boolean { if (isLisp(item)) { if (!isLisp(item)) return false; const [op, a, b] = item; if ( op === LispType.Labeled || op === LispType.Try || op === LispType.If || op === LispType.Loop || op === LispType.Switch ) { hoist(a, res); hoist(b, res); } else if (op === LispType.Var) { res.push(createLisp({ op: LispType.Var, a: a, b: LispType.None })); } else if (op === LispType.Function && a[2]) { res.push(item); return true; } } else if (Array.isArray(item)) { const rep: LispItemSingle[] = []; for (const it of item) { if (!hoist(it, res)) { rep.push(it); } } if (rep.length !== item.length) { item.length = 0; item.push(...res, ...rep); } } return false; } const closingsNoInsertion = /^(\})\s*(catch|finally|else|while|instanceof)(?![\w$])/; // \w|)|] \n \w = 2 // \} \w|\{ = 5 const colonsRegex = /^((([\w$\])"'`]|\+\+|--)\s*\r?\n\s*([\w$+\-!~]))|(\}\s*[\w$!~+\-{("'`]))/; // if () \w \n; \w == \w \n \w | last === if a // if () { }; \w == \} ^else | last === if b // if () \w \n; else \n \w \n; == \w \n \w | last === else a // if () {} else {}; \w == \} \w | last === else b // while () \n \w \n; \w == \w \n \w | last === while a // while () { }; \w == \} \w | last === while b // do \w \n; while (); \w == \w \n while | last === do a // do { } while (); \w == \) \w | last === while c // try {} catch () {}; \w == \} \w | last === catch|finally b // \w \n; \w == \w \n \w | last === none a // cb() \n \w == \) \n \w | last === none a // obj[a] \n \w == \] \n \w | last === none a // {} {} == \} \{ | last === none b export function insertSemicolons(constants: IConstants, str: CodeString): CodeString { let rest = str; let sub = emptyString; let details: restDetails = {}; let pendingDoWhile = false; const inserted = insertedSemicolons.get(str.ref) || new Array(str.ref.str.length); while ( (sub = restOfExp(constants, rest, [], undefined, undefined, [colonsRegex], details)).length ) { let valid = false; let part = sub; let edge = sub.length; if (details.regRes) { valid = true; const [, , a, , , b] = details.regRes; edge = details.regRes[3] === '++' || details.regRes[3] === '--' ? sub.length + 1 : sub.length; part = rest.substring(0, edge); if (b) { const res = closingsNoInsertion.exec(rest.substring(sub.length - 1).toString()); if (res) { if (res[2] === 'while') { if (details.lastWord === 'do') { valid = false; pendingDoWhile = true; } else { valid = true; } } else { valid = false; } } else if ( details.lastWord === 'function' && details.regRes[5][0] === '}' && details.regRes[5].slice(-1) === '(' ) { valid = false; } } else if (a) { if (pendingDoWhile && details.lastWord === 'while') { valid = true; pendingDoWhile = false; } else if ( details.lastWord === 'if' || details.lastWord === 'while' || details.lastWord === 'for' || details.lastWord === 'else' ) { valid = !!details.bodyContentAfterKeyword; } } } if (valid) { inserted[part.end] = true; } rest = rest.substring(edge); details = {}; } insertedSemicolons.set(str.ref, inserted); return str; } export function checkRegex(str: string): IRegEx | null { let i = 1; let escape = false; let done = false; let cancel = false; while (i < str.length && !done && !cancel) { done = str[i] === '/' && !escape; escape = str[i] === '\\' && !escape; cancel = str[i] === '\n'; i++; } const after = str.substring(i); cancel = cancel || !done || /^\s*\d/.test(after); if (cancel) return null; const flags = /^[a-z]*/.exec(after); if (/^\s+[\w$]/.test(str.substring(i + flags![0].length))) { return null; } const regexPattern = str.substring(1, i - 1); const regexFlags = (flags && flags[0]) || ''; try { new RegExp(regexPattern, regexFlags); } catch (e) { if (e instanceof SyntaxError) throw e; } return { regex: regexPattern, flags: regexFlags, length: i + ((flags && flags[0].length) || 0), }; } const notDivide = /(typeof|delete|instanceof|return|in|of|throw|new|void|do|if)$/; const possibleDivide = /^([\w$\])]|\+\+|--)[\s/]/; export function extractConstants( constants: IConstants, str: string, currentEnclosure = '', depth = 0, ): { str: string; length: number } { if (depth > constants.maxDepth) { throw new SandboxCapabilityError('Maximum expression depth exceeded'); } let quote; let extract: (string | number)[] = []; let escape = false; let regexFound: IRegEx | null; let comment = ''; let commentStart = -1; let currJs: string[] = []; let char = ''; const strRes: (string | number)[] = []; const enclosures: string[] = []; let isPossibleDivide: RegExpExecArray | null = null; let i = 0; for (i = 0; i < str.length; i++) { char = str[i]; if (comment) { if (char === comment) { if (comment === '*' && str[i + 1] === '/') { comment = ''; i++; } else if (comment === '\n') { comment = ''; strRes.push('\n'); } } } else { if (escape) { escape = false; extract.push(char); continue; } if (quote) { if (quote === '`' && char === '$' && str[i + 1] === '{') { const skip = extractConstants(constants, str.substring(i + 2), '{', depth + 1); if (!skip.str.trim().length) { throw new SyntaxError('Unexpected end of expression'); } currJs.push(skip.str); extract.push('${', currJs.length - 1, `}`); i += skip.length + 2; } else if (quote === char) { if (quote === '`') { const li = createLisp({ op: LispType.Literal, a: unraw(extract.join('')), b: [], }); li.tempJsStrings = currJs; constants.literals.push(li); strRes.push(`\``, constants.literals.length - 1, `\``); } else { constants.strings.push(unraw(extract.join(''))); strRes.push(`"`, constants.strings.length - 1, `"`); } quote = null; extract = []; } else { extract.push(char); } } else { if (char === "'" || char === '"' || char === '`') { currJs = []; quote = char; } else if (closings[currentEnclosure] === char && !enclosures.length) { return { str: strRes.join(''), length: i }; } else if (closings[char]) { enclosures.push(char); strRes.push(char); } else if (closings[enclosures[enclosures.length - 1]] === char) { enclosures.pop(); strRes.push(char); } else if (char === '/' && (str[i + 1] === '*' || str[i + 1] === '/')) { comment = str[i + 1] === '*' ? '*' : '\n'; commentStart = i; } else if ( char === '/' && !isPossibleDivide && (regexFound = checkRegex(str.substring(i))) ) { constants.regexes.push(regexFound); strRes.push(`/`, constants.regexes.length - 1, `/r`); i += regexFound.length - 1; } else { strRes.push(char); } if (!isPossibleDivide || !space.test(char)) { if ((isPossibleDivide = possibleDivide.exec(str.substring(i)))) { if (notDivide.test(str.substring(0, i + isPossibleDivide[1].length))) { isPossibleDivide = null; } } } } escape = !!(quote && char === '\\'); } } if (quote) { throw new SyntaxError(`Unclosed '${quote}'`); } if (comment) { if (comment === '*') { throw new SyntaxError(`Unclosed comment '/*': ${str.substring(commentStart)}`); } } return { str: strRes.join(''), length: i }; } export default function parse( code: string, eager = false, expression = false, maxParserRecursionDepth = 256, ): IExecutionTree { if (typeof code !== 'string') throw new ParseError(`Cannot parse ${code}`, String(code)); let str = ' ' + code; const constants: IConstants = { strings: [], literals: [], regexes: [], eager, maxDepth: maxParserRecursionDepth, }; str = extractConstants(constants, str).str; for (const l of constants.literals) { l[2] = l.tempJsStrings!.map((js: string) => lispifyExpr(constants, new CodeString(js))); delete l.tempJsStrings; } return { tree: lispifyFunction(new CodeString(str), constants, expression), constants }; } ================================================ FILE: src/utils/CodeString.ts ================================================ export class CodeString { start: number; end: number; ref: { str: string }; constructor(str: string | CodeString) { this.ref = { str: '' }; if (str instanceof CodeString) { this.ref = str.ref; this.start = str.start; this.end = str.end; } else { this.ref.str = str; this.start = 0; this.end = str.length; } } substring(start: number, end?: number): CodeString { if (!this.length) return this; start = this.start + start; if (start < 0) { start = 0; } if (start > this.end) { start = this.end; } end = end === undefined ? this.end : this.start + end; if (end < 0) { end = 0; } if (end > this.end) { end = this.end; } const code = new CodeString(this); code.start = start; code.end = end; return code; } get length() { const len = this.end - this.start; return len < 0 ? 0 : len; } char(i: number) { if (this.start === this.end) return undefined; return this.ref.str[this.start + i]; } toString() { return this.ref.str.substring(this.start, this.end); } trimStart() { const found = /^\s+/.exec(this.toString()); const code = new CodeString(this); if (found) { code.start += found[0].length; } return code; } slice(start: number, end?: number) { if (start < 0) { start = this.end - this.start + start; } if (start < 0) { start = 0; } if (end === undefined) { end = this.end - this.start; } if (end < 0) { end = this.end - this.start + end; } if (end < 0) { end = 0; } return this.substring(start, end); } trim() { const code = this.trimStart(); const found = /\s+$/.exec(code.toString()); if (found) { code.end -= found[0].length; } return code; } valueOf() { return this.toString(); } } ================================================ FILE: src/utils/ExecContext.ts ================================================ import type { IEvalContext } from '../eval'; import type { Change } from '../executor'; import { DEFAULT_FUNCTION_REPLACEMENTS } from './functionReplacements'; import type { IConstants, IExecutionTree, Lisp, LispItem } from '../parser'; import type SandboxExec from '../SandboxExec'; import { AsyncFunction, GeneratorFunction, AsyncGeneratorFunction, NON_BLOCKING_THRESHOLD, LispType, type IContext, type IExecContext, type IOptions, type ISymbolWhitelist, type ISandboxGlobal, type SandboxSymbolContext, type SubscriptionSubject, } from './types'; import { Scope } from './Scope'; import { hasOwnProperty } from './Prop'; export class ExecContext implements IExecContext { constructor( public ctx: IContext, public constants: IConstants, public tree: Lisp[], public getSubscriptions: Set<(obj: SubscriptionSubject, name: string) => void>, public setSubscriptions: WeakMap< SubscriptionSubject, Map void>> >, public changeSubscriptions: WeakMap void>>, public setSubscriptionsGlobal: WeakMap< SubscriptionSubject, Map void>> >, public changeSubscriptionsGlobal: WeakMap< SubscriptionSubject, Set<(modification: Change) => void> >, public evals: Map, public registerSandboxFunction: (fn: (...args: any[]) => any) => void, public allowJit: boolean, public evalContext?: IEvalContext, ) {} } function createSandboxSymbolContext(symbolWhitelist: ISymbolWhitelist): SandboxSymbolContext { return { registry: new Map(), reverseRegistry: new Map(), whitelist: { ...symbolWhitelist }, }; } const RESERVED_SYMBOL_PROPERTIES = new Set(['length', 'name', 'prototype', 'for', 'keyFor']); function copyWhitelistedSymbols(target: Function, symbolWhitelist: ISymbolWhitelist) { for (const [key, value] of Object.entries(symbolWhitelist)) { if (RESERVED_SYMBOL_PROPERTIES.has(key)) continue; const descriptor = Object.getOwnPropertyDescriptor(Symbol, key); if (descriptor) { Object.defineProperty(target, key, descriptor); } } } export function getSandboxSymbolCtor(symbols: SandboxSymbolContext) { if (symbols.ctor) { return symbols.ctor; } function SandboxSymbol(this: unknown, description?: unknown) { if (new.target) { throw new TypeError('Symbol is not a constructor'); } return Symbol(description === undefined ? undefined : String(description)); } copyWhitelistedSymbols(SandboxSymbol, symbols.whitelist); Object.defineProperties(SandboxSymbol, { prototype: { value: Symbol.prototype, enumerable: false, configurable: false, writable: false, }, for: { value(key: unknown) { const stringKey = String(key); let symbol = symbols.registry.get(stringKey); if (!symbol) { symbol = Symbol(stringKey); symbols.registry.set(stringKey, symbol); symbols.reverseRegistry.set(symbol, stringKey); } return symbol; }, enumerable: false, configurable: true, writable: true, }, keyFor: { value(symbol: unknown) { return typeof symbol === 'symbol' ? symbols.reverseRegistry.get(symbol) : undefined; }, enumerable: false, configurable: true, writable: true, }, }); symbols.ctor = SandboxSymbol; return SandboxSymbol; } function SandboxGlobal() {} interface SandboxGlobalConstructor { new (): ISandboxGlobal; } export function sandboxedGlobal(globals: ISandboxGlobal): SandboxGlobalConstructor { SG.prototype = SandboxGlobal.prototype; return SG as unknown as SandboxGlobalConstructor; function SG(this: ISandboxGlobal) { for (const i in globals) { this[i] = globals[i]; } } } export function createContext(sandbox: SandboxExec, options: IOptions): IContext { const sandboxSymbols = createSandboxSymbolContext(options.symbolWhitelist); const SandboxGlobal = sandboxedGlobal(options.globals); const sandboxGlobal = new SandboxGlobal(); const context: IContext = { sandbox: sandbox, globalsWhitelist: new Set(Object.values(options.globals)), prototypeWhitelist: new Map([...options.prototypeWhitelist].map((a) => [a[0].prototype, a[1]])), sandboxSymbols, options, globalScope: new Scope(null, sandboxGlobal, sandboxGlobal), sandboxGlobal, ticks: { ticks: 0n, tickLimit: options.executionQuota, nextYield: options.nonBlocking ? NON_BLOCKING_THRESHOLD : undefined, }, sandboxedFunctions: new WeakSet(), }; context.prototypeWhitelist.set(Object.getPrototypeOf(sandboxGlobal), new Set()); context.prototypeWhitelist.set(Object.getPrototypeOf([][Symbol.iterator]()) as object, new Set()); // Whitelist Generator and AsyncGenerator prototype chains const genProto = Object.getPrototypeOf((function* () {})()); context.prototypeWhitelist.set(Object.getPrototypeOf(genProto), new Set()); const asyncGenProto = Object.getPrototypeOf((async function* () {})()); context.prototypeWhitelist.set(Object.getPrototypeOf(asyncGenProto), new Set()); return context; } export function createExecContext( sandbox: { readonly setSubscriptions: WeakMap< SubscriptionSubject, Map void>> >; readonly changeSubscriptions: WeakMap void>>; readonly sandboxFunctions: WeakMap<(...args: any[]) => any, IExecContext>; readonly context: IContext; }, executionTree: IExecutionTree, evalContext?: IEvalContext, ): IExecContext { const evals = new Map(); const execContext: IExecContext = new ExecContext( sandbox.context, executionTree.constants, executionTree.tree, new Set<(obj: SubscriptionSubject, name: string) => void>(), new WeakMap void>>>(), new WeakMap void>>(), sandbox.setSubscriptions, sandbox.changeSubscriptions, evals, (fn: any) => sandbox.sandboxFunctions.set(fn, execContext), !!evalContext, evalContext, ); if (evalContext) { const func = evalContext.sandboxFunction(execContext); const asyncFunc = evalContext.sandboxAsyncFunction(execContext); const genFunc = evalContext.sandboxGeneratorFunction(execContext); const asyncGenFunc = evalContext.sandboxAsyncGeneratorFunction(execContext); const sandboxSymbol = evalContext.sandboxedSymbol(execContext); evals.set(Function, func); evals.set(AsyncFunction, asyncFunc); evals.set(GeneratorFunction, genFunc); evals.set(AsyncGeneratorFunction, asyncGenFunc); evals.set(Symbol, sandboxSymbol); evals.set(eval, evalContext.sandboxedEval(func, execContext)); evals.set(setTimeout, evalContext.sandboxedSetTimeout(func, execContext)); evals.set(setInterval, evalContext.sandboxedSetInterval(func, execContext)); evals.set(clearTimeout, evalContext.sandboxedClearTimeout(execContext)); evals.set(clearInterval, evalContext.sandboxedClearInterval(execContext)); for (const [original, factory] of DEFAULT_FUNCTION_REPLACEMENTS) { evals.set(original, factory(execContext)); } for (const [original, factory] of sandbox.context.options.functionReplacements) { evals.set(original, factory(execContext, evals.get(original))); } const ptwl = sandbox.context.prototypeWhitelist; for (const [key, value] of evals) { if (!ptwl.has(key.prototype)) { ptwl.set(key.prototype, new Set()); } if (!ptwl.has(value.prototype)) { ptwl.set(value.prototype, ptwl.get(key.prototype) || new Set()); } if (sandbox.context.globalsWhitelist.has(key)) { sandbox.context.globalsWhitelist.add(value); } if (hasOwnProperty(sandbox.context.sandboxGlobal, key.name)) { sandbox.context.sandboxGlobal[key.name] = value; } } if (sandbox.context.sandboxGlobal.globalThis) { sandbox.context.sandboxGlobal.globalThis = sandbox.context.sandboxGlobal; } } return execContext; } export function isLisp(item: LispItem | LispItem): item is Type { return ( Array.isArray(item) && typeof item[0] === 'number' && item[0] !== LispType.None && item[0] !== LispType.True ); } ================================================ FILE: src/utils/Prop.ts ================================================ import type { IExecContext } from './types'; import { THIS_DEPENDENT_FUNCTION_REPLACEMENTS } from './functionReplacements'; const boundFunctionCache = new WeakMap>(); const replacementReceiver = new WeakMap(); export class Prop { constructor( public context: T, public prop: PropertyKey, public isConst = false, public isGlobal = false, public isVariable = false, public isInternal = false, ) {} get(context: IExecContext): T { const ctx = this.context; if (ctx === undefined) throw new ReferenceError(`${this.prop.toString()} is not defined`); if (ctx === null) throw new TypeError(`Cannot read properties of null, (reading '${this.prop.toString()}')`); context.getSubscriptions.forEach((cb) => cb(ctx, this.prop.toString())); const val = (ctx as any)[this.prop]; return getReplacementValue(val, context, ctx) as T; } } export function hasOwnProperty(obj: unknown, prop: PropertyKey): boolean { return Object.prototype.hasOwnProperty.call(obj, prop); } export function getReplacementReceiver(fn: Function) { return replacementReceiver.get(fn); } export function resolveSandboxProp(val: unknown, context: IExecContext, prop?: Prop) { if (!val) return; if (val instanceof Prop) { if (!prop) { prop = val; } val = val.get(context); } const p = prop?.prop || 'prop'; if (val === globalThis) { return new Prop( { [p]: context.ctx.sandboxGlobal, }, p, prop?.isConst || false, false, prop?.isVariable || false, ); } if (prop && !prop.isVariable) { return; } const replacement = getReplacementValue(val, context, prop?.context); if (replacement !== val) { return new Prop( { [p]: replacement }, p, prop?.isConst || false, prop?.isGlobal || false, prop?.isVariable || false, ); } } function getReplacementValue(val: unknown, context: IExecContext, bindContext?: unknown) { if (typeof val !== 'function') { return val; } const replacement = context.evals.get(val as Function); if (replacement === undefined) { return val; } if (!shouldBindReplacement(val as Function, bindContext, context)) { return replacement; } return bindReplacement(replacement, bindContext as object, val as Function, context); } function shouldBindReplacement(original: Function, bindContext: unknown, context: IExecContext) { return ( THIS_DEPENDENT_FUNCTION_REPLACEMENTS.has(original) && bindContext !== null && (typeof bindContext === 'object' || typeof bindContext === 'function') && bindContext !== context.ctx.sandboxGlobal && !context.ctx.globalsWhitelist.has(bindContext) ); } function bindReplacement( replacement: Function, bindContext: object, original: Function, context: IExecContext, ) { let cache = boundFunctionCache.get(replacement); if (!cache) { cache = new WeakMap(); boundFunctionCache.set(replacement, cache); } let bound = cache.get(bindContext); if (bound) { return bound; } bound = function (this: unknown, ...args: unknown[]) { return replacement.apply(this, args); }; redefineFunctionMetadata(bound, original); context.ctx.sandboxedFunctions.add(bound); replacementReceiver.set(bound, bindContext); cache.set(bindContext, bound); return bound; } function redefineFunctionMetadata( target: Function, source: Function, overrides: Partial> = {}, ) { for (const key of ['name', 'length'] as const) { const descriptor = Object.getOwnPropertyDescriptor(source, key); if (descriptor?.configurable) { Object.defineProperty(target, key, { ...descriptor, value: overrides[key] ?? source[key], }); } } } ================================================ FILE: src/utils/Scope.ts ================================================ import { reservedWords, VarType } from './types'; import { Prop, resolveSandboxProp, hasOwnProperty } from './Prop'; import { SandboxError } from './errors'; import type { IExecContext, IScope } from './types'; function keysOnly(obj: unknown): Record { const ret: Record = Object.assign({}, obj); for (const key in ret) { ret[key] = true; } return ret; } export type Unknown = undefined | null | Record; export class Scope { parent: Scope | null; const: { [key: string]: true } = {}; let: { [key: string]: true } = {}; var: { [key: string]: true } = {}; internal: { [key: string]: true } = {}; globals: { [key: string]: true }; allVars: { [key: string]: unknown } & object; internalVars: { [key: string]: unknown } = {}; functionThis?: Unknown; constructor(parent: Scope | null, vars = {}, functionThis?: Unknown) { const isFuncScope = functionThis !== undefined || parent === null; this.parent = parent; this.allVars = vars; this.let = isFuncScope ? this.let : keysOnly(vars); this.var = isFuncScope ? keysOnly(vars) : this.var; this.globals = parent === null ? keysOnly(vars) : {}; this.functionThis = functionThis; } get(key: string, internal: boolean): Prop { const isThis = key === 'this'; const scope = this.getWhereValScope(key, isThis, internal); if (scope && isThis) { return new Prop({ this: scope.functionThis }, key, false, false, true); } if (!scope) { return new Prop(undefined, key); } if (internal && scope.internalVars[key]) { return new Prop(scope.internalVars, key, false, false, true, true); } return new Prop( scope.allVars, key, hasOwnProperty(scope.const, key), hasOwnProperty(scope.globals, key), true, ); } set(key: string, val: unknown, internal: boolean) { if (key === 'this') throw new SyntaxError('"this" cannot be assigned'); if (reservedWords.has(key)) throw new SyntaxError("Unexepected token '" + key + "'"); const prop = this.get(key, internal); if (prop.context === undefined) { throw new ReferenceError(`Variable '${key}' was not declared.`); } if (prop.context === null) { throw new TypeError(`Cannot set properties of null, (setting '${key}')`); } if (prop.isConst) { throw new TypeError(`Assignment to constant variable`); } if (prop.isGlobal) { throw new SandboxError(`Cannot override global variable '${key}'`); } (prop.context as any)[prop.prop] = val; return prop; } getWhereValScope(key: string, isThis: boolean, internal: boolean): Scope | null { let scope: Scope = this; if (isThis) { do { if (scope.functionThis !== undefined) return scope; scope = scope.parent!; } while (scope !== null); return null; } do { if ( internal && key in scope.internalVars && !(key in {} && !hasOwnProperty(scope.internalVars, key)) ) { return scope; } if (key in scope.allVars && !(key in {} && !hasOwnProperty(scope.allVars, key))) { return scope; } scope = scope.parent!; } while (scope !== null); return null; } getWhereVarScope(key: string, localScope: boolean, internal: boolean): Scope { let scope: Scope = this; do { if ( internal && key in scope.internalVars && !(key in {} && !hasOwnProperty(scope.internalVars, key)) ) { return scope; } if (key in scope.allVars && !(key in {} && !hasOwnProperty(scope.allVars, key))) { return scope; } if (scope.parent === null || localScope || scope.functionThis !== undefined) { return scope; } scope = scope.parent!; } while (scope !== null); return scope; } declare(key: string, type: VarType, value: unknown, isGlobal: boolean, internal: boolean): Prop { if (key === 'this') throw new SyntaxError('"this" cannot be declared'); if (reservedWords.has(key)) throw new SyntaxError("Unexepected token '" + key + "'"); const existingScope = this.getWhereVarScope(key, type !== VarType.var, internal); if (type === VarType.var) { if (existingScope.var[key]) { existingScope.allVars[key] = value; if (!isGlobal) { delete existingScope.globals[key]; } else { existingScope.globals[key] = true; } return new Prop(existingScope.allVars, key, false, existingScope.globals[key], true); } else if (key in existingScope.allVars) { throw new SyntaxError(`Identifier '${key}' has already been declared`); } } if (key in existingScope.allVars || key in existingScope.internalVars) { throw new SyntaxError(`Identifier '${key}' has already been declared`); } if (isGlobal) { existingScope.globals[key] = true; } existingScope[type][key] = true; if (type === VarType.internal) { existingScope.internalVars[key] = value; } else { existingScope.allVars[key] = value; } return new Prop( type === VarType.internal ? this.internalVars : this.allVars, key, type === VarType.const, isGlobal, true, type === VarType.internal, ); } } export class FunctionScope implements IScope {} export class LocalScope implements IScope {} export const optional = {}; export class DelayedSynchronousResult { readonly result: unknown; constructor(cb: () => unknown) { this.result = cb(); } } export function delaySynchronousResult(cb: () => Promise) { return new DelayedSynchronousResult(cb); } export function sanitizeProp( value: unknown, context: IExecContext, cache = new WeakSet(), ): unknown { if (value === null || (typeof value !== 'object' && typeof value !== 'function')) return value; value = resolveSandboxProp(value, context) || value; if (value instanceof Prop) { value = value.get(context); } if (value === optional) { return undefined; } return value; } export function sanitizeScope(scope: IScope, context: IExecContext, cache = new WeakSet()) { if (cache.has(scope)) return; cache.add(scope); for (const key in scope) { const val = scope[key]; if (val !== null && typeof val === 'object') { sanitizeScope(val, context, cache); } scope[key] = sanitizeProp(val, context); } } export function sanitizeScopes( scopes: IScope[], context: IExecContext, cache = new WeakSet(), ) { for (const scope of scopes) { sanitizeScope(scope, context, cache); } } ================================================ FILE: src/utils/errors.ts ================================================ import { IExecContext, IScope } from './types'; export class SandboxError extends Error {} export class SandboxExecutionQuotaExceededError extends SandboxError {} export class SandboxExecutionTreeError extends SandboxError {} export class SandboxCapabilityError extends SandboxError {} export class SandboxAccessError extends SandboxError {} ================================================ FILE: src/utils/functionReplacements.ts ================================================ import type { IContext, IExecContext } from './types'; import { SandboxExecutionQuotaExceededError } from './errors'; /** * Checks if adding `expectTicks` would exceed the tick limit, and throws SandboxExecutionQuotaExceededError * (which bypasses user try/catch) if so. Otherwise increments the tick counter. */ export function checkTicksAndThrow(ctx: IExecContext, expectTicks: bigint): void { const { ticks } = ctx.ctx; if (ticks.tickLimit !== undefined && ticks.tickLimit <= ticks.ticks + expectTicks) { throw new SandboxExecutionQuotaExceededError('Execution quota exceeded'); } ticks.ticks += expectTicks; } // --------------------------------------------------------------------------- // TypedArray shared prototype detection (mirrors call.ts) // --------------------------------------------------------------------------- const _typedArrayCtors = [ Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array, ]; export const typedArrayProtos = new Set( _typedArrayCtors.map((T) => Object.getPrototypeOf(T.prototype) as any), ); function isTypedArray(obj: unknown): obj is { length: number } { return ( ArrayBuffer.isView(obj) && !(obj instanceof DataView) && typedArrayProtos.has(Object.getPrototypeOf(Object.getPrototypeOf(obj))) ); } // --------------------------------------------------------------------------- // Helpers to build replacements // --------------------------------------------------------------------------- type Factory = (ctx: IExecContext) => Function; function makeReplacement( original: Function, getTicks: (thisArg: unknown, args: unknown[]) => bigint, ): Factory { return (ctx: IExecContext) => function (this: unknown, ...args: unknown[]) { checkTicksAndThrow(ctx, getTicks(this, args)); return (original as any).apply(this, args); }; } // --------------------------------------------------------------------------- // Array replacements // --------------------------------------------------------------------------- const arr: any[] = []; const arrProto = Array.prototype as any; function arrayTicks( complexity: 'one' | 'n' | 'nlogn' | 'arrs', original: Function, ): (thisArg: unknown, args: unknown[]) => bigint { return (thisArg, args) => { if (!Array.isArray(thisArg)) return 0n; const n = BigInt(thisArg.length); switch (complexity) { case 'one': return 1n; case 'n': return n; case 'nlogn': return thisArg.length <= 1 ? 1n : n * BigInt(Math.round(Math.log2(thisArg.length))); case 'arrs': { let ticks = 0n; const maxDepth = original === arr.flat ? (typeof args[0] === 'number' ? args[0] : 1) : 1; const recurse = (a: unknown[], depth = 0) => { ticks += BigInt(a.length); if (depth >= maxDepth) return; for (const item of a) { if (Array.isArray(item)) recurse(item, depth + 1); } }; recurse(thisArg); return ticks; } } }; } const arrayReplacementDefs: [Function, 'one' | 'n' | 'nlogn' | 'arrs'][] = [ // O(1) [arr.push, 'one'], [arr.pop, 'one'], [arrProto.at, 'one'], // O(n) [arr.fill, 'n'], [arr.includes, 'n'], [arr.indexOf, 'n'], [arr.lastIndexOf, 'n'], [arr.find, 'n'], [arr.findIndex, 'n'], [arrProto.findLast, 'n'], [arrProto.findLastIndex, 'n'], [arr.forEach, 'n'], [arr.map, 'n'], [arr.filter, 'n'], [arr.reduce, 'n'], [arr.reduceRight, 'n'], [arr.every, 'n'], [arr.some, 'n'], [arr.join, 'n'], [arr.reverse, 'n'], [arr.shift, 'n'], [arr.unshift, 'n'], [arr.splice, 'n'], [arr.slice, 'n'], [arr.copyWithin, 'n'], [arr.entries, 'n'], [arr.keys, 'n'], [arr.values, 'n'], [arrProto.toReversed, 'n'], [arrProto.toSpliced, 'n'], [arrProto.with, 'n'], [arr.toString, 'n'], [arr.toLocaleString, 'n'], // O(n log n) [arr.sort, 'nlogn'], [arrProto.toSorted, 'nlogn'], // O(n) across arrays [arr.flat, 'arrs'], [arr.flatMap, 'arrs'], [arr.concat, 'arrs'], ]; // --------------------------------------------------------------------------- // String replacements // --------------------------------------------------------------------------- const str = ''; const strProto = String.prototype as any; function stringTicks(complexity: 'one' | 'n'): (thisArg: unknown, _args: unknown[]) => bigint { return (thisArg) => { if (typeof thisArg !== 'string') return 0n; return complexity === 'one' ? 1n : BigInt(thisArg.length); }; } const stringReplacementDefs: [Function, 'one' | 'n'][] = [ // O(1) [str.charAt, 'one'], [str.charCodeAt, 'one'], [str.codePointAt, 'one'], [strProto.at, 'one'], // O(n) [str.indexOf, 'n'], [str.lastIndexOf, 'n'], [str.includes, 'n'], [str.startsWith, 'n'], [str.endsWith, 'n'], [str.slice, 'n'], [str.substring, 'n'], [str.padStart, 'n'], [str.padEnd, 'n'], [str.repeat, 'n'], [str.split, 'n'], [str.replace, 'n'], [strProto.replaceAll, 'n'], [str.match, 'n'], [str.matchAll, 'n'], [str.search, 'n'], [str.trim, 'n'], [str.trimStart, 'n'], [str.trimEnd, 'n'], [str.toLowerCase, 'n'], [str.toUpperCase, 'n'], [str.toLocaleLowerCase, 'n'], [str.toLocaleUpperCase, 'n'], [str.normalize, 'n'], [str.concat, 'n'], [str.toString, 'n'], [str.valueOf, 'n'], ]; // --------------------------------------------------------------------------- // Map replacements // --------------------------------------------------------------------------- const _map = new Map(); const mapReplacementDefs: [Function, 'one' | 'n'][] = [ [_map.get, 'one'], [_map.set, 'one'], [_map.has, 'one'], [_map.delete, 'one'], [_map.keys, 'n'], [_map.values, 'n'], [_map.entries, 'n'], [_map.forEach, 'n'], [_map.clear, 'n'], ]; function mapTicks(complexity: 'one' | 'n'): (thisArg: unknown, _args: unknown[]) => bigint { return (thisArg) => { if (!(thisArg instanceof Map)) return 0n; return complexity === 'one' ? 1n : BigInt(thisArg.size); }; } // --------------------------------------------------------------------------- // Set replacements // --------------------------------------------------------------------------- const _set = new Set(); const setReplacementDefs: [Function, 'one' | 'n'][] = [ [_set.add, 'one'], [_set.has, 'one'], [_set.delete, 'one'], [_set.values, 'n'], [_set.keys, 'n'], [_set.entries, 'n'], [_set.forEach, 'n'], [_set.clear, 'n'], ]; function setTicks(complexity: 'one' | 'n'): (thisArg: unknown, _args: unknown[]) => bigint { return (thisArg) => { if (!(thisArg instanceof Set)) return 0n; return complexity === 'one' ? 1n : BigInt(thisArg.size); }; } // --------------------------------------------------------------------------- // TypedArray replacements // --------------------------------------------------------------------------- const typedArrayReplacementDefs: [Function, 'one' | 'n' | 'nlogn'][] = []; for (const proto of typedArrayProtos) { if (proto.at) typedArrayReplacementDefs.push([proto.at, 'one']); if (proto.set) typedArrayReplacementDefs.push([proto.set, 'one']); const nMethods = [ 'fill', 'find', 'findIndex', 'findLast', 'findLastIndex', 'includes', 'indexOf', 'lastIndexOf', 'forEach', 'map', 'filter', 'reduce', 'reduceRight', 'every', 'some', 'join', 'reverse', 'slice', 'subarray', 'copyWithin', 'entries', 'keys', 'values', 'toReversed', 'with', 'toString', 'toLocaleString', ] as const; for (const m of nMethods) { if (proto[m]) typedArrayReplacementDefs.push([proto[m], 'n']); } if (proto.sort) typedArrayReplacementDefs.push([proto.sort, 'nlogn']); if (proto.toSorted) typedArrayReplacementDefs.push([proto.toSorted, 'nlogn']); } function typedArrayTicks( complexity: 'one' | 'n' | 'nlogn', ): (thisArg: unknown, _args: unknown[]) => bigint { return (thisArg) => { if (!isTypedArray(thisArg)) return 0n; const n = BigInt(thisArg.length); switch (complexity) { case 'one': return 1n; case 'n': return n; case 'nlogn': return (thisArg as any).length <= 1 ? 1n : n * BigInt(Math.round(Math.log2((thisArg as any).length))); } }; } // --------------------------------------------------------------------------- // Math replacements // --------------------------------------------------------------------------- const mathReplacementDefs: [Function][] = [[Math.max], [Math.min], [Math.hypot]]; // --------------------------------------------------------------------------- // JSON replacements // --------------------------------------------------------------------------- // (JSON.stringify with subscription traversal is handled separately in LispType.Call) // --------------------------------------------------------------------------- // RegExp replacements // --------------------------------------------------------------------------- const _re = /x/; const regexpReplacementDefs: Function[] = [ _re.exec, _re.test, (_re as any)[Symbol.match], (_re as any)[Symbol.matchAll], (_re as any)[Symbol.replace], (_re as any)[Symbol.search], (_re as any)[Symbol.split], ]; // --------------------------------------------------------------------------- // Promise replacements // --------------------------------------------------------------------------- const promiseReplacementDefs: Function[] = [ Promise.all, Promise.allSettled, Promise.race, ...(typeof (Promise as any).any === 'function' ? [(Promise as any).any] : []), ]; // --------------------------------------------------------------------------- // Object replacements // --------------------------------------------------------------------------- const objectReplacementDefs: [Function, 'one' | 'n'][] = [ [Object.prototype.hasOwnProperty, 'one'], [Object.prototype.propertyIsEnumerable, 'one'], [Object.prototype.isPrototypeOf, 'one'], [Object.create, 'one'], [Object.getPrototypeOf, 'one'], [Object.setPrototypeOf, 'one'], [Object.is, 'one'], [Object.defineProperty, 'one'], [Object.getOwnPropertyDescriptor, 'one'], [Object.isExtensible, 'one'], [Object.preventExtensions, 'one'], [Object.keys, 'n'], [Object.values, 'n'], [Object.entries, 'n'], [Object.assign, 'n'], [Object.fromEntries, 'n'], [Object.getOwnPropertyNames, 'n'], [Object.getOwnPropertySymbols, 'n'], [Object.getOwnPropertyDescriptors, 'n'], [Object.freeze, 'n'], [Object.seal, 'n'], [Object.isFrozen, 'n'], [Object.isSealed, 'n'], ]; function objectTicks( complexity: 'one' | 'n', isStatic: boolean, ): (thisArg: unknown, args: unknown[]) => bigint { return (thisArg, args) => { if (complexity === 'one') return 1n; const target = isStatic ? args[0] : thisArg; if (target !== null && typeof target === 'object') return BigInt(Object.keys(target as object).length); return 1n; }; } // Static Object methods whose tick count is based on args[0] key count const staticObjectMethods = new Set([ Object.keys, Object.values, Object.entries, Object.assign, Object.fromEntries, Object.getOwnPropertyNames, Object.getOwnPropertySymbols, Object.getOwnPropertyDescriptors, Object.freeze, Object.seal, Object.isFrozen, Object.isSealed, ]); // --------------------------------------------------------------------------- // Array.from / Array.fromAsync // --------------------------------------------------------------------------- // --------------------------------------------------------------------------- // Build the default replacements map // --------------------------------------------------------------------------- export const DEFAULT_FUNCTION_REPLACEMENTS = new Map(); export const THIS_DEPENDENT_FUNCTION_REPLACEMENTS = new Set(); // Array for (const [original, complexity] of arrayReplacementDefs) { if (!original) continue; DEFAULT_FUNCTION_REPLACEMENTS.set( original, makeReplacement(original, arrayTicks(complexity, original)), ); THIS_DEPENDENT_FUNCTION_REPLACEMENTS.add(original); } // String for (const [original, complexity] of stringReplacementDefs) { if (!original) continue; DEFAULT_FUNCTION_REPLACEMENTS.set(original, makeReplacement(original, stringTicks(complexity))); THIS_DEPENDENT_FUNCTION_REPLACEMENTS.add(original); } // Map for (const [original, complexity] of mapReplacementDefs) { if (!original) continue; DEFAULT_FUNCTION_REPLACEMENTS.set(original, makeReplacement(original, mapTicks(complexity))); THIS_DEPENDENT_FUNCTION_REPLACEMENTS.add(original); } // Set for (const [original, complexity] of setReplacementDefs) { if (!original) continue; DEFAULT_FUNCTION_REPLACEMENTS.set(original, makeReplacement(original, setTicks(complexity))); THIS_DEPENDENT_FUNCTION_REPLACEMENTS.add(original); } // TypedArray for (const [original, complexity] of typedArrayReplacementDefs) { if (!original) continue; DEFAULT_FUNCTION_REPLACEMENTS.set( original, makeReplacement(original, typedArrayTicks(complexity)), ); THIS_DEPENDENT_FUNCTION_REPLACEMENTS.add(original); } // Math — O(n) on arg count for (const [original] of mathReplacementDefs) { if (!original) continue; DEFAULT_FUNCTION_REPLACEMENTS.set( original, makeReplacement(original, (_thisArg, args) => BigInt(args.length)), ); } // JSON — O(n) on string/object size for (const original of [JSON.parse, JSON.stringify]) { DEFAULT_FUNCTION_REPLACEMENTS.set( original, makeReplacement(original, (_thisArg, args) => { const target = args[0]; if (typeof target === 'string') return BigInt(target.length); if (target !== null && typeof target === 'object') return BigInt(Object.keys(target as object).length); return 1n; }), ); } // RegExp — O(n) on input string length for (const original of regexpReplacementDefs) { if (!original) continue; DEFAULT_FUNCTION_REPLACEMENTS.set( original, makeReplacement(original, (_thisArg, args) => { const input = args[0]; return typeof input === 'string' ? BigInt(input.length) : 1n; }), ); THIS_DEPENDENT_FUNCTION_REPLACEMENTS.add(original); } // Promise.all/allSettled/race/any — O(n) on iterable length for (const original of promiseReplacementDefs) { if (!original) continue; DEFAULT_FUNCTION_REPLACEMENTS.set( original, makeReplacement(original, (_thisArg, args) => { const iterable = args[0]; return Array.isArray(iterable) ? BigInt(iterable.length) : 0n; }), ); } // Object static & instance methods for (const [original, complexity] of objectReplacementDefs) { if (!original) continue; const isStatic = staticObjectMethods.has(original); DEFAULT_FUNCTION_REPLACEMENTS.set( original, makeReplacement(original, objectTicks(complexity, isStatic)), ); if (!isStatic) { THIS_DEPENDENT_FUNCTION_REPLACEMENTS.add(original); } } // Array.from — O(n) on source length if (Array.from) { DEFAULT_FUNCTION_REPLACEMENTS.set( Array.from, makeReplacement(Array.from, (_thisArg, args) => { const source = args[0]; if (source != null && typeof (source as any).length === 'number') return BigInt((source as any).length); return 0n; }), ); } // Array.fromAsync — O(n) on source length (if available) if (typeof (Array as any).fromAsync === 'function') { const fromAsync = (Array as any).fromAsync as Function; DEFAULT_FUNCTION_REPLACEMENTS.set( fromAsync, makeReplacement(fromAsync, (_thisArg, args) => { const source = args[0]; if (source != null && typeof (source as any).length === 'number') return BigInt((source as any).length); return 0n; }), ); } ================================================ FILE: src/utils/index.ts ================================================ export * from './unraw'; export * from './errors'; export * from './CodeString'; export * from './types'; export * from './Prop'; export * from './Scope'; export * from './ExecContext'; ================================================ FILE: src/utils/types.ts ================================================ // Reusable AsyncFunction constructor references export const AsyncFunction: Function = Object.getPrototypeOf(async function () {}).constructor; export const GeneratorFunction: Function = Object.getPrototypeOf(function* () {}).constructor; export const AsyncGeneratorFunction: Function = Object.getPrototypeOf( async function* () {}, ).constructor; import type { IEvalContext } from '../eval'; import type { Change } from '../executor'; import type { IExecutionTree } from '../parser'; import type SandboxExec from '../SandboxExec'; import type { Scope } from './Scope'; export interface IOptionParams { audit?: boolean; forbidFunctionCalls?: boolean; forbidFunctionCreation?: boolean; prototypeWhitelist?: Map>; globals?: IGlobals; symbolWhitelist?: ISymbolWhitelist; executionQuota?: bigint; nonBlocking?: boolean; haltOnSandboxError?: boolean; maxParserRecursionDepth?: number; /** * Additional function replacements to merge with the built-in tick-checking replacements. * Maps a native function to a factory that receives the IContext and returns the replacement. * When sandboxed code accesses a property that returns a mapped function, the factory is * called once per context and the result is cached and returned instead. */ functionReplacements?: Map Function>; } export interface IOptions { audit: boolean; forbidFunctionCalls: boolean; forbidFunctionCreation: boolean; prototypeWhitelist: Map>; globals: IGlobals; symbolWhitelist: ISymbolWhitelist; executionQuota?: bigint; haltOnSandboxError?: boolean; maxParserRecursionDepth: number; nonBlocking: boolean; functionReplacements: Map< Function, (ctx: IExecContext, builtInReplacement?: Function) => Function >; } export interface IContext { sandbox: SandboxExec; globalScope: Scope; sandboxGlobal: ISandboxGlobal; globalsWhitelist: Set; prototypeWhitelist: Map>; sandboxedFunctions: WeakSet; sandboxSymbols: SandboxSymbolContext; options: IOptions; auditReport?: IAuditReport; ticks: Ticks; } export interface IAuditReport { globalsAccess: Set; prototypeAccess: { [name: string]: Set }; } export interface Ticks { ticks: bigint; tickLimit?: bigint; nextYield?: bigint; } export type SubscriptionSubject = object; export type HaltContext = | { type: 'error'; error: Error; ticks: Ticks; scope: Scope; context: IExecContext; } | { type: 'manual'; error?: never; ticks?: never; scope?: never; context?: never; } | { type: 'yield'; error?: never; ticks?: never; scope?: never; context?: never; }; export interface IExecContext extends IExecutionTree { ctx: IContext; getSubscriptions: Set<(obj: SubscriptionSubject, name: string) => void>; setSubscriptions: WeakMap void>>>; changeSubscriptions: WeakMap void>>; setSubscriptionsGlobal: WeakMap< SubscriptionSubject, Map void>> >; changeSubscriptionsGlobal: WeakMap void>>; registerSandboxFunction: (fn: (...args: any[]) => any) => void; evals: Map; allowJit: boolean; evalContext?: IEvalContext; } export interface ISandboxGlobal { [key: string]: unknown; } export interface ISymbolWhitelist { [key: string]: symbol; } export interface SandboxSymbolContext { ctor?: Function; registry: Map; reverseRegistry: Map; whitelist: ISymbolWhitelist; } export type IGlobals = ISandboxGlobal; export interface IScope { [key: string]: any; } export const NON_BLOCKING_THRESHOLD = 50_000n; export const reservedWords = new Set([ 'await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default', 'delete', 'do', 'else', 'enum', 'export', 'extends', 'false', 'finally', 'for', 'function', 'if', 'implements', 'import', 'in', 'instanceof', 'let', 'new', 'null', 'return', 'super', 'switch', 'this', 'throw', 'true', 'try', 'typeof', 'var', 'void', 'while', 'with', ]); export const enum VarType { let = 'let', const = 'const', var = 'var', internal = 'internal', } export const enum LispType { None, Prop, StringIndex, Let, Const, Call, KeyVal, Number, Return, Assign, InlineFunction, ArrowFunction, CreateArray, If, IfCase, InlineIf, InlineIfCase, SpreadObject, SpreadArray, ArrayProp, PropOptional, CallOptional, CreateObject, Group, Not, IncrementBefore, IncrementAfter, DecrementBefore, DecrementAfter, And, Or, StrictNotEqual, StrictEqual, Plus, Var, GlobalSymbol, Literal, Function, Loop, Try, Switch, SwitchCase, InternalBlock, Expression, Await, New, Throw, Minus, Divide, Power, Multiply, Modulus, Equal, NotEqual, SmallerEqualThan, LargerEqualThan, SmallerThan, LargerThan, Negative, Positive, Typeof, Delete, Instanceof, In, Inverse, SubractEquals, AddEquals, DivideEquals, PowerEquals, MultiplyEquals, ModulusEquals, BitNegateEquals, BitAndEquals, BitOrEquals, UnsignedShiftRightEquals, ShiftRightEquals, ShiftLeftEquals, BitAnd, BitOr, BitNegate, BitShiftLeft, BitShiftRight, BitUnsignedShiftRight, BigInt, LiteralIndex, RegexIndex, LoopAction, Void, True, NullishCoalescing, AndEquals, OrEquals, NullishCoalescingEquals, Block, Labeled, Internal, Yield, YieldDelegate, Hole, LispEnumSize, } ================================================ FILE: src/utils/unraw.ts ================================================ /** * Parse a string as a base-16 number. This is more strict than `parseInt` as it * will not allow any other characters, including (for example) "+", "-", and * ".". * @param hex A string containing a hexadecimal number. * @returns The parsed integer, or `NaN` if the string is not a valid hex * number. */ function parseHexToInt(hex: string): number { const isOnlyHexChars = !hex.match(/[^a-f0-9]/i); return isOnlyHexChars ? parseInt(hex, 16) : NaN; } /** * Check the validity and length of a hexadecimal code and optionally enforces * a specific number of hex digits. * @param hex The string to validate and parse. * @param errorName The name of the error message to throw a `SyntaxError` with * if `hex` is invalid. This is used to index `errorMessages`. * @param enforcedLength If provided, will throw an error if `hex` is not * exactly this many characters. * @returns The parsed hex number as a normal number. * @throws {SyntaxError} If the code is not valid. */ function validateAndParseHex(hex: string, errorName: string, enforcedLength?: number): number { const parsedHex = parseHexToInt(hex); if (Number.isNaN(parsedHex) || (enforcedLength !== undefined && enforcedLength !== hex.length)) { throw new SyntaxError(errorName + ': ' + hex); } return parsedHex; } /** * Parse a two-digit hexadecimal character escape code. * @param code The two-digit hexadecimal number that represents the character to * output. * @returns The single character represented by the code. * @throws {SyntaxError} If the code is not valid hex or is not the right * length. */ function parseHexadecimalCode(code: string): string { const parsedCode = validateAndParseHex(code, 'Malformed Hexadecimal', 2); return String.fromCharCode(parsedCode); } /** * Parse a four-digit Unicode character escape code. * @param code The four-digit unicode number that represents the character to * output. * @param surrogateCode Optional four-digit unicode surrogate that represents * the other half of the character to output. * @returns The single character represented by the code. * @throws {SyntaxError} If the codes are not valid hex or are not the right * length. */ function parseUnicodeCode(code: string, surrogateCode?: string): string { const parsedCode = validateAndParseHex(code, 'Malformed Unicode', 4); if (surrogateCode !== undefined) { const parsedSurrogateCode = validateAndParseHex(surrogateCode, 'Malformed Unicode', 4); return String.fromCharCode(parsedCode, parsedSurrogateCode); } return String.fromCharCode(parsedCode); } /** * Test if the text is surrounded by curly braces (`{}`). * @param text Text to check. * @returns `true` if the text is in the form `{*}`. */ function isCurlyBraced(text: string): boolean { return text.charAt(0) === '{' && text.charAt(text.length - 1) === '}'; } /** * Parse a Unicode code point character escape code. * @param codePoint A unicode escape code point, including the surrounding curly * braces. * @returns The single character represented by the code. * @throws {SyntaxError} If the code is not valid hex or does not have the * surrounding curly braces. */ function parseUnicodeCodePointCode(codePoint: string): string { if (!isCurlyBraced(codePoint)) { throw new SyntaxError('Malformed Unicode: +' + codePoint); } const withoutBraces = codePoint.slice(1, -1); const parsedCode = validateAndParseHex(withoutBraces, 'Malformed Unicode'); try { return String.fromCodePoint(parsedCode); } catch (err) { throw err instanceof RangeError ? new SyntaxError('Code Point Limit:' + parsedCode) : err; } } /** * Map of unescaped letters to their corresponding special JS escape characters. * Intentionally does not include characters that map to themselves like "\'". */ const singleCharacterEscapes = new Map([ ['b', '\b'], ['f', '\f'], ['n', '\n'], ['r', '\r'], ['t', '\t'], ['v', '\v'], ['0', '\0'], ]); /** * Parse a single character escape sequence and return the matching character. * If none is matched, defaults to `code`. * @param code A single character code. */ function parseSingleCharacterCode(code: string): string { return singleCharacterEscapes.get(code) || code; } /** * Matches every escape sequence possible, including invalid ones. * * All capture groups (described below) are unique (only one will match), except * for 4, which can only potentially match if 3 does. * * **Capture Groups:** * 0. A single backslash * 1. Hexadecimal code * 2. Unicode code point code with surrounding curly braces * 3. Unicode escape code with surrogate * 4. Surrogate code * 5. Unicode escape code without surrogate * 6. Octal code _NOTE: includes "0"._ * 7. A single character (will never be \, x, u, or 0-3) */ const escapeMatch = /\\(?:(\\)|x([\s\S]{0,2})|u(\{[^}]*\}?)|u([\s\S]{4})\\u([^{][\s\S]{0,3})|u([\s\S]{0,4})|([0-3]?[0-7]{1,2})|([\s\S])|$)/g; /** * Replace raw escape character strings with their escape characters. * @param raw A string where escape characters are represented as raw string * values like `\'` rather than `'`. * @param allowOctals If `true`, will process the now-deprecated octal escape * sequences (ie, `\111`). * @returns The processed string, with escape characters replaced by their * respective actual Unicode characters. */ export function unraw(raw: string): string { return raw.replace( escapeMatch, function ( _, backslash?: string, hex?: string, codePoint?: string, unicodeWithSurrogate?: string, surrogate?: string, unicode?: string, octal?: string, singleCharacter?: string, ): string { // Compare groups to undefined because empty strings mean different errors // Otherwise, `\u` would fail the same as `\` which is wrong. if (backslash !== undefined) { return '\\'; } if (hex !== undefined) { return parseHexadecimalCode(hex); } if (codePoint !== undefined) { return parseUnicodeCodePointCode(codePoint); } if (unicodeWithSurrogate !== undefined) { return parseUnicodeCode(unicodeWithSurrogate, surrogate); } if (unicode !== undefined) { return parseUnicodeCode(unicode); } if (octal === '0') { return '\0'; } if (octal !== undefined) { throw new SyntaxError('Octal Deprecation: ' + octal); } if (singleCharacter !== undefined) { return parseSingleCharacterCode(singleCharacter); } throw new SyntaxError('End of string'); }, ); } export default unraw; ================================================ FILE: test/audit.spec.ts ================================================ import Sandbox from '../src/Sandbox.js'; describe('Sandbox.audit Tests', () => { describe('Basic functionality', () => { it('should execute simple code and return result', () => { const result = Sandbox.audit('return 2 + 3'); expect(result.result).toBe(5); }); it('should execute code with variables from scope', () => { const scope = { x: 10, y: 20 }; const result = Sandbox.audit('return x + y', [scope]); expect(result.result).toBe(30); }); it('should handle function definitions', () => { const code = ` function add(a, b) { return a + b; } return add(5, 7); `; const result = Sandbox.audit(code); expect(result.result).toBe(12); }); it('should handle arrow functions', () => { const code = 'return ((x, y) => x * y)(3, 4)'; const result = Sandbox.audit(code); expect(result.result).toBe(12); }); it('should handle arrays and objects', () => { const code = 'return [1, 2, 3].map(x => x * 2)'; const result = Sandbox.audit(code); expect(result.result).toEqual([2, 4, 6]); }); it('should handle object creation', () => { const code = 'return { a: 1, b: 2, c: 3 }'; const result = Sandbox.audit<{ a: number; b: number; c: number }>(code); expect(result.result).toEqual({ a: 1, b: 2, c: 3 }); }); }); describe('Audit mode behavior', () => { it('should allow access to global objects', () => { const code = 'return typeof Math'; const result = Sandbox.audit(code); expect(result.result).toBe('object'); }); it('should allow access to Date', () => { const code = 'return new Date(0).toISOString()'; const result = Sandbox.audit(code); expect(result.result).toBe('1970-01-01T00:00:00.000Z'); }); it('should allow access to JSON', () => { const code = 'return JSON.stringify({test: 1})'; const result = Sandbox.audit(code); expect(result.result).toBe('{"test":1}'); }); it('should provide access to console methods', () => { const code = 'return typeof console.log'; const result = Sandbox.audit(code); expect(result.result).toBe('function'); }); }); describe('With multiple scopes', () => { it('should handle multiple scopes in order', () => { const scope1 = { a: 1 }; const scope2 = { b: 2 }; const scope3 = { c: 3 }; const result = Sandbox.audit('return a + b + c', [scope1, scope2, scope3]); expect(result.result).toBe(6); }); it('should handle scope shadowing', () => { const scope1 = { x: 10 }; const scope2 = { x: 20 }; const result = Sandbox.audit('return x', [scope1, scope2]); // Later scopes shadow earlier ones expect(result.result).toBe(20); }); }); describe('Error handling', () => { it('should throw runtime errors', () => { const code = 'throw new Error("test error")'; expect(() => { Sandbox.audit(code); }).toThrow('test error'); }); it('should throw on undefined variable access', () => { const code = 'return nonExistentVar'; expect(() => { Sandbox.audit(code); }).toThrow(ReferenceError); }); it('should throw type errors', () => { const code = 'return null.property'; expect(() => { Sandbox.audit(code); }).toThrow(TypeError); }); }); describe('Complex operations', () => { it('should handle loops', () => { const code = ` let sum = 0; for (let i = 1; i <= 5; i++) { sum += i; } return sum; `; const result = Sandbox.audit(code); expect(result.result).toBe(15); }); it('should handle nested functions', () => { const code = ` function outer(x) { function inner(y) { return x + y; } return inner(5); } return outer(10); `; const result = Sandbox.audit(code); expect(result.result).toBe(15); }); it('should handle closures', () => { const code = ` function makeCounter() { let count = 0; return function() { return ++count; }; } const counter = makeCounter(); counter(); counter(); return counter(); `; const result = Sandbox.audit(code); expect(result.result).toBe(3); }); it('should handle template literals', () => { const code = ` const name = "World"; return \`Hello, \${name}!\`; `; const result = Sandbox.audit(code); expect(result.result).toBe('Hello, World!'); }); }); }); ================================================ FILE: test/compileRerun.spec.ts ================================================ import Sandbox from '../src/Sandbox.js'; describe('Compiled code rerun', () => { it('should correctly rerun compiled code with named function declarations', () => { const sandbox = new Sandbox(); const exec = sandbox.compile('function double(x) { return x * 2; } return double(21);'); expect(exec({}).run()).toBe(42); expect(exec({}).run()).toBe(42); }); it('should correctly rerun compiled code with recursive functions', () => { const sandbox = new Sandbox(); const exec = sandbox.compile(` function fact(n) { return n <= 1 ? 1 : n * fact(n - 1); } return fact(5); `); expect(exec({}).run()).toBe(120); expect(exec({}).run()).toBe(120); }); it('should correctly rerun compiled code with constructor functions', () => { const sandbox = new Sandbox(); const exec = sandbox.compile(` function Point(x, y) { this.x = x; this.y = y; } const p = new Point(3, 4); return p.x + p.y; `); expect(exec({}).run()).toBe(7); expect(exec({}).run()).toBe(7); }); it('should correctly rerun compiled code with multiple function declarations', () => { const sandbox = new Sandbox(); const exec = sandbox.compile(` function LinkedListNode(value) { this.value = value; this.next = null; } function reverse(head) { if (!head || !head.next) return head; let tmp = reverse(head.next); head.next.next = head; head.next = undefined; return tmp; } const root = new LinkedListNode(0); let current = root; for (let i = 1; i < 5; i++) { const node = new LinkedListNode(i); current.next = node; current = node; } return reverse(root).value; `); expect(exec({}).run()).toBe(4); expect(exec({}).run()).toBe(4); }); it('should correctly rerun compiled async code with named function declarations', async () => { const sandbox = new Sandbox(); const exec = sandbox.compileAsync(` async function add(a, b) { return a + b; } return add(1, 2); `); expect(await exec({}).run()).toBe(3); expect(await exec({}).run()).toBe(3); }); it('should correctly rerun compiled code with arrow functions', () => { const sandbox = new Sandbox(); const exec = sandbox.compile('const fn = (x) => x + 1; return fn(5);'); expect(exec({}).run()).toBe(6); expect(exec({}).run()).toBe(6); }); it('should correctly rerun compiled code with async arrow functions', async () => { const sandbox = new Sandbox(); const exec = sandbox.compileAsync('const fn = async (x) => x + 1; return fn(5);'); expect(await exec({}).run()).toBe(6); expect(await exec({}).run()).toBe(6); }); }); ================================================ FILE: test/delaySynchronousResult.spec.ts ================================================ import Sandbox, { delaySynchronousResult } from '../src/Sandbox.js'; const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); describe('delaySynchronousResult', () => { it('delivers a value from a setTimeout without async/await in sandbox code', async () => { const fetchValue = () => delaySynchronousResult(async () => { await wait(10); return 42; }); const sandbox = new Sandbox(); const scope = { fetchValue }; const result = await sandbox.compileAsync('return fetchValue();')(scope).run(); expect(result).toBe(42); }); it('sandbox code does not need await to get the delayed result', async () => { const results: number[] = []; const loadItem = (n: number) => delaySynchronousResult(async () => { await wait(10); return n * 10; }); const sandbox = new Sandbox(); const scope = { loadItem, results }; // Note: no await in the sandbox code — delaySynchronousResult handles it await sandbox .compileAsync( ` const a = loadItem(1); const b = loadItem(2); results.push(a, b); `, )(scope) .run(); expect(results).toEqual([10, 20]); }); it('sequences delayed setTimeout calls in order without await', async () => { const order: string[] = []; const step = (label: string) => delaySynchronousResult(async () => { await wait(10); order.push(label); return label; }); const sandbox = new Sandbox(); const scope = { step, order }; const ret = await sandbox .compileAsync( ` return step('a') + step('b') + step('c'); `, )(scope) .run(); expect(order).toEqual(['a', 'b', 'c']); expect(ret).toEqual('abc'); }); it('propagates a setTimeout rejection without await in sandbox code', async () => { const failAfterDelay = () => delaySynchronousResult(async () => { await wait(10); throw new Error('timeout error'); }); const sandbox = new Sandbox(); const scope = { failAfterDelay }; await expect( sandbox.compileAsync('return failAfterDelay();')(scope).run(), ).rejects.toThrow('timeout error'); }); it('can catch a delayed setTimeout rejection inside sandbox code', async () => { const failAfterDelay = () => delaySynchronousResult(async () => { await wait(10); throw new Error('oops'); }); const sandbox = new Sandbox(); const scope = { failAfterDelay, msg: '' }; await sandbox .compileAsync( ` try { failAfterDelay(); } catch (e) { msg = e.message; } `, )(scope) .run(); expect(scope.msg).toBe('oops'); }); it('works via a method on an object', async () => { const api = { fetch: (val: number) => delaySynchronousResult(async () => { await wait(10); return val; }), }; const sandbox = new Sandbox(); const scope = { api }; const result = await sandbox.compileAsync('return api.fetch(99);')(scope).run(); expect(result).toBe(99); }); }); ================================================ FILE: test/eval/README.md ================================================ # Test Files Organization This directory contains organized test files for the SandboxJS evaluation engine. ## Structure - **testCases/** - Directory containing all test-related files: - `*.spec.ts` - Category-specific test spec files (Jest tests) - `*.data.ts` - Test data arrays (exported and imported by spec files) - `index.ts` - Central export that re-exports all test data - `types.ts` - TypeScript interface definitions for test cases - `test-utils.ts` - Reusable `run()` and `getState()` functions - **export-tests.ts** - Script to consolidate all tests into tests.json - **tests.json** - All tests exported to a single JSON file ## Test Categories Tests are organized into the following categories: - Data Types - Security - Arithmetic Operators - Assignment Operators - Bitwise Operators - Comments - Comparison Operators - Complex Expressions - Conditionals - Error Handling - Functions - Logical Operators - Loops - Objects & Arrays - Operator Precedence - Other Operators - Switch - Template Literals ## Exporting Tests to JSON To regenerate the `tests.json` file from the individual test files: ```bash cd test/eval npx tsx export-tests.ts ``` This script: 1. Imports all test arrays from `testCases/index.ts` 2. Consolidates them into a single array 4. Orders the tests with: 3. Orders the tests with: - **Data Types** tests first - **Security** tests second - All other categories in the order they appear 4. Preserves the original order of tests within each category 5 ### Output The script will display: - Number of tests extracted from each file - Total test count - Tests per category distribution Example output: ``` Extracted 36 tests from data-types.spec.ts Extracted 39 tests from security.spec.ts ... Total tests: 340 Exported to: /Users/nyariv/workspace/SandboxJS/test/eval/tests.json ``` ## Running Tests Run all tests: ```bash npm test ``` Run specific category tests: ```bash npm test -- test/eval/testCases/data-types.spec.ts npm test -- test/eval/testCases/security.spec.ts ``` ================================================ FILE: test/eval/reveseLinkedList.js ================================================ function LinkedListNode(value) { this.value = value; this.next = null; } function reverse(head) { let node = head, previous, tmp; while (node) { // save next before we overwrite node.next! tmp = node.next; // reverse pointer node.next = previous; // step forward in the list previous = node; node = tmp; } return previous; } function reverse(head) { if (!head || !head.next) { return head; } let tmp = reverse(head.next); head.next.next = head; head.next = undefined; return tmp; } ================================================ FILE: test/eval/script.js ================================================ // @ts-nocheck import Sandbox, { LocalScope, SandboxExecutionQuotaExceededError } from '../../dist/esm/Sandbox.js'; import { EditorState } from 'https://esm.sh/@codemirror/state@6'; import { EditorView, keymap, lineNumbers, highlightActiveLine, highlightActiveLineGutter, drawSelection } from 'https://esm.sh/@codemirror/view@6'; import { defaultKeymap, history, historyKeymap, indentWithTab } from 'https://esm.sh/@codemirror/commands@6'; import { javascript } from 'https://esm.sh/@codemirror/lang-javascript@6'; import { oneDark } from 'https://esm.sh/@codemirror/theme-one-dark@6'; import { bracketMatching, indentOnInput, syntaxHighlighting, defaultHighlightStyle } from 'https://esm.sh/@codemirror/language@6'; import { closeBrackets, closeBracketsKeymap } from 'https://esm.sh/@codemirror/autocomplete@6'; window['Sandbox'] = Sandbox; const testsPromise = fetch('test/eval/tests.json').then((res) => res.json()); const SANDBOX_HASH_KEY = 'sandbox'; const SANDBOX_SETTINGS_HASH_KEY = 'settings'; const runtimeTypeEl = document.getElementById('runtime-type'); const jitParsingEl = document.getElementById('jit-parsing'); const runBtnEl = document.getElementById('run-btn'); const openSandboxModalBtnEl = document.getElementById('open-sandbox-modal-btn'); const sandboxModalEl = document.getElementById('sandbox-modal'); const sandboxEditorEl = document.getElementById('sandbox-editor'); const sandboxConsoleOutputEl = document.getElementById('sandbox-console-output'); const sandboxReturnOutputEl = document.getElementById('sandbox-return-output'); const sandboxStatusEl = document.getElementById('sandbox-status'); const sandboxBypassNoticeEl = document.getElementById('sandbox-bypass-notice'); const sandboxBypassIconEl = document.getElementById('sandbox-bypass-icon'); const sandboxBypassTextEl = document.getElementById('sandbox-bypass-text'); const sandboxTicksOutputEl = document.getElementById('sandbox-ticks-output'); const sandboxExecBtnEl = document.getElementById('sandbox-exec-btn'); const sandboxCloseBtnEl = document.getElementById('sandbox-close-btn'); const sandboxClearBtnEl = document.getElementById('sandbox-clear-btn'); const sandboxSettingsBtnEl = document.getElementById('sandbox-settings-btn'); const sandboxSettingsPanelEl = document.getElementById('sandbox-settings-panel'); const sandboxSettingsBadgeEl = document.getElementById('sandbox-settings-badge'); const sandboxSettingsResetBtnEl = document.getElementById('sandbox-settings-reset-btn'); const settingForbidCallsEl = document.getElementById('setting-forbid-calls'); const settingForbidCreationEl = document.getElementById('setting-forbid-creation'); const settingHaltOnErrorEl = document.getElementById('setting-halt-on-error'); const settingQuotaEl = document.getElementById('setting-quota'); const settingScopeEl = document.getElementById('setting-scope'); const settingAsyncEl = document.getElementById('setting-async'); // ── CodeMirror editor ──────────────────────────────────────── const editorView = new EditorView({ state: EditorState.create({ doc: '', extensions: [ history(), lineNumbers(), highlightActiveLine(), highlightActiveLineGutter(), drawSelection(), indentOnInput(), bracketMatching(), closeBrackets(), syntaxHighlighting(defaultHighlightStyle, { fallback: true }), keymap.of([{ key: 'Mod-Enter', run: () => { runSandboxCode(); return true; } }, ...closeBracketsKeymap, ...defaultKeymap, ...historyKeymap, indentWithTab]), javascript(), oneDark, EditorView.updateListener.of((update) => { if (update.docChanged) { setSandboxHashCode(editorView.state.doc.toString()); setBypassNotice(false); setSandboxStatus('Idle'); } }), EditorView.theme({ '&': { borderRadius: '12px', overflow: 'hidden' }, '.cm-scroller': { minHeight: '240px', fontFamily: "'Fira Code', 'Cascadia Code', monospace", fontSize: '0.84rem', lineHeight: '1.6' }, '.cm-focused': { outline: 'none' }, }), ], }), parent: sandboxEditorEl, }); const getEditorValue = () => editorView.state.doc.toString(); const setEditorValue = (value) => { editorView.dispatch({ changes: { from: 0, to: editorView.state.doc.length, insert: value }, }); }; // ───────────────────────────────────────────────────────────── // ── Settings ───────────────────────────────────────────────── const getSandboxSettings = () => { const quotaRaw = settingQuotaEl.value.trim(); let executionQuota; if (quotaRaw) { const n = Number(quotaRaw); executionQuota = Number.isInteger(n) && n >= 0 ? BigInt(n) : undefined; } return { forbidFunctionCalls: settingForbidCallsEl.checked, forbidFunctionCreation: settingForbidCreationEl.checked, haltOnSandboxError: settingHaltOnErrorEl.checked, async: settingAsyncEl.checked, executionQuota, scopeJson: settingScopeEl.value.trim(), }; }; const validateSettings = () => { const errors = []; const quotaRaw = settingQuotaEl.value.trim(); if (quotaRaw) { const n = Number(quotaRaw); if (!Number.isInteger(n) || n < 0) { errors.push('Execution quota must be a non-negative integer.'); } setFieldError(settingQuotaEl, !Number.isInteger(n) || n < 0); } else { setFieldError(settingQuotaEl, false); } const scopeRaw = settingScopeEl.value.trim(); if (scopeRaw) { let parsed; try { parsed = JSON.parse(scopeRaw); } catch { parsed = null; } const bad = parsed === null || typeof parsed !== 'object' || Array.isArray(parsed); if (bad) errors.push('Scope must be a valid JSON object (e.g. {"x": 1}).'); setFieldError(settingScopeEl, bad); } else { setFieldError(settingScopeEl, false); } return errors; }; const setFieldError = (el, hasError) => { el.classList.toggle('settings-field-error', hasError); }; const countNonDefaultSettings = () => { let n = 0; if (settingForbidCallsEl.checked) n++; if (settingForbidCreationEl.checked) n++; if (settingHaltOnErrorEl.checked) n++; if (settingAsyncEl.checked) n++; if (settingQuotaEl.value.trim()) n++; if (settingScopeEl.value.trim()) n++; return n; }; const updateSettingsBadge = () => { const n = countNonDefaultSettings(); if (n > 0) { sandboxSettingsBadgeEl.textContent = n; sandboxSettingsBadgeEl.classList.remove('hidden'); } else { sandboxSettingsBadgeEl.classList.add('hidden'); } }; const resetSettings = () => { settingForbidCallsEl.checked = false; settingForbidCreationEl.checked = false; settingHaltOnErrorEl.checked = false; settingAsyncEl.checked = false; settingQuotaEl.value = ''; settingScopeEl.value = ''; setFieldError(settingQuotaEl, false); setFieldError(settingScopeEl, false); updateSettingsHashAndBadge(); }; const applySettingsFromObject = (settings) => { settingForbidCallsEl.checked = Boolean(settings.forbidFunctionCalls); settingForbidCreationEl.checked = Boolean(settings.forbidFunctionCreation); settingHaltOnErrorEl.checked = Boolean(settings.haltOnSandboxError); settingAsyncEl.checked = Boolean(settings.async); settingQuotaEl.value = settings.executionQuota != null ? String(settings.executionQuota) : ''; settingScopeEl.value = settings.scopeJson || ''; setFieldError(settingQuotaEl, false); setFieldError(settingScopeEl, false); updateSettingsBadge(); }; const parseScopeJson = (json) => { if (!json) return {}; try { return JSON.parse(json); } catch { return {}; } }; const updateSettingsHashAndBadge = () => { updateSettingsBadge(); const code = getEditorValue(); setSandboxHashCode(code); }; [settingForbidCallsEl, settingForbidCreationEl, settingHaltOnErrorEl, settingAsyncEl].forEach(el => { el.addEventListener('change', updateSettingsHashAndBadge); }); settingQuotaEl.addEventListener('input', () => { updateSettingsHashAndBadge(); validateSettings(); }); settingScopeEl.addEventListener('input', () => { updateSettingsHashAndBadge(); validateSettings(); }); settingScopeEl.addEventListener('keydown', (e) => { const el = settingScopeEl; const start = el.selectionStart; const end = el.selectionEnd; const val = el.value; const pairs = { '{': '}', '[': ']', '"': '"' }; const closers = new Set(['}', ']', '"']); if (e.key in pairs) { const close = pairs[e.key]; // For quotes: skip if next char is already a closing quote if (e.key === '"' && val[start] === '"') { e.preventDefault(); el.setSelectionRange(start + 1, start + 1); return; } e.preventDefault(); const selected = val.slice(start, end); const insert = e.key + selected + close; el.value = val.slice(0, start) + insert + val.slice(end); el.setSelectionRange(start + 1, start + 1); updateSettingsHashAndBadge(); validateSettings(); return; } if (e.key === 'Backspace' && start === end) { const before = val[start - 1]; const after = val[start]; if (before in pairs && pairs[before] === after) { e.preventDefault(); el.value = val.slice(0, start - 1) + val.slice(start + 1); el.setSelectionRange(start - 1, start - 1); updateSettingsHashAndBadge(); validateSettings(); return; } } // Skip over a closing char if already present if (closers.has(e.key) && val[start] === e.key) { e.preventDefault(); el.setSelectionRange(start + 1, start + 1); return; } if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) return; if (e.key === 'Enter') { e.preventDefault(); // Smart indent: match current line's indentation, add extra level inside { or [ const lineStart = val.lastIndexOf('\n', start - 1) + 1; const currentLine = val.slice(lineStart, start); const indent = currentLine.match(/^(\s*)/)[1]; const charBefore = val[start - 1]; const charAfter = val[end]; let insert, cursorOffset; if ((charBefore === '{' && charAfter === '}') || (charBefore === '[' && charAfter === ']')) { const newIndent = indent + ' '; insert = '\n' + newIndent + '\n' + indent; cursorOffset = 1 + newIndent.length; } else { insert = '\n' + indent; cursorOffset = insert.length; } el.value = val.slice(0, start) + insert + val.slice(end); el.setSelectionRange(start + cursorOffset, start + cursorOffset); updateSettingsHashAndBadge(); validateSettings(); } }); sandboxSettingsBtnEl.addEventListener('click', () => { const isOpen = !sandboxSettingsPanelEl.classList.contains('hidden'); sandboxSettingsPanelEl.classList.toggle('hidden', isOpen); sandboxSettingsBtnEl.classList.toggle('active', !isOpen); }); sandboxSettingsResetBtnEl.addEventListener('click', resetSettings); // ───────────────────────────────────────────────────────────── const clonePrototypeWhitelist = () => { const prototypeWhitelist = new Map(); for (const [key, value] of Sandbox.SAFE_PROTOTYPES.entries()) { prototypeWhitelist.set(key, new Set(value)); } return prototypeWhitelist; }; const freeze = Object.freeze; const Function = globalThis.Function; const createSandboxInstance = (extraGlobals = {}, sandboxOptions = {}) => { const prototypeWhitelist = clonePrototypeWhitelist(); const globals = { ...Sandbox.SAFE_GLOBALS, setTimeout, ...extraGlobals }; const functionReplacements = new Map(); functionReplacements.set([].filter, () => [].filter) const sandbox = new Sandbox({ prototypeWhitelist, globals, functionReplacements, ...sandboxOptions }); return { sandbox }; }; const encodeBase64Url = (value) => { const bytes = new TextEncoder().encode(value); let binary = ''; for (const byte of bytes) binary += String.fromCharCode(byte); return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); }; const decodeBase64Url = (value) => { const normalized = value.replace(/-/g, '+').replace(/_/g, '/'); const padding = normalized.length % 4 === 0 ? '' : '='.repeat(4 - (normalized.length % 4)); const binary = atob(normalized + padding); const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0)); return new TextDecoder().decode(bytes); }; const getHashParams = () => new URLSearchParams(window.location.hash.replace(/^#/, '')); const getSandboxHashCode = () => { const encoded = getHashParams().get(SANDBOX_HASH_KEY); if (!encoded) return ''; try { return decodeBase64Url(encoded); } catch { return ''; } }; const serializeSettings = (settings) => { const obj = {}; if (settings.forbidFunctionCalls) obj.forbidFunctionCalls = true; if (settings.forbidFunctionCreation) obj.forbidFunctionCreation = true; if (settings.haltOnSandboxError) obj.haltOnSandboxError = true; if (settings.async) obj.async = true; if (settings.executionQuota != null) obj.executionQuota = String(settings.executionQuota); if (settings.scopeJson) obj.scopeJson = settings.scopeJson; return Object.keys(obj).length ? JSON.stringify(obj) : null; }; const deserializeSettings = (str) => { try { const obj = JSON.parse(str); return { forbidFunctionCalls: Boolean(obj.forbidFunctionCalls), forbidFunctionCreation: Boolean(obj.forbidFunctionCreation), haltOnSandboxError: Boolean(obj.haltOnSandboxError), async: Boolean(obj.async), executionQuota: obj.executionQuota != null ? BigInt(obj.executionQuota) : undefined, scopeJson: obj.scopeJson || '', }; } catch { return null; } }; const getSandboxHashSettings = () => { const encoded = getHashParams().get(SANDBOX_SETTINGS_HASH_KEY); if (!encoded) return null; try { return deserializeSettings(decodeBase64Url(encoded)); } catch { return null; } }; const setSandboxHashCode = (value) => { const params = getHashParams(); if (value) params.set(SANDBOX_HASH_KEY, encodeBase64Url(value)); else params.delete(SANDBOX_HASH_KEY); const settingsStr = serializeSettings(getSandboxSettings()); if (settingsStr) params.set(SANDBOX_SETTINGS_HASH_KEY, encodeBase64Url(settingsStr)); else params.delete(SANDBOX_SETTINGS_HASH_KEY); const nextHash = params.toString(); const nextUrl = `${window.location.pathname}${window.location.search}${nextHash ? `#${nextHash}` : ''}`; window.history.replaceState(null, '', nextUrl); }; const buildSandboxHashHref = (value, settings = null) => { const params = new URLSearchParams(); if (value) params.set(SANDBOX_HASH_KEY, encodeBase64Url(value)); if (settings) { const str = serializeSettings(settings); if (str) params.set(SANDBOX_SETTINGS_HASH_KEY, encodeBase64Url(str)); } return `${window.location.pathname}${window.location.search}#${params.toString()}`; }; const getRunnableCode = (code) => { const needsReturn = !code.includes(';') && !code.startsWith('throw'); return `${needsReturn ? 'return ' : ''}${code}`; }; const resetSandboxOutput = () => { sandboxConsoleOutputEl.textContent = 'No logs yet.'; sandboxReturnOutputEl.textContent = 'Run code to inspect the result.'; sandboxTicksOutputEl.textContent = '—'; }; const setBypassNotice = (isBypassed) => { sandboxBypassNoticeEl.classList.remove('sandbox-notice-safe', 'sandbox-notice-critical'); if (isBypassed) { sandboxBypassNoticeEl.classList.add('sandbox-notice-critical'); sandboxBypassIconEl.textContent = '!'; sandboxBypassTextEl.textContent = '`globalThis.bypassed` is truthy. Critical notice: the sandbox protections were bypassed.'; return; } sandboxBypassNoticeEl.classList.add('sandbox-notice-safe'); sandboxBypassIconEl.textContent = '✓'; sandboxBypassTextEl.textContent = '`globalThis.bypassed` is falsy.'; }; const setSandboxStatus = (text, type = '') => { sandboxStatusEl.textContent = text; sandboxStatusEl.classList.remove('status-running', 'status-success', 'status-error', 'status-halted'); if (type) sandboxStatusEl.classList.add(`status-${type}`); }; const openSandboxModal = () => { sandboxModalEl.classList.remove('hidden'); sandboxModalEl.setAttribute('aria-hidden', 'false'); document.body.classList.add('modal-open'); editorView.focus(); }; const openSandboxModalWithCode = (code, settings = null) => { setEditorValue(code); resetSandboxOutput(); setBypassNotice(false); setSandboxStatus('Idle'); if (settings) applySettingsFromObject(settings); setSandboxHashCode(code); openSandboxModal(); }; const closeSandboxModal = () => { sandboxModalEl.classList.add('hidden'); sandboxModalEl.setAttribute('aria-hidden', 'true'); document.body.classList.remove('modal-open'); const params = getHashParams(); params.delete(SANDBOX_HASH_KEY); params.delete(SANDBOX_SETTINGS_HASH_KEY); const nextHash = params.toString(); const nextUrl = `${window.location.pathname}${window.location.search}${nextHash ? `#${nextHash}` : ''}`; window.history.replaceState(null, '', nextUrl); }; const formatValue = (value) => { if (typeof value === 'string') return value; if (typeof value === 'undefined') return 'undefined'; if (typeof value === 'function') return value.toString(); if (typeof value === 'symbol') return value.toString(); if (typeof value === 'bigint') return `${value}n`; if (value instanceof Error) return `${value.name}: ${value.message}`; const seen = new WeakSet(); try { return JSON.stringify(value, (_key, item) => { if (typeof item === 'bigint') return `${item}n`; if (typeof item === 'function') return item.toString(); if (typeof item === 'symbol') return item.toString(); if (!item || typeof item !== 'object') return item; if (seen.has(item)) return '[Circular]'; seen.add(item); return item; }, 2); } catch { return String(value); } }; const createUserRunner = (sandbox, code, isAsync, optimize) => { return isAsync ? sandbox.compileAsync(code, optimize) : sandbox.compile(code, optimize); }; const createSandboxRunnerScope = (extraScope = {}) => { const scope = { type: 'Sandbox', test: [(a, b) => 1], test2: 1, a: { b: { c: 2 } }, ...extraScope, }; Object.setPrototypeOf(scope, LocalScope.prototype); return scope; }; const runSandboxCode = async () => { const code = getEditorValue(); if (!code.trim()) { resetSandboxOutput(); setSandboxStatus('Idle'); return; } const settingsErrors = validateSettings(); if (settingsErrors.length) { setSandboxStatus('Invalid settings', 'error'); sandboxConsoleOutputEl.textContent = settingsErrors.join('\n'); sandboxReturnOutputEl.textContent = '—'; sandboxSettingsPanelEl.classList.remove('hidden'); sandboxSettingsBtnEl.classList.add('active'); return; } setSandboxHashCode(code); setSandboxStatus('Running', 'running'); sandboxConsoleOutputEl.textContent = 'Running...'; sandboxReturnOutputEl.textContent = 'Running...'; setBypassNotice(false); globalThis.bypassed = false; const logs = []; const pushLog = (level, args) => { const prefix = level === 'log' ? '' : `[${level}] `; logs.push(prefix + args.map((arg) => formatValue(arg)).join(' ')); }; const sandboxConsole = { log: (...args) => pushLog('log', args), info: (...args) => pushLog('info', args), warn: (...args) => pushLog('warn', args), error: (...args) => pushLog('error', args), debug: (...args) => pushLog('debug', args), }; const settings = getSandboxSettings(); const sandboxOptions = {}; if (settings.forbidFunctionCalls) sandboxOptions.forbidFunctionCalls = true; if (settings.forbidFunctionCreation) sandboxOptions.forbidFunctionCreation = true; if (settings.haltOnSandboxError) sandboxOptions.haltOnSandboxError = true; if (settings.executionQuota != null) sandboxOptions.executionQuota = settings.executionQuota; const { sandbox } = createSandboxInstance({}, sandboxOptions); window.sandbox = sandbox; sandbox.subscribeHalt((haltContext) => { const ticks = runner?.context?.ctx?.ticks?.ticks; sandboxConsoleOutputEl.textContent = logs.length ? logs.join('\n') : 'No logs.'; sandboxTicksOutputEl.textContent = ticks != null ? ticks.toLocaleString() : '—'; setBypassNotice(Boolean(globalThis.bypassed)); if (haltContext.type === 'error') { const isQuota = haltContext.error instanceof SandboxExecutionQuotaExceededError; const label = 'Halted'; const reason = haltContext.error?.message || String(haltContext.error); sandboxReturnOutputEl.textContent = reason; setSandboxStatus(`${label}: ${reason}`, 'halted'); } else { sandboxReturnOutputEl.textContent = haltContext.type; setSandboxStatus(`Halted: ${haltContext.type}`, 'halted'); } }); const extraScope = parseScopeJson(settings.scopeJson); const scope = createSandboxRunnerScope({ console: sandboxConsole, cheat: () => { globalThis.bypassed = true; sandboxConsole.error("cheat() was called!") }, ...extraScope }); let runner; try { const execFn = createUserRunner( sandbox, code, settings.async || runtimeTypeEl.value === 'async', jitParsingEl.checked, ); runner = execFn(scope, new LocalScope()); const result = await runner.run(); if (sandbox.halted) return; sandboxConsoleOutputEl.textContent = logs.length ? logs.join('\n') : 'No logs.'; sandboxReturnOutputEl.textContent = formatValue(result); sandboxTicksOutputEl.textContent = runner.context.ctx.ticks.ticks.toLocaleString(); setBypassNotice(Boolean(globalThis.bypassed)); setSandboxStatus('Success', 'success'); } catch (error) { if (sandbox.halted) return; console.error(error); sandboxConsoleOutputEl.textContent = logs.length ? logs.join('\n') : 'No logs.'; sandboxReturnOutputEl.textContent = formatValue(error); sandboxTicksOutputEl.textContent = runner ? String(runner.context.ctx.ticks.ticks.toLocaleString()) : '—'; setBypassNotice(Boolean(globalThis.bypassed)); setSandboxStatus('Error', 'error'); } }; const exec = async () => { const tests = await testsPromise; delete Object.anything; delete {}.constructor.anything1; window.bypassed = false; const isAsync = runtimeTypeEl.value === 'async'; const jit = jitParsingEl.checked; const { sandbox } = createSandboxInstance(); window.sandbox = sandbox; class TestError { constructor(error) { this.error = error; } } const validate = (value, compare) => { if (compare === 'error') return value instanceof TestError; if (typeof compare === 'string' && compare.startsWith('/') && compare.endsWith('/')) { const reg = new RegExp(compare.substring(1, compare.length - 1)); return Boolean(value?.error?.message) && reg.test(value.error.message); } if (compare === null) return compare === value; if (compare === 'NaN') return isNaN(value) && typeof value === 'number'; if (typeof compare !== 'object') return value === compare; let res = value?.length === compare?.length; for (let i in compare) res = res && validate(value?.[i], compare[i]); return res; }; // Group tests by category const categories = []; const categoryMap = new Map(); for (const test of tests) { const cat = test.category || 'Uncategorized'; if (!categoryMap.has(cat)) { categoryMap.set(cat, []); categories.push(cat); } categoryMap.get(cat).push(test); } // Clear main const main = document.getElementById('tests-main'); const summaryBar = document.getElementById('summary-bar'); main.querySelectorAll('.test-section').forEach(el => el.remove()); summaryBar.innerHTML = ''; // Build category nav const catList = document.getElementById('category-list'); catList.innerHTML = ''; // Perf counters let totalCompileNative = 0, totalCompileSandbox = 0; let totalExecuteNative = 0, totalExecuteSandbox = 0; let grandTotal = 0, grandPass = 0; // Results per section (fill in after running) const sectionResults = new Map(); // Run all tests and build DOM for (const cat of categories) { const catTests = categoryMap.get(cat); const section = document.createElement('div'); section.className = 'test-section'; section.id = 'section-' + cat.replace(/[^a-z0-9]/gi, '-').toLowerCase(); const headerEl = document.createElement('div'); headerEl.className = 'section-header'; const titleEl = document.createElement('span'); titleEl.className = 'section-title'; titleEl.textContent = cat; const countEl = document.createElement('span'); countEl.className = 'section-count'; countEl.textContent = catTests.length; const rateEl = document.createElement('span'); rateEl.className = 'section-pass-rate'; const toggleEl = document.createElement('span'); toggleEl.className = 'section-toggle'; toggleEl.textContent = '▾'; headerEl.append(titleEl, countEl, rateEl, toggleEl); headerEl.addEventListener('click', () => { section.classList.toggle('collapsed'); }); const body = document.createElement('div'); body.className = 'section-body'; const table = document.createElement('table'); table.className = 'test-table'; table.innerHTML = ` Code eval Sandbox.js Result `; const tbody = document.createElement('tbody'); table.appendChild(tbody); body.appendChild(table); section.append(headerEl, body); main.appendChild(section); let catPass = 0; for (const test of catTests) { let sandbox = createSandboxInstance().sandbox; const state = { type: 'eval', test: [(a, b) => 1], test2: 1, a: { b: { c: 2 } }, Object, Math, Date, Array, undefined, NaN, Error }; const state2 = createSandboxRunnerScope(); sandbox.context.ticks.ticks = 0n; bypassed = false; const tr = document.createElement('tr'); const runnableCode = getRunnableCode(test.code); // Sandbox.js column const sbResultTd = document.createElement('td'); sbResultTd.className = 'td-result'; let emsg = ''; let time = performance.now(); let ret; try { const fn = isAsync ? sandbox.compileAsync(runnableCode) : sandbox.compile(runnableCode); totalCompileSandbox += performance.now() - time; time = performance.now(); ret = await fn(state2, new LocalScope()).run(); totalExecuteSandbox += performance.now() - time; } catch (e) { emsg = e?.message; sbResultTd.classList.add('error'); ret = new TestError(e); } let res; try { res = ret instanceof TestError ? ret : await ret; } catch (e) { emsg = e?.message; sbResultTd.classList.add('error'); res = new TestError(e); } sbResultTd.title = emsg; sbResultTd.textContent = bypassed ? 'bypassed' : res instanceof TestError ? 'Error' : isNaN(res) && typeof res === 'number' ? 'NaN' : JSON.stringify(res) + (ret instanceof Promise ? ' (Promise)' : ''); // Verdict column const verdictTd = document.createElement('td'); verdictTd.className = 'td-verdict'; const valid = validate(res, test.safeExpect); const passed = valid && !bypassed; if (passed) catPass++; verdictTd.textContent = bypassed ? 'BYPASSED' : valid ? 'PASS' : 'FAIL'; verdictTd.classList.add(passed ? 'positive' : 'negative'); // eval column const evalTd = document.createElement('td'); evalTd.className = 'td-result'; let evalEmsg = ''; const evall = () => { if (isAsync) { return new Function('sandbox', `return (async () => {with (sandbox) {\n${runnableCode}\n}})()`); } return new Function('sandbox', `with (sandbox) {\n${runnableCode}\n}`); }; const proxy = new Proxy(state, { has(target, key, context) { if (key in state) return Reflect.has(target, key, context); if (key === 'x') return false; throw new Error('Not allowed: ' + key); }, }); bypassed = false; time = performance.now(); try { const fn = evall(); totalCompileNative += performance.now() - time; time = performance.now(); const evalRet = test.code.includes('__proto__') ? undefined : await fn(proxy); totalExecuteNative += performance.now() - time; const evalRes = await evalRet; evalTd.textContent = bypassed ? 'bypassed' : isNaN(evalRet) && typeof evalRes === 'number' ? 'NaN' : JSON.stringify(evalRes) + (evalRet instanceof Promise ? ' (Promise)' : ''); } catch (e) { evalEmsg = e?.message; evalTd.classList.add('error'); evalTd.textContent = 'Error'; } evalTd.title = evalEmsg; evalTd.classList.toggle('negative', bypassed); // Code column const codeTd = document.createElement('td'); codeTd.className = 'td-code'; codeTd.title = test.code; const codeWrap = document.createElement('div'); codeWrap.className = 'td-code-wrap'; const codeText = document.createElement('span'); codeText.className = 'td-code-text'; codeText.textContent = test.code.length > 60 ? test.code.substring(0, 60) + '…' : test.code; const runnerLink = document.createElement('a'); runnerLink.className = 'runner-link'; const testSettings = { forbidFunctionCalls: false, forbidFunctionCreation: false, haltOnSandboxError: false, async: isAsync, executionQuota: undefined, scopeJson: '' }; runnerLink.href = buildSandboxHashHref(runnableCode, testSettings); runnerLink.title = 'Open this test in the Sandbox Runner'; runnerLink.setAttribute('aria-label', 'Open this test in the Sandbox Runner'); runnerLink.innerHTML = ` `; runnerLink.addEventListener('click', (event) => { if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0) return; event.preventDefault(); openSandboxModalWithCode(runnableCode, testSettings); }); codeWrap.append(codeText, runnerLink); codeTd.appendChild(codeWrap); tr.append(codeTd, evalTd, sbResultTd, verdictTd); tbody.appendChild(tr); Object.freeze = freeze; globalThis.Function = Function; } grandTotal += catTests.length; grandPass += catPass; // Update pass-rate badge const pct = Math.round((catPass / catTests.length) * 100); rateEl.textContent = `${catPass}/${catTests.length}`; rateEl.classList.add(pct === 100 ? 'rate-good' : 'rate-bad'); sectionResults.set(cat, { pass: catPass, total: catTests.length }); // Category nav item const li = document.createElement('li'); const button = document.createElement('button'); button.type = 'button'; button.className = 'category-link'; button.dataset.sectionId = section.id; button.innerHTML = `${cat}${catPass}/${catTests.length}`; button.addEventListener('click', () => { const header = document.querySelector('.site-header'); const offset = header ? header.offsetHeight : 0; const top = section.getBoundingClientRect().top + window.scrollY - offset; window.scrollTo({ top, behavior: 'smooth' }); }); li.appendChild(button); catList.appendChild(li); } // Summary chips const failCount = grandTotal - grandPass; summaryBar.innerHTML = `
${grandTotal} Total Tests
${grandPass} Passing
${failCount} Failing
${Math.round((grandPass / grandTotal) * 100)}% Pass Rate
`; // Performance table renderPerf(totalCompileNative, totalCompileSandbox, totalExecuteNative, totalExecuteSandbox); // Lodash benchmark (background) ;(async () => { const code = await (await fetch('https://cdn.jsdelivr.net/npm/lodash@4.17.20/lodash.min.js')).text(); let start = performance.now(); let error = ''; try { new Sandbox().compile(code, !jit); } catch (e) { error = e.message; } const slodash = performance.now() - start; start = performance.now(); new Function(code); const elodash = performance.now() - start; const timesBody = document.querySelector('#times tbody'); const tr = document.createElement('tr'); const td1 = document.createElement('td'); td1.textContent = 'Lodash compile'; const td2 = document.createElement('td'); td2.textContent = Math.round(elodash * 10) / 10 + 'ms'; const td3 = document.createElement('td'); td3.title = error; td3.textContent = Math.round(slodash * 10) / 10 + 'ms'; tr.append(td1, td2, td3); timesBody.appendChild(tr); })(); }; function renderPerf(cn, cs, en, es) { const body = document.querySelector('#times tbody'); body.innerHTML = ''; const rows = [ ['', 'eval', 'Sandbox'], ['Compile', Math.round(cn * 10) / 10 + 'ms', Math.round(cs * 10) / 10 + 'ms'], ['Execute', Math.round(en * 10) / 10 + 'ms', Math.round(es * 10) / 10 + 'ms'], ['Total', Math.round((cn + en) * 10) / 10 + 'ms', Math.round((cs + es) * 10) / 10 + 'ms'], ]; for (const [label, evalVal, sbVal] of rows) { const tr = document.createElement('tr'); const makeCell = (text, tag = 'td') => { const el = document.createElement(tag); el.textContent = text; return el; }; tr.append(makeCell(label, 'th'), makeCell(evalVal), makeCell(sbVal)); body.appendChild(tr); } } // Active section highlight in nav const observer = new IntersectionObserver((entries) => { for (const entry of entries) { const id = entry.target.id; const link = document.querySelector(`#category-list .category-link[data-section-id="${id}"]`); if (link) link.classList.toggle('active', entry.isIntersecting); } }, { threshold: 0.1 }); const observeSections = () => { observer.disconnect(); document.querySelectorAll('.test-section').forEach(s => observer.observe(s)); }; exec().then(observeSections); runBtnEl.addEventListener('click', () => exec().then(observeSections)); runtimeTypeEl.addEventListener('change', () => exec().then(observeSections)); jitParsingEl.addEventListener('change', () => exec().then(observeSections)); openSandboxModalBtnEl.addEventListener('click', openSandboxModal); sandboxExecBtnEl.addEventListener('click', runSandboxCode); sandboxCloseBtnEl.addEventListener('click', closeSandboxModal); sandboxClearBtnEl.addEventListener('click', () => { resetSettings(); setEditorValue(''); resetSandboxOutput(); setBypassNotice(false); setSandboxStatus('Idle'); }); sandboxModalEl.addEventListener('click', (event) => { if (event.target.dataset.closeModal === 'true') closeSandboxModal(); }); window.addEventListener('keydown', (event) => { if (event.key === 'Escape' && !sandboxModalEl.classList.contains('hidden')) closeSandboxModal(); }); window.addEventListener('hashchange', () => { const code = getSandboxHashCode(); if (!code) return; const settings = getSandboxHashSettings(); if (settings) applySettingsFromObject(settings); setEditorValue(code); openSandboxModal(); setSandboxStatus('Idle'); }); const initialSandboxCode = getSandboxHashCode(); if (initialSandboxCode) { const initialSettings = getSandboxHashSettings(); if (initialSettings) applySettingsFromObject(initialSettings); setEditorValue(initialSandboxCode); openSandboxModal(); } setBypassNotice(false); ================================================ FILE: test/eval/testCases/arithmetic-operators.data.ts ================================================ 'use strict'; import { TestCase } from './types.js'; export const tests: TestCase[] = [ { code: '1+1', evalExpect: 2, safeExpect: 2, category: 'Arithmetic Operators', }, { code: '1 * 2 + 3 * (4 + 5) * 6', evalExpect: 164, safeExpect: 164, category: 'Arithmetic Operators', }, { code: '(test2 * (2 + 3 * (4 + 5))) * 6', evalExpect: 174, safeExpect: 174, category: 'Arithmetic Operators', }, { code: '1+2*4/5-6+7/8 % 9+10-11-12/13*14', evalExpect: -16.448076923076925, safeExpect: -16.448076923076925, category: 'Arithmetic Operators', }, { code: 'test2 **= 0', evalExpect: 1, safeExpect: 1, category: 'Arithmetic Operators', }, { code: '2 ** 3', evalExpect: 8, safeExpect: 8, category: 'Arithmetic Operators', }, { code: '10 ** 0', evalExpect: 1, safeExpect: 1, category: 'Arithmetic Operators', }, { code: '2 ** 10', evalExpect: 1024, safeExpect: 1024, category: 'Arithmetic Operators', }, { code: '3 ** 2 ** 2', evalExpect: 81, safeExpect: 81, category: 'Arithmetic Operators', }, { code: 'test2 %= 1', evalExpect: 0, safeExpect: 0, category: 'Arithmetic Operators', }, { code: "+'1'", evalExpect: 1, safeExpect: 1, category: 'Arithmetic Operators', }, { code: "-'1'", evalExpect: -1, safeExpect: -1, category: 'Arithmetic Operators', }, { code: 'var i = 1; return i + 1', evalExpect: 'error', safeExpect: 2, category: 'Arithmetic Operators', }, { code: 'let j = 1; return j + 1', evalExpect: 'error', safeExpect: 2, category: 'Arithmetic Operators', }, { code: 'const k = 1; return k + 1', evalExpect: 'error', safeExpect: 2, category: 'Arithmetic Operators', }, { code: 'null?.a + 5', evalExpect: 'NaN', safeExpect: 'NaN', category: 'Arithmetic Operators', }, { code: '({}).a ?? 10 + 5', evalExpect: 15, safeExpect: 15, category: 'Arithmetic Operators', }, { code: 'let x = 5; return x++ + 2', evalExpect: 7, safeExpect: 7, category: 'Arithmetic Operators', }, { code: 'let y = 5; return ++y + 2', evalExpect: 8, safeExpect: 8, category: 'Arithmetic Operators', }, { code: '+-5', evalExpect: -5, safeExpect: -5, category: 'Arithmetic Operators', }, { code: '~-1', evalExpect: 0, safeExpect: 0, category: 'Arithmetic Operators', }, { code: '(1, 2) + (3, 4)', evalExpect: 6, safeExpect: 6, category: 'Arithmetic Operators', }, { code: 'let z = 5; return --z * 2', evalExpect: 8, safeExpect: 8, category: 'Arithmetic Operators', }, { code: 'let z = 5; return z-- * 2', evalExpect: 10, safeExpect: 10, category: 'Arithmetic Operators', }, { code: '1 + 2 + 3 + 4', evalExpect: 10, safeExpect: 10, category: 'Arithmetic Operators', }, { code: '10 - 5 - 2', evalExpect: 3, safeExpect: 3, category: 'Arithmetic Operators', }, { code: '2 * 3 * 4', evalExpect: 24, safeExpect: 24, category: 'Arithmetic Operators', }, { code: '100 / 5 / 2', evalExpect: 10, safeExpect: 10, category: 'Arithmetic Operators', }, { code: '17 % 5 % 2', evalExpect: 0, safeExpect: 0, category: 'Arithmetic Operators', }, { code: 'let a = 1, b = 2, c = 3; return a + b + c', evalExpect: 6, safeExpect: 6, category: 'Arithmetic Operators', }, { code: 'const a = 1, b = 2; return a * b', evalExpect: 2, safeExpect: 2, category: 'Arithmetic Operators', }, { code: '(false ? 1 : 2) + (true ? 3 : 4)', evalExpect: 5, safeExpect: 5, category: 'Arithmetic Operators', }, { code: 'let x = 5; x++; return x', evalExpect: 6, safeExpect: 6, category: 'Arithmetic Operators', }, { code: 'let x = 5; return ++x', evalExpect: 6, safeExpect: 6, category: 'Arithmetic Operators', }, { code: 'let x = 5; x--; return x', evalExpect: 4, safeExpect: 4, category: 'Arithmetic Operators', }, { code: 'let x = 5; return --x', evalExpect: 4, safeExpect: 4, category: 'Arithmetic Operators', }, ]; ================================================ FILE: test/eval/testCases/arithmetic-operators.spec.ts ================================================ 'use strict'; import { run, getState } from './test-utils.js'; import { tests } from './arithmetic-operators.data.js'; describe('Arithmetic Operators Tests', () => { describe('Sync', () => { tests.forEach((test) => { let state = getState(); it(test.code.substring(0, 100), async () => { await run(test, state, false); }); }); }); describe('Async', () => { tests.forEach((test) => { let state = getState(); it(test.code.substring(0, 100), async () => { await run(test, state, true); }); }); }); }); ================================================ FILE: test/eval/testCases/assignment-operators.data.ts ================================================ 'use strict'; import { TestCase } from './types.js'; export const tests: TestCase[] = [ { code: 'test.b = test2 - test[0]() - test[0]()', evalExpect: -1, safeExpect: -1, category: 'Assignment Operators', }, { code: 'test2++', evalExpect: 1, safeExpect: 1, category: 'Assignment Operators', }, { code: '++test2', evalExpect: 2, safeExpect: 2, category: 'Assignment Operators', }, { code: 'test2 = 1', evalExpect: 1, safeExpect: 1, category: 'Assignment Operators', }, { code: 'test2 += 1', evalExpect: 2, safeExpect: 2, category: 'Assignment Operators', }, { code: 'test2 -= 1', evalExpect: 0, safeExpect: 0, category: 'Assignment Operators', }, { code: 'test2 *= 2', evalExpect: 2, safeExpect: 2, category: 'Assignment Operators', }, { code: 'test2 /= 2', evalExpect: 0.5, safeExpect: 0.5, category: 'Assignment Operators', }, { code: 'let a = 1; let a = 2; return a', evalExpect: 'error', safeExpect: "/Identifier 'a' has already been declared/", category: 'Assignment Operators', }, { code: 'var z = 1; var z = 2; return z', evalExpect: 2, safeExpect: 2, category: 'Assignment Operators', }, { code: 'let y = 1; var y = 2; return y', evalExpect: 'error', safeExpect: "/Identifier 'y' has already been declared/", category: 'Assignment Operators', }, { code: "var z = globalThis; let y = globalThis; return z.constructor.name === 'SandboxGlobal' && z === y", evalExpect: true, safeExpect: true, category: 'Assignment Operators', }, { code: 'var v = 1; var v = Object; return v.name', evalExpect: 'Object', safeExpect: 'Object', category: 'Assignment Operators', }, { code: 'let a = 1; {let a = 2}; return a', evalExpect: 1, safeExpect: 1, category: 'Assignment Operators', }, { code: 'this = 1', evalExpect: 'error', safeExpect: '/"this" cannot be assigned/', category: 'Assignment Operators', }, { code: 'const l = 1; return l = 2', evalExpect: 'error', safeExpect: '/Assignment to constant variable/', category: 'Assignment Operators', }, { code: 'delete 1', evalExpect: true, safeExpect: true, category: 'Assignment Operators', }, { code: 'let a = {b: 1}; return delete a.b', evalExpect: true, safeExpect: true, category: 'Assignment Operators', }, { code: 'let b = {a: 1}; return delete b', evalExpect: false, safeExpect: false, category: 'Assignment Operators', }, { code: 'let x = 5; x <<= 1; return x', evalExpect: 10, safeExpect: 10, category: 'Assignment Operators', }, { code: 'let x = 8; x >>= 1; return x', evalExpect: 4, safeExpect: 4, category: 'Assignment Operators', }, { code: 'let obj = {a: 1}; delete obj.a; return obj.a === undefined', evalExpect: true, safeExpect: true, category: 'Assignment Operators', }, { code: 'delete {a: 1}.a', evalExpect: true, safeExpect: true, category: 'Assignment Operators', }, { code: 'let x = 10; x <<= 2; return x;', evalExpect: 40, safeExpect: 40, category: 'Assignment Operators', }, { code: 'let x = 16; x >>= 2; return x;', evalExpect: 4, safeExpect: 4, category: 'Assignment Operators', }, { code: '(1 == 1) == true', evalExpect: true, safeExpect: true, category: 'Assignment Operators', }, { code: 'let x = (1, 2, 3); return x', evalExpect: 3, safeExpect: 3, category: 'Assignment Operators', }, { code: 'let x = 0; x = x || 5; return x', evalExpect: 5, safeExpect: 5, category: 'Assignment Operators', }, { code: 'let x = 10; x = x && 5; return x', evalExpect: 5, safeExpect: 5, category: 'Assignment Operators', }, { code: 'let x = 10; x &&= 5; return x', evalExpect: 5, safeExpect: 5, category: 'Assignment Operators', }, { code: 'let x = 0; x &&= 5; return x', evalExpect: 0, safeExpect: 0, category: 'Assignment Operators', }, { code: 'let x = false; x &&= true; return x', evalExpect: false, safeExpect: false, category: 'Assignment Operators', }, { code: 'let x = 0; x ||= 5; return x', evalExpect: 5, safeExpect: 5, category: 'Assignment Operators', }, { code: 'let x = 10; x ||= 5; return x', evalExpect: 10, safeExpect: 10, category: 'Assignment Operators', }, { code: 'let x = false; x ||= "default"; return x', evalExpect: 'default', safeExpect: 'default', category: 'Assignment Operators', }, { code: 'let x = null; x ??= 5; return x', evalExpect: 5, safeExpect: 5, category: 'Assignment Operators', }, { code: 'let x = undefined; x ??= 10; return x', evalExpect: 10, safeExpect: 10, category: 'Assignment Operators', }, { code: 'let x = 0; x ??= 5; return x', evalExpect: 0, safeExpect: 0, category: 'Assignment Operators', }, { code: 'let x = ""; x ??= "default"; return x', evalExpect: '', safeExpect: '', category: 'Assignment Operators', }, ]; ================================================ FILE: test/eval/testCases/assignment-operators.spec.ts ================================================ 'use strict'; import { run, getState } from './test-utils.js'; import { tests } from './assignment-operators.data.js'; describe('Assignment Operators Tests', () => { describe('Sync', () => { tests.forEach((test) => { let state = getState(); it(test.code.substring(0, 100), async () => { await run(test, state, false); }); }); }); describe('Async', () => { tests.forEach((test) => { let state = getState(); it(test.code.substring(0, 100), async () => { await run(test, state, true); }); }); }); }); ================================================ FILE: test/eval/testCases/bitwise-operators.data.ts ================================================ 'use strict'; import { TestCase } from './types.js'; export const tests: TestCase[] = [ { code: '~test2', evalExpect: -2, safeExpect: -2, category: 'Bitwise Operators', }, { code: 'let test3 = 8; test3 >>>= 2; return test3', evalExpect: 2, safeExpect: 2, category: 'Bitwise Operators', }, { code: 'let test4 = -8; test4 >>>= 2; return test4', evalExpect: 1073741822, safeExpect: 1073741822, category: 'Bitwise Operators', }, { code: 'let test5 = 16; test5 >>>= 1; return test5', evalExpect: 8, safeExpect: 8, category: 'Bitwise Operators', }, { code: 'let test6 = -1; test6 >>>= 0; return test6', evalExpect: 4294967295, safeExpect: 4294967295, category: 'Bitwise Operators', }, { code: 'test2 ^= 1', evalExpect: 0, safeExpect: 0, category: 'Bitwise Operators', }, { code: 'test2 &= 3', evalExpect: 1, safeExpect: 1, category: 'Bitwise Operators', }, { code: 'test2 |= 2', evalExpect: 3, safeExpect: 3, category: 'Bitwise Operators', }, { code: 'test2 & 1', evalExpect: 1, safeExpect: 1, category: 'Bitwise Operators', }, { code: 'test2 | 4', evalExpect: 5, safeExpect: 5, category: 'Bitwise Operators', }, { code: '8 >> 1 >> 1', evalExpect: 2, safeExpect: 2, category: 'Bitwise Operators', }, { code: '1 << 2 << 1', evalExpect: 8, safeExpect: 8, category: 'Bitwise Operators', }, { code: '16 >>> 2 >> 1', evalExpect: 2, safeExpect: 2, category: 'Bitwise Operators', }, { code: '1 << 1 << 1', evalExpect: 4, safeExpect: 4, category: 'Bitwise Operators', }, { code: '16 >> 1 >> 1', evalExpect: 4, safeExpect: 4, category: 'Bitwise Operators', }, { code: '5 & 7 & 3', evalExpect: 1, safeExpect: 1, category: 'Bitwise Operators', }, { code: '1 | 2 | 4', evalExpect: 7, safeExpect: 7, category: 'Bitwise Operators', }, { code: '15 ^ 10 ^ 5', evalExpect: 0, safeExpect: 0, category: 'Bitwise Operators', }, { code: '~5', evalExpect: -6, safeExpect: -6, category: 'Bitwise Operators', }, ]; ================================================ FILE: test/eval/testCases/bitwise-operators.spec.ts ================================================ 'use strict'; import { run, getState } from './test-utils.js'; import { tests } from './bitwise-operators.data.js'; describe('Bitwise Operators Tests', () => { describe('Sync', () => { tests.forEach((test) => { let state = getState(); it(test.code.substring(0, 100), async () => { await run(test, state, false); }); }); }); describe('Async', () => { tests.forEach((test) => { let state = getState(); it(test.code.substring(0, 100), async () => { await run(test, state, true); }); }); }); }); ================================================ FILE: test/eval/testCases/comments.data.ts ================================================ 'use strict'; import { TestCase } from './types.js'; export const tests: TestCase[] = [ { code: '/* 2 */ 1', evalExpect: 1, safeExpect: 1, category: 'Comments', }, { code: '1 // 2', evalExpect: 1, safeExpect: 1, category: 'Comments', }, { code: '/* 2 */ (() => /* 3 */ 1)() // 4', evalExpect: 1, safeExpect: 1, category: 'Comments', }, { code: '/* never closed', evalExpect: 'error', safeExpect: "/Unclosed comment '\\/\\*/", category: 'Comments', }, ]; ================================================ FILE: test/eval/testCases/comments.spec.ts ================================================ 'use strict'; import { run, getState } from './test-utils.js'; import { tests } from './comments.data.js'; describe('Comments Tests', () => { describe('Sync', () => { tests.forEach((test) => { let state = getState(); it(test.code.substring(0, 100), async () => { await run(test, state, false); }); }); }); describe('Async', () => { tests.forEach((test) => { let state = getState(); it(test.code.substring(0, 100), async () => { await run(test, state, true); }); }); }); }); ================================================ FILE: test/eval/testCases/comparison-operators.data.ts ================================================ 'use strict'; import { TestCase } from './types.js'; export const tests: TestCase[] = [ { code: "test2 == '1'", evalExpect: true, safeExpect: true, category: 'Comparison Operators', }, { code: "test2 === '1'", evalExpect: false, safeExpect: false, category: 'Comparison Operators', }, { code: "test2 != '1'", evalExpect: false, safeExpect: false, category: 'Comparison Operators', }, { code: "test2 !== '1'", evalExpect: true, safeExpect: true, category: 'Comparison Operators', }, { code: 'test2 < 1', evalExpect: false, safeExpect: false, category: 'Comparison Operators', }, { code: 'test2 > 1', evalExpect: false, safeExpect: false, category: 'Comparison Operators', }, { code: 'test2 >= 1', evalExpect: true, safeExpect: true, category: 'Comparison Operators', }, { code: 'test2 <= 1', evalExpect: true, safeExpect: true, category: 'Comparison Operators', }, ]; ================================================ FILE: test/eval/testCases/comparison-operators.spec.ts ================================================ 'use strict'; import { run, getState } from './test-utils.js'; import { tests } from './comparison-operators.data.js'; describe('Comparison Operators Tests', () => { describe('Sync', () => { tests.forEach((test) => { let state = getState(); it(test.code.substring(0, 100), async () => { await run(test, state, false); }); }); }); describe('Async', () => { tests.forEach((test) => { let state = getState(); it(test.code.substring(0, 100), async () => { await run(test, state, true); }); }); }); }); ================================================ FILE: test/eval/testCases/complex-expressions.data.ts ================================================ 'use strict'; import { TestCase } from './types.js'; export const tests: TestCase[] = [ { code: 'let a = null; let b = [a?.a]; return b[0] === undefined && b.length', evalExpect: 1, safeExpect: 1, category: 'Complex Expressions', }, { code: '{a: 1, ...{b: 2, c: {d: test2,}}, e: 5}', evalExpect: { a: 1, b: 2, c: { d: 1, }, e: 5, }, safeExpect: { a: 1, b: 2, c: { d: 1, }, e: 5, }, category: 'Complex Expressions', }, { code: '{a: 1,b: 2, /*,*/}', evalExpect: { a: 1, b: 2, }, safeExpect: { a: 1, b: 2, }, category: 'Complex Expressions', }, { code: 'test2 = 1,(() => 2)(),test2', evalExpect: 1, safeExpect: 1, category: 'Complex Expressions', }, { code: 'const a = () => {return 1}; const b = () => {return 2}; return (() => a() + b())()', evalExpect: 3, safeExpect: 3, category: 'Complex Expressions', }, { code: '({}).a?.toString()', category: 'Complex Expressions', }, { code: '({}).a?.toSring() + ({}).b?.toString()', evalExpect: 'NaN', safeExpect: 'NaN', category: 'Complex Expressions', }, { code: "({})['b']?.toString() === undefined", evalExpect: true, safeExpect: true, category: 'Complex Expressions', }, { code: '({}).c?.()() ? 1 : 2', evalExpect: 2, safeExpect: 2, category: 'Complex Expressions', }, { code: 'function plus(n,u){return n+u};function minus(n,u){return n-u};var added=plus(1,10);return minus(added,5);', evalExpect: 6, safeExpect: 6, category: 'Complex Expressions', }, { code: 'function LinkedListNode(e){this.value=e,this.next=null}function reverse(e){let n,t,r=e;for(;r;)t=r.next,r.next=n,n=r,r=t;return n}function reverse(e){if(!e||!e.next)return e;let n=reverse(e.next);return e.next.next=e,e.next=null,n} let l1 = new LinkedListNode(1); l1.next = new LinkedListNode(2); return reverse(l1);', evalExpect: { value: 2, next: { value: 1, next: null, }, }, safeExpect: { value: 2, next: { value: 1, next: null, }, }, category: 'Complex Expressions', }, { code: 'const f = function factorial(n) { return n <= 1 ? 1 : n * factorial(n - 1); }; return f(5)', evalExpect: 120, safeExpect: 120, category: 'Complex Expressions', }, { code: '({a: 10}).a?.toString() + 5', evalExpect: '105', safeExpect: '105', category: 'Complex Expressions', }, { code: 'let arr = [1, 2, 3]; return arr?.[1]', evalExpect: 2, safeExpect: 2, category: 'Complex Expressions', }, { code: 'let obj = {fn: () => 42}; return obj?.fn?.()', evalExpect: 42, safeExpect: 42, category: 'Complex Expressions', }, ]; ================================================ FILE: test/eval/testCases/complex-expressions.spec.ts ================================================ 'use strict'; import { run, getState } from './test-utils.js'; import { tests } from './complex-expressions.data.js'; describe('Complex Expressions Tests', () => { describe('Sync', () => { tests.forEach((test) => { let state = getState(); it(test.code.substring(0, 100), async () => { await run(test, state, false); }); }); }); describe('Async', () => { tests.forEach((test) => { let state = getState(); it(test.code.substring(0, 100), async () => { await run(test, state, true); }); }); }); }); ================================================ FILE: test/eval/testCases/conditionals.data.ts ================================================ 'use strict'; import { TestCase } from './types.js'; export const tests: TestCase[] = [ { code: "test[test2] ? true : false ? 'not ok' : 'ok'", evalExpect: 'ok', safeExpect: 'ok', category: 'Conditionals', }, { code: 'let ok = true; false ? ok = false : ok; return ok', evalExpect: true, safeExpect: true, category: 'Conditionals', }, { code: 'if (true) { return true; } else return false', evalExpect: true, safeExpect: true, category: 'Conditionals', }, { code: 'let a = false; if (a) { return false; } else return true', evalExpect: true, safeExpect: true, category: 'Conditionals', }, { code: 'let a = null; if (a?.a) { return false; } else return true', evalExpect: true, safeExpect: true, category: 'Conditionals', }, { code: 'if (false) return true; else if (false) {return true} else return false', evalExpect: false, safeExpect: false, category: 'Conditionals', }, { code: 'if (false) { return true; } else return false', evalExpect: false, safeExpect: false, category: 'Conditionals', }, { code: 'if (false) return true; return false', evalExpect: false, safeExpect: false, category: 'Conditionals', }, { code: 'if (true) {\n if (false)\n if (true)\n if (false)\n return 1\n else if (true)\n return 2\n else\n return 3\n else\n return 4\n else if (true)\n if (false)\n return 5\n else if (true)\n return 6\n else\n return 7\n else\n return 8\n} else if (true)\n return 9;', evalExpect: 6, safeExpect: 6, category: 'Conditionals', }, ]; ================================================ FILE: test/eval/testCases/conditionals.spec.ts ================================================ 'use strict'; import { run, getState } from './test-utils.js'; import { tests } from './conditionals.data.js'; describe('Conditionals Tests', () => { describe('Sync', () => { tests.forEach((test) => { let state = getState(); it(test.code.substring(0, 100), async () => { await run(test, state, false); }); }); }); describe('Async', () => { tests.forEach((test) => { let state = getState(); it(test.code.substring(0, 100), async () => { await run(test, state, true); }); }); }); }); ================================================ FILE: test/eval/testCases/data-types.data.ts ================================================ 'use strict'; import { TestCase } from './types.js'; export const tests: TestCase[] = [ { code: '`${type}`', evalExpect: 'eval', safeExpect: 'Sandbox', category: 'Data Types', }, { code: 'test2', evalExpect: 1, safeExpect: 1, category: 'Data Types', }, { code: '2.2204460492503130808472633361816E-16', evalExpect: 2.220446049250313e-16, safeExpect: 2.220446049250313e-16, category: 'Data Types', }, { code: '"test2"', evalExpect: 'test2', safeExpect: 'test2', category: 'Data Types', }, { code: '`test2 is ${`also ${test2}`}`', evalExpect: 'test2 is also 1', safeExpect: 'test2 is also 1', category: 'Data Types', }, { code: '"\\\\"', evalExpect: '\\', safeExpect: '\\', category: 'Data Types', }, { code: "`\\\\$$\\${${`\\\\\\`${'ok'}`}\\\\}`", evalExpect: '\\$$${\\`ok\\}', safeExpect: '\\$$${\\`ok\\}', category: 'Data Types', }, { code: '["\\\\", "\\xd9", "\\n", "\\r", "\\u2028", "\\u2029"]', evalExpect: ['\\', '\u00d9', '\n', '\r', '\u2028', '\u2029'], safeExpect: ['\\', '\u00d9', '\n', '\r', '\u2028', '\u2029'], category: 'Data Types', }, { code: "/a/.test('a')", evalExpect: true, safeExpect: true, category: 'Data Types', }, { code: "/a/i.test('A')", evalExpect: true, safeExpect: true, category: 'Data Types', }, { code: "let reg = /a/g; reg.exec('aaa'); return reg.exec('aaa').index", evalExpect: 1, safeExpect: 1, category: 'Data Types', }, { code: '(1n + 0x1n).toString()', evalExpect: '2', safeExpect: '2', category: 'Data Types', }, { code: '0b1010', evalExpect: 10, safeExpect: 10, category: 'Data Types', }, { code: '0B1111', evalExpect: 15, safeExpect: 15, category: 'Data Types', }, { code: '0b1010n.toString()', evalExpect: '10', safeExpect: '10', category: 'Data Types', }, { code: '0b1_000', evalExpect: 8, safeExpect: 8, category: 'Data Types', }, { code: '1_000', evalExpect: 1000, safeExpect: 1000, category: 'Data Types', }, { code: '0b0', evalExpect: 0, safeExpect: 0, category: 'Data Types', }, { code: '0o17', evalExpect: 15, safeExpect: 15, category: 'Data Types', }, { code: '0O77', evalExpect: 63, safeExpect: 63, category: 'Data Types', }, { code: '0o17n.toString()', evalExpect: '15', safeExpect: '15', category: 'Data Types', }, { code: '0o7_777', evalExpect: 4095, safeExpect: 4095, category: 'Data Types', }, { code: '0o0', evalExpect: 0, safeExpect: 0, category: 'Data Types', }, { code: '0b1010 + 0o17', evalExpect: 25, safeExpect: 25, category: 'Data Types', }, { code: '(0b1010n + 0o17n).toString()', evalExpect: '25', safeExpect: '25', category: 'Data Types', }, { code: "String(BigInt('12345'))", evalExpect: '12345', safeExpect: '12345', category: 'Data Types', }, { code: '(123_456_789n).toString()', evalExpect: '123456789', safeExpect: '123456789', category: 'Data Types', }, { code: "/test/gi.test('test')", evalExpect: true, safeExpect: true, category: 'Data Types', }, { code: "/[a-z]+/i.test('Hello')", evalExpect: true, safeExpect: true, category: 'Data Types', }, { code: 'NaN', evalExpect: 'NaN', safeExpect: 'NaN', category: 'Data Types', }, { code: "typeof Infinity === 'number'", evalExpect: true, safeExpect: true, category: 'Data Types', }, { code: "typeof undefined === 'undefined'", evalExpect: true, safeExpect: true, category: 'Data Types', }, { code: 'null', evalExpect: null, safeExpect: null, category: 'Data Types', }, { code: 'true', evalExpect: true, safeExpect: true, category: 'Data Types', }, { code: 'false', evalExpect: false, safeExpect: false, category: 'Data Types', }, { code: 'undefined === undefined', evalExpect: true, safeExpect: true, category: 'Data Types', }, ]; ================================================ FILE: test/eval/testCases/data-types.spec.ts ================================================ 'use strict'; import { run, getState } from './test-utils.js'; import { tests } from './data-types.data.js'; describe('Data Types Tests', () => { describe('Sync', () => { tests.forEach((test) => { let state = getState(); it(test.code.substring(0, 100), async () => { await run(test, state, false); }); }); }); describe('Async', () => { tests.forEach((test) => { let state = getState(); it(test.code.substring(0, 100), async () => { await run(test, state, true); }); }); }); }); ================================================ FILE: test/eval/testCases/defaults.data.ts ================================================ 'use strict'; import { TestCase } from './types.js'; export const tests: TestCase[] = [ // Basic single default { code: 'function fn(a = 1) { return a }; return fn()', evalExpect: 1, safeExpect: 1, category: 'Defaults', }, { code: 'function fn(a = 1) { return a }; return fn(5)', evalExpect: 5, safeExpect: 5, category: 'Defaults', }, // Multiple defaults { code: 'function fn(a = 1, b = 2) { return a + b }; return fn()', evalExpect: 3, safeExpect: 3, category: 'Defaults', }, { code: 'function fn(a = 1, b = 2) { return a + b }; return fn(10)', evalExpect: 12, safeExpect: 12, category: 'Defaults', }, { code: 'function fn(a = 1, b = 2) { return a + b }; return fn(10, 20)', evalExpect: 30, safeExpect: 30, category: 'Defaults', }, // Mixed required and default { code: 'function fn(a, b = 2) { return a + b }; return fn(10)', evalExpect: 12, safeExpect: 12, category: 'Defaults', }, { code: 'function fn(a, b = 2) { return a + b }; return fn(10, 20)', evalExpect: 30, safeExpect: 30, category: 'Defaults', }, // Default with expression { code: 'function fn(a = 1 + 2) { return a }; return fn()', evalExpect: 3, safeExpect: 3, category: 'Defaults', }, // Default referencing earlier param { code: 'function fn(a = 1, b = a + 1) { return b }; return fn()', evalExpect: 2, safeExpect: 2, category: 'Defaults', }, // Arrow function with defaults { code: 'const fn = (a = 10) => a; return fn()', evalExpect: 10, safeExpect: 10, category: 'Defaults', }, { code: 'const fn = (a = 10) => a; return fn(5)', evalExpect: 5, safeExpect: 5, category: 'Defaults', }, { code: 'const fn = (a = 1, b = 2) => a + b; return fn()', evalExpect: 3, safeExpect: 3, category: 'Defaults', }, // Default with null (only undefined triggers default) { code: 'function fn(a = 99) { return a }; return fn(null)', evalExpect: null, safeExpect: null, category: 'Defaults', }, // Default with rest params { code: 'function fn(a = 1, ...rest) { return a + rest.length }; return fn()', evalExpect: 1, safeExpect: 1, category: 'Defaults', }, { code: 'function fn(a = 1, ...rest) { return a + rest.length }; return fn(10, 20, 30)', evalExpect: 12, safeExpect: 12, category: 'Defaults', }, // Default with string value { code: 'function fn(name = "world") { return "hello " + name }; return fn()', evalExpect: 'hello world', safeExpect: 'hello world', category: 'Defaults', }, { code: 'function fn(name = "world") { return "hello " + name }; return fn("there")', evalExpect: 'hello there', safeExpect: 'hello there', category: 'Defaults', }, // Default is a function call { code: 'function make() { return 42 }; function fn(a = make()) { return a }; return fn()', evalExpect: 42, safeExpect: 42, category: 'Defaults', }, // Default is only evaluated when needed { code: 'let calls = 0; function make() { calls++; return 1 }; function fn(a = make()) { return a }; fn(99); return calls', evalExpect: 0, safeExpect: 0, category: 'Defaults', }, // Default evaluated each call { code: 'let n = 0; function next() { return ++n }; function fn(a = next()) { return a }; fn(); fn(); return fn()', evalExpect: 3, safeExpect: 3, category: 'Defaults', }, // Default references earlier param (already tested b = a+1, now more complex) { code: 'function fn(a = 2, b = a * 3, c = a + b) { return c }; return fn()', evalExpect: 8, safeExpect: 8, category: 'Defaults', }, // Default with destructuring param { code: 'function fn({a = 1, b = 2} = {}) { return a + b }; return fn()', evalExpect: 3, safeExpect: 3, category: 'Defaults', }, { code: 'function fn({a = 1, b = 2} = {}) { return a + b }; return fn({a: 10})', evalExpect: 12, safeExpect: 12, category: 'Defaults', }, { code: 'function fn([a = 10, b = 20] = []) { return a + b }; return fn()', evalExpect: 30, safeExpect: 30, category: 'Defaults', }, { code: 'function fn([a = 10, b = 20] = []) { return a + b }; return fn([1])', evalExpect: 21, safeExpect: 21, category: 'Defaults', }, // Default in method { code: 'const obj = { fn(a = 7) { return a } }; return obj.fn()', evalExpect: 7, safeExpect: 7, category: 'Defaults', }, // Default in recursive function { code: 'function sum(n, acc = 0) { if (n <= 0) return acc; return sum(n - 1, acc + n) }; return sum(5)', evalExpect: 15, safeExpect: 15, category: 'Defaults', }, // Passing undefined explicitly triggers default { code: 'function fn(a = 5, b = 6) { return a + b }; return fn(undefined, 10)', evalExpect: 15, safeExpect: 15, category: 'Defaults', }, // Default with array literal { code: 'function fn(a = [1, 2, 3]) { return a.length }; return fn()', evalExpect: 3, safeExpect: 3, category: 'Defaults', }, // Default with object literal { code: 'function fn(opts = { x: 10, y: 20 }) { return opts.x + opts.y }; return fn()', evalExpect: 30, safeExpect: 30, category: 'Defaults', }, // Async function with defaults { code: 'async function fn(a = 1, b = 2) { return a + b }; return fn()', evalExpect: 3, safeExpect: 3, category: 'Defaults', }, // Arrow with destructuring default { code: 'const fn = ({x = 1, y = 2} = {}) => x + y; return fn()', evalExpect: 3, safeExpect: 3, category: 'Defaults', }, { code: 'const fn = ({x = 1, y = 2} = {}) => x + y; return fn({x: 5})', evalExpect: 7, safeExpect: 7, category: 'Defaults', }, ]; ================================================ FILE: test/eval/testCases/defaults.spec.ts ================================================ 'use strict'; import { run, getState } from './test-utils.js'; import { tests } from './defaults.data.js'; describe('Parameter Default Values Tests', () => { describe('Sync', () => { tests.forEach((test) => { let state = getState(); it(test.code.substring(0, 100), async () => { await run(test, state, false); }); }); }); describe('Async', () => { tests.forEach((test) => { let state = getState(); it(test.code.substring(0, 100), async () => { await run(test, state, true); }); }); }); }); ================================================ FILE: test/eval/testCases/destructuring.data.ts ================================================ 'use strict'; import { TestCase } from './types.js'; export const tests: TestCase[] = [ // Array destructuring - basic { code: 'const [a, b] = [1, 2]; return a', evalExpect: 1, safeExpect: 1, category: 'Destructuring', }, { code: 'const [a, b] = [1, 2]; return b', evalExpect: 2, safeExpect: 2, category: 'Destructuring', }, { code: 'const [a, b, c] = [1, 2, 3]; return a + b + c', evalExpect: 6, safeExpect: 6, category: 'Destructuring', }, // Array destructuring - skipping elements { code: 'const [, b] = [1, 2]; return b', evalExpect: 2, safeExpect: 2, category: 'Destructuring', }, { code: 'const [a, , c] = [1, 2, 3]; return a + c', evalExpect: 4, safeExpect: 4, category: 'Destructuring', }, // Array destructuring - defaults { code: 'const [a = 10] = []; return a', evalExpect: 10, safeExpect: 10, category: 'Destructuring', }, { code: 'const [a = 10] = [5]; return a', evalExpect: 5, safeExpect: 5, category: 'Destructuring', }, { code: 'const [a = 1, b = 2] = [10]; return a + b', evalExpect: 12, safeExpect: 12, category: 'Destructuring', }, // Array destructuring - rest { code: 'const [a, ...rest] = [1, 2, 3]; return rest', evalExpect: [2, 3], safeExpect: [2, 3], category: 'Destructuring', }, { code: 'const [a, b, ...rest] = [1, 2, 3, 4, 5]; return rest.length', evalExpect: 3, safeExpect: 3, category: 'Destructuring', }, { code: 'const [...all] = [1, 2, 3]; return all', evalExpect: [1, 2, 3], safeExpect: [1, 2, 3], category: 'Destructuring', }, { code: 'const res = []; const a = [1,2,3]; for (let i = 0; i < a.length; i++) { const [...[a]] = a; res.push(a); } return res', evalExpect: [1, 1, 1], safeExpect: [1, 1, 1], category: 'Destructuring', }, // Object destructuring - basic { code: 'const {a} = {a: 1}; return a', evalExpect: 1, safeExpect: 1, category: 'Destructuring', }, { code: 'const {a, b} = {a: 1, b: 2}; return a + b', evalExpect: 3, safeExpect: 3, category: 'Destructuring', }, // Object destructuring - renaming (custom variable names) { code: 'const {a: myA} = {a: 1}; return myA', evalExpect: 1, safeExpect: 1, category: 'Destructuring', }, { code: 'const {a: x, b: y} = {a: 10, b: 20}; return x + y', evalExpect: 30, safeExpect: 30, category: 'Destructuring', }, { code: 'const {firstName: first, lastName: last} = {firstName: "John", lastName: "Doe"}; return first + " " + last', evalExpect: 'John Doe', safeExpect: 'John Doe', category: 'Destructuring', }, // Object destructuring - defaults { code: 'const {a = 5} = {}; return a', evalExpect: 5, safeExpect: 5, category: 'Destructuring', }, { code: 'const {a = 5} = {a: 10}; return a', evalExpect: 10, safeExpect: 10, category: 'Destructuring', }, { code: 'const {a = 1, b = 2} = {a: 10}; return a + b', evalExpect: 12, safeExpect: 12, category: 'Destructuring', }, // Object destructuring - rename with default { code: 'const {a: x = 99} = {}; return x', evalExpect: 99, safeExpect: 99, category: 'Destructuring', }, { code: 'const {a: x = 99} = {a: 1}; return x', evalExpect: 1, safeExpect: 1, category: 'Destructuring', }, // Object destructuring - rest { code: 'const {a, ...rest} = {a: 1, b: 2, c: 3}; return rest.b + rest.c', evalExpect: 5, safeExpect: 5, category: 'Destructuring', }, { code: 'const {a, b, ...rest} = {a: 1, b: 2, c: 3, d: 4}; return Object.keys(rest).length', evalExpect: 2, safeExpect: 2, category: 'Destructuring', }, { code: 'const {...all} = {a: 1, b: 2}; return all.a + all.b', evalExpect: 3, safeExpect: 3, category: 'Destructuring', }, // Nested destructuring - arrays { code: 'const [a, [b, c]] = [1, [2, 3]]; return a + b + c', evalExpect: 6, safeExpect: 6, category: 'Destructuring', }, { code: 'const [[a]] = [[1]]; return a', evalExpect: 1, safeExpect: 1, category: 'Destructuring', }, // Nested destructuring - objects { code: 'const {a: {b}} = {a: {b: 42}}; return b', evalExpect: 42, safeExpect: 42, category: 'Destructuring', }, { code: 'const {a: {b, c}} = {a: {b: 1, c: 2}}; return b + c', evalExpect: 3, safeExpect: 3, category: 'Destructuring', }, // Mixed nested { code: 'const {a: [x, y]} = {a: [1, 2]}; return x + y', evalExpect: 3, safeExpect: 3, category: 'Destructuring', }, { code: 'const [a, {b, c}] = [1, {b: 2, c: 3}]; return a + b + c', evalExpect: 6, safeExpect: 6, category: 'Destructuring', }, // Nested with defaults { code: 'const {a: {b = 10} = {}} = {}; return b', evalExpect: 10, safeExpect: 10, category: 'Destructuring', }, { code: 'const [a = 1, [b = 2] = []] = []; return a + b', evalExpect: 3, safeExpect: 3, category: 'Destructuring', }, // var and let { code: 'var [p, q] = [1, 2]; return p + q', evalExpect: 3, safeExpect: 3, category: 'Destructuring', }, { code: 'let {x, y} = {x: 10, y: 20}; return x + y', evalExpect: 30, safeExpect: 30, category: 'Destructuring', }, // Computed property names in destructuring { code: 'const key = "a"; const {[key]: val} = {a: 42}; return val', evalExpect: 42, safeExpect: 42, category: 'Destructuring', }, { code: 'const {["a" + "b"]: v} = {ab: 42}; return v', evalExpect: 42, safeExpect: 42, category: 'Destructuring', }, { code: 'const k = "a"; const {[k]: v = 10} = {}; return v', evalExpect: 10, safeExpect: 10, category: 'Destructuring', }, { code: 'const a = "x", b = "y"; const {[a]: v1, [b]: v2} = {x: 1, y: 2}; return v1 + v2', evalExpect: 3, safeExpect: 3, category: 'Destructuring', }, { code: 'function f({["a"]: v}) { return v }; return f({a: 42})', evalExpect: 42, safeExpect: 42, category: 'Destructuring', }, // Destructuring in function parameters - array { code: 'function fn([a, b]) { return a + b }; return fn([1, 2])', evalExpect: 3, safeExpect: 3, category: 'Destructuring', }, { code: 'const fn = ([a, b]) => a + b; return fn([10, 20])', evalExpect: 30, safeExpect: 30, category: 'Destructuring', }, // Destructuring in function parameters - object { code: 'function fn({a, b}) { return a + b }; return fn({a: 1, b: 2})', evalExpect: 3, safeExpect: 3, category: 'Destructuring', }, { code: 'const fn = ({a, b}) => a + b; return fn({a: 5, b: 6})', evalExpect: 11, safeExpect: 11, category: 'Destructuring', }, // Destructuring in function parameters - with rename { code: 'function fn({a: x, b: y}) { return x + y }; return fn({a: 3, b: 4})', evalExpect: 7, safeExpect: 7, category: 'Destructuring', }, // Destructuring in function parameters - with defaults { code: 'function fn({a = 10, b = 20} = {}) { return a + b }; return fn({})', evalExpect: 30, safeExpect: 30, category: 'Destructuring', }, { code: 'function fn({a = 10, b = 20} = {}) { return a + b }; return fn({a: 1})', evalExpect: 21, safeExpect: 21, category: 'Destructuring', }, // Destructuring in function parameters - mixed { code: 'function fn({a}, [b, c]) { return a + b + c }; return fn({a: 1}, [2, 3])', evalExpect: 6, safeExpect: 6, category: 'Destructuring', }, // Destructuring in function parameters - rest { code: 'function fn([first, ...rest]) { return rest.length }; return fn([1, 2, 3, 4])', evalExpect: 3, safeExpect: 3, category: 'Destructuring', }, // RHS evaluated once { code: 'let count = 0; function makeArr() { count++; return [1, 2]; } const [a, b] = makeArr(); return count', evalExpect: 1, safeExpect: 1, category: 'Destructuring', }, // In loops { code: 'let sum = 0; for (const [a, b] of [[1, 2], [3, 4]]) { sum += a + b; } return sum', evalExpect: 10, safeExpect: 10, category: 'Destructuring', }, { code: 'const pairs = []; for (const [first, second] in {ab: 1, cd: 2}) { pairs.push(first + second); } return pairs', evalExpect: ['ab', 'cd'], safeExpect: ['ab', 'cd'], category: 'Destructuring', }, { code: 'let total = 0; for (const {a, b: {c}} of [{a: 1, b: {c: 2}}, {a: 3, b: {c: 4}}]) { total += a + c; } return total', evalExpect: 10, safeExpect: 10, category: 'Destructuring', }, { code: 'const [value];', evalExpect: 'error', safeExpect: '/Destructuring declaration requires an initializer/', category: 'Destructuring', }, // Shorthand properties with destructuring { code: 'const obj = {x: 1, y: 2}; const {x, y} = obj; return x + y', evalExpect: 3, safeExpect: 3, category: 'Destructuring', }, ]; ================================================ FILE: test/eval/testCases/destructuring.spec.ts ================================================ 'use strict'; import { run, getState } from './test-utils.js'; import { tests } from './destructuring.data.js'; describe('Destructuring Tests', () => { describe('Sync', () => { tests.forEach((test) => { let state = getState(); it(test.code.substring(0, 100), async () => { await run(test, state, false); }); }); }); describe('Async', () => { tests.forEach((test) => { let state = getState(); it(test.code.substring(0, 100), async () => { await run(test, state, true); }); }); }); }); ================================================ FILE: test/eval/testCases/error-handling.data.ts ================================================ 'use strict'; import { TestCase } from './types.js'; export const tests: TestCase[] = [ { code: "throw new Error('test')", evalExpect: 'error', safeExpect: '/test/', category: 'Error Handling', }, { code: 'throw undefined', evalExpect: 'error', safeExpect: 'error', category: 'Error Handling', }, { code: 'try {a.x.a} catch {return 1}; return 2', evalExpect: 1, safeExpect: 1, category: 'Error Handling', }, { code: "try { throw new Error('test'); } catch { return 'caught'; }", evalExpect: 'caught', safeExpect: 'caught', category: 'Error Handling', }, // Finally block tests { code: 'let x = 0; try { x = 1; } finally { x = 2; } return x;', evalExpect: 2, safeExpect: 2, category: 'Error Handling', }, { code: 'let x = 0; try { throw new Error(); } catch(e) { x = 1; } finally { x = 2; } return x;', evalExpect: 2, safeExpect: 2, category: 'Error Handling', }, { code: 'let x = 0; try { x = 1; } finally { x += 10; } return x;', evalExpect: 11, safeExpect: 11, category: 'Error Handling', }, { code: 'let x = 0; try { throw new Error("test"); } catch(e) { x = 5; } finally { x *= 2; } return x;', evalExpect: 10, safeExpect: 10, category: 'Error Handling', }, { code: 'try { return 1; } finally { return 2; }', evalExpect: 2, safeExpect: 2, category: 'Error Handling', }, { code: 'let x = 0; try { return 10; } finally { x = 5; } return x;', evalExpect: 10, safeExpect: 10, category: 'Error Handling', }, { code: 'try { throw new Error("first"); } finally { throw new Error("second"); }', evalExpect: 'error', safeExpect: '/second/', category: 'Error Handling', }, { code: 'try { throw new Error("test"); } catch(e) { return e.message; } finally { let y = 1; }', evalExpect: 'test', safeExpect: 'test', category: 'Error Handling', }, { code: 'let result = ""; try { result += "a"; } catch(e) { result += "b"; } finally { result += "c"; } return result;', evalExpect: 'ac', safeExpect: 'ac', category: 'Error Handling', }, { code: 'let result = ""; try { throw new Error(); } catch(e) { result += "b"; } finally { result += "c"; } return result;', evalExpect: 'bc', safeExpect: 'bc', category: 'Error Handling', }, { code: 'function testFn1() { try { return "try"; } finally { } } return testFn1();', evalExpect: 'try', safeExpect: 'try', category: 'Error Handling', }, { code: 'function testFn2() { try { throw new Error("err"); } catch(e) { return "catch"; } finally { } } return testFn2();', evalExpect: 'catch', safeExpect: 'catch', category: 'Error Handling', }, { code: 'let x = 0; try { } finally { x = 1; } return x;', evalExpect: 1, safeExpect: 1, category: 'Error Handling', }, { code: 'missing = 1;', evalExpect: 'error', safeExpect: '/missing is not defined/', category: 'Error Handling', }, { code: 'Math = 1;', evalExpect: 'error', safeExpect: "/Cannot assign property 'Math' of a global object/", category: 'Error Handling', }, { code: 'const answer = 1; answer = 2;', evalExpect: 'error', safeExpect: '/Assignment to constant variable\\./', category: 'Error Handling', }, { code: '1 2', evalExpect: 'error', safeExpect: '/Unexpected token after .*: 2/', category: 'Error Handling', }, { code: 'const x = 0; ({ x } = { x: 1 });', evalExpect: 'error', safeExpect: '/Assignment to constant variable/', category: 'Error Handling', }, { code: '({ missing } = { missing: 1 });', evalExpect: 'error', safeExpect: '/missing is not defined/', category: 'Error Handling', }, { code: '{a: 1} = 1', evalExpect: 'error', safeExpect: '/Unexpected token/', category: 'Error Handling', }, { code: 'missing: for (;;) { break nope; }', evalExpect: 'error', safeExpect: "/Undefined label 'nope'/", category: 'Error Handling', }, { code: 'outer: { continue outer; }', evalExpect: 'error', safeExpect: '/Illegal continue statement/', category: 'Error Handling', }, { code: 'outer: inner: { continue inner; }', evalExpect: 'error', safeExpect: '/Illegal continue statement/', category: 'Error Handling', }, { code: 'outer: switch (1) { case 1: continue outer; }', evalExpect: 'error', safeExpect: '/Illegal continue statement/', category: 'Error Handling', }, { code: 'outer: if (true) { continue outer; }', evalExpect: 'error', safeExpect: '/Illegal continue statement/', category: 'Error Handling', }, { code: 'outer: try { continue outer; } finally {}', evalExpect: 'error', safeExpect: '/Illegal continue statement/', category: 'Error Handling', }, { code: 'outer: { } break outer;', evalExpect: 'error', safeExpect: "/Undefined label 'outer'/", category: 'Error Handling', }, { code: 'outer: for (;;) { try { break outer; } finally { throw new Error("stop"); } }', evalExpect: 'error', safeExpect: '/stop/', category: 'Error Handling', }, { code: 'nonExistentVar', evalExpect: 'error', safeExpect: '/nonExistentVar is not defined/', category: 'Error Handling', }, ]; ================================================ FILE: test/eval/testCases/error-handling.spec.ts ================================================ 'use strict'; import { run, getState } from './test-utils.js'; import { tests } from './error-handling.data.js'; describe('Error Handling Tests', () => { describe('Sync', () => { tests.forEach((test) => { let state = getState(); it(test.code.substring(0, 100), async () => { await run(test, state, false); }); }); }); describe('Async', () => { tests.forEach((test) => { let state = getState(); it(test.code.substring(0, 100), async () => { await run(test, state, true); }); }); }); }); ================================================ FILE: test/eval/testCases/function-replacements.data.ts ================================================ 'use strict'; import { TestCase } from './types.js'; export const tests: TestCase[] = [ { code: "const list = []; const push = list.push; push(1); push(2); return list.join(',')", evalExpect: '1,2', safeExpect: '1,2', category: 'Function Replacements', }, { code: 'const list = []; const push = list.push; return push.name', evalExpect: 'push', safeExpect: 'push', category: 'Function Replacements', }, { code: "const list = []; const other = []; const push = list.push; const rebound = push.bind(other); rebound(3); return [list.join(','), other.join(','), rebound.name].join('|')", evalExpect: '|3|bound push', safeExpect: '|3|bound push', category: 'Function Replacements', }, { code: "const list = []; const other = []; const push = list.push; const rebound = push.bind(other, 3, 4); rebound(); return [list.join(','), other.join(','), rebound.name, rebound.length].join('|')", evalExpect: '|3,4|bound push|0', safeExpect: '|3,4|bound push|0', category: 'Function Replacements', }, { code: "const list = []; const other = []; const third = []; const push = list.push; const rebound = push.bind(other, 3); const reboundAgain = rebound.bind(third, 4); reboundAgain(5); return [list.join(','), other.join(','), third.join(','), reboundAgain.name].join('|')", evalExpect: '|3,4,5||bound bound push', safeExpect: '|3,4,5||bound bound push', category: 'Function Replacements', }, ]; ================================================ FILE: test/eval/testCases/function-replacements.spec.ts ================================================ 'use strict'; import { run, getState } from './test-utils.js'; import { tests } from './function-replacements.data.js'; describe('Function Replacements Tests', () => { describe('Sync', () => { tests.forEach((test) => { const state = getState(); it(test.code.substring(0, 100), async () => { await run(test, state, false); }); }); }); describe('Async', () => { tests.forEach((test) => { const state = getState(); it(test.code.substring(0, 100), async () => { await run(test, state, true); }); }); }); }); ================================================ FILE: test/eval/testCases/functions.data.ts ================================================ 'use strict'; import { TestCase } from './types.js'; export const tests: TestCase[] = [ { code: '((a) => {return a + 1})(1)', evalExpect: 2, safeExpect: 2, category: 'Functions', }, { code: "(() => '1' + (() => '22')())()", evalExpect: '122', safeExpect: '122', category: 'Functions', }, { code: '(a => a + 1)(1)', evalExpect: 2, safeExpect: 2, category: 'Functions', }, { code: 'function f(a) { return a + 1 } return f(2);', evalExpect: 3, safeExpect: 3, category: 'Functions', }, { code: '(function () { return 1 })()', evalExpect: 1, safeExpect: 1, category: 'Functions', }, { code: 'let list = [0, 1]; return list.sort((a, b) => (a < b) ? 1 : -1)', evalExpect: [1, 0], safeExpect: [1, 0], category: 'Functions', }, { code: 'let y = {a: 1, b(x) {return this.a + x}}; return y.b(2)', evalExpect: 3, safeExpect: 3, category: 'Functions', }, { code: "let y = {a: '2', b() {return this.a = '1'}}; y.b(); return y.a", evalExpect: '1', safeExpect: '1', category: 'Functions', }, { code: '[0,1].filter((...args) => args[1])', evalExpect: [1], safeExpect: [1], category: 'Functions', }, { code: 'Math.pow(...[2, 2])', evalExpect: 4, safeExpect: 4, category: 'Functions', }, { code: 'return f(); function f() { return 1; }', evalExpect: 1, safeExpect: 1, category: 'Functions', }, { code: 'return add(2, 3); function add(a, b) { return a + b; }', evalExpect: 5, safeExpect: 5, category: 'Functions', }, { code: 'let x = f(); function f() { return 42; } return x;', evalExpect: 42, safeExpect: 42, category: 'Functions', }, { code: 'const fn = ({ a: for }) => for; return fn({ a: 1 });', evalExpect: 'error', safeExpect: "/Unexpected token 'for'/", category: 'Functions', }, { code: 'function fn(for) { return for; } return fn(1);', evalExpect: 'error', safeExpect: "/Unexpected token 'for'/", category: 'Functions', }, // Trailing commas in function parameters { code: 'function fn(a, b,) { return a + b } return fn(1, 2);', evalExpect: 3, safeExpect: 3, category: 'Functions', }, // Trailing commas in function call { code: 'function fn(a, b) { return a + b } return fn(1, 2,);', evalExpect: 3, safeExpect: 3, category: 'Functions', }, // Trailing commas in arrow function parameters { code: '((a, b,) => a + b)(1, 2)', evalExpect: 3, safeExpect: 3, category: 'Functions', }, ]; ================================================ FILE: test/eval/testCases/functions.spec.ts ================================================ 'use strict'; import { run, getState } from './test-utils.js'; import { tests } from './functions.data.js'; describe('Functions Tests', () => { describe('Sync', () => { tests.forEach((test) => { let state = getState(); it(test.code.substring(0, 100), async () => { await run(test, state, false); }); }); }); describe('Async', () => { tests.forEach((test) => { let state = getState(); it(test.code.substring(0, 100), async () => { await run(test, state, true); }); }); }); }); ================================================ FILE: test/eval/testCases/generators.data.ts ================================================ 'use strict'; import { TestCase } from './types.js'; export const tests: TestCase[] = [ // Basic generator { code: 'function* gen() { yield 1; yield 2; yield 3; } const g = gen(); return [g.next().value, g.next().value, g.next().value]', evalExpect: [1, 2, 3], safeExpect: [1, 2, 3], category: 'Generators', }, // Generator done state { code: 'function* gen() { yield 1; } const g = gen(); g.next(); return g.next().done', evalExpect: true, safeExpect: true, category: 'Generators', }, // Generator return value { code: 'function* gen() { yield 1; return 42; } const g = gen(); g.next(); return g.next().value', evalExpect: 42, safeExpect: 42, category: 'Generators', }, // for-of with generator { code: 'function* gen() { yield 1; yield 2; yield 3; } const arr = []; for (const x of gen()) { arr.push(x); } return arr', evalExpect: [1, 2, 3], safeExpect: [1, 2, 3], category: 'Generators', }, // Spread with generator { code: 'function* gen() { yield 1; yield 2; yield 3; } return [...gen()]', evalExpect: [1, 2, 3], safeExpect: [1, 2, 3], category: 'Generators', }, // yield* delegation { code: 'function* inner() { yield 2; yield 3; } function* outer() { yield 1; yield* inner(); yield 4; } return [...outer()]', evalExpect: [1, 2, 3, 4], safeExpect: [1, 2, 3, 4], category: 'Generators', }, // yield* with array { code: 'function* gen() { yield 1; yield* [2, 3]; yield 4; } return [...gen()]', evalExpect: [1, 2, 3, 4], safeExpect: [1, 2, 3, 4], category: 'Generators', }, // yield* invalid target throws { code: 'function* gen() { yield* 1; } return [...gen()]', evalExpect: 'error', safeExpect: '/not iterable/', category: 'Generators', }, // Inline generator function expression { code: 'const gen = function* () { yield 1; yield 2; }; return [...gen()]', evalExpect: [1, 2], safeExpect: [1, 2], category: 'Generators', }, // Anonymous generator IIFE { code: 'return (function* () { yield 1; yield 2; })().next().value', evalExpect: 1, safeExpect: 1, category: 'Generators', }, // Generator with arguments { code: 'function* range(start, end) { for (let i = start; i < end; i++) yield i; } return [...range(0, 4)]', evalExpect: [0, 1, 2, 3], safeExpect: [0, 1, 2, 3], category: 'Generators', }, // Generator with return in middle { code: 'function* gen() { yield 1; return; yield 2; } return [...gen()]', evalExpect: [1], safeExpect: [1], category: 'Generators', }, // yield outside generator throws { code: 'function f() { yield 1; } f()', evalExpect: 'error', safeExpect: 'error', category: 'Generators', }, // yield inside nested regular function inside generator throws { code: 'function* gen() { function inner() { yield 1; } inner(); } [...gen()]', evalExpect: 'error', safeExpect: 'error', category: 'Generators', }, // yield inside arrow function inside generator throws { code: 'function* gen() { const f = () => { yield 1; }; f(); } [...gen()]', evalExpect: 'error', safeExpect: 'error', category: 'Generators', }, // yield inside method of object literal inside generator throws { code: 'function* gen() { const obj = { m() { yield 1; } }; obj.m(); } [...gen()]', evalExpect: 'error', safeExpect: 'error', category: 'Generators', }, // Generator with conditional yields { code: 'function* gen(n) { if (n > 0) { yield 1; yield 2; } else { yield -1; } } return [...gen(1)]', evalExpect: [1, 2], safeExpect: [1, 2], category: 'Generators', }, { code: 'function* gen(n) { if (n > 0) { yield 1; yield 2; } else { yield -1; } } return [...gen(-1)]', evalExpect: [-1], safeExpect: [-1], category: 'Generators', }, // Generator with if and no else branch { code: 'function* gen(flag) { if (flag) { yield 1; } } return [...gen(false)]', evalExpect: [], safeExpect: [], category: 'Generators', }, // Generator with if and no else branch { code: 'function* gen() { if (false) { yield 1; } yield 2; } return [...gen()]', evalExpect: [2], safeExpect: [2], category: 'Generators', }, // Generator with try/finally { code: 'const log = []; function* gen() { try { yield 1; yield 2; } finally { log.push("done"); } }; [...gen()]; return log', evalExpect: ['done'], safeExpect: ['done'], category: 'Generators', }, // Generator with try/catch (no throw, catch block not entered) { code: 'const log = []; function* gen() { try { yield 1; } catch(e) { log.push("caught"); } yield 2; }; [...gen()]; return log', evalExpect: [], safeExpect: [], category: 'Generators', }, // Generator with destructuring in for-of { code: 'function* pairs() { yield [1, "a"]; yield [2, "b"]; yield [3, "c"]; } const arr = []; for (const [n, s] of pairs()) { arr.push(s + n); } return arr', evalExpect: ['a1', 'b2', 'c3'], safeExpect: ['a1', 'b2', 'c3'], category: 'Generators', }, // Multiple yield* delegations in sequence { code: 'function* genA() { yield 1; yield 2; } function* genB() { yield 3; yield 4; } function* genC() { yield* genA(); yield* genB(); yield 5; } return [...genC()]', evalExpect: [1, 2, 3, 4, 5], safeExpect: [1, 2, 3, 4, 5], category: 'Generators', }, // Nested yield* delegation { code: 'function* genX() { yield 1; } function* genY() { yield* genX(); yield 2; } function* genZ() { yield* genY(); yield 3; } return [...genZ()]', evalExpect: [1, 2, 3], safeExpect: [1, 2, 3], category: 'Generators', }, // Generator with while loop { code: 'function* countdown(n) { while (n > 0) { yield n; n = n - 1; } } return [...countdown(3)]', evalExpect: [3, 2, 1], safeExpect: [3, 2, 1], category: 'Generators', }, // Generator return() ends iteration { code: 'function* gen() { yield 1; yield 2; yield 3; } const g = gen(); g.next(); const r = g.return(42); return [r.value, r.done, g.next().done]', evalExpect: [42, true, true], safeExpect: [42, true, true], category: 'Generators', }, // Generator throw() propagates error { code: 'function* gen() { yield 1; yield 2; } const g = gen(); try { g.throw(new Error("boom")); } catch(e) { return e.message }', evalExpect: 'boom', safeExpect: 'boom', category: 'Generators', }, // Generator throw() after partial consumption { code: 'function* gen() { yield 1; yield 2; } const g = gen(); g.next(); try { g.throw(new Error("x")); } catch(e) { return e.message }', evalExpect: 'x', safeExpect: 'x', category: 'Generators', }, // next(value) injection — yield as expression { code: 'function* gen() { const x = yield 1; const y = yield 2; return x + y; } const g = gen(); g.next(); g.next(10); return g.next(20).value', evalExpect: 30, safeExpect: 30, category: 'Generators', }, // yield expression returns undefined when no value injected { code: 'function* gen() { const x = yield 1; return x; } const g = gen(); g.next(); return g.next().value', evalExpect: undefined, safeExpect: undefined, category: 'Generators', }, // Generator return() runs finally block { code: 'const log = []; function* gen() { try { yield 1; } finally { log.push("F"); } } const g = gen(); g.next(); g.return(99); return log', evalExpect: ['F'], safeExpect: ['F'], category: 'Generators', }, // Generator throw() triggers catch block { code: 'function* gen() { try { yield 1; } catch(e) { yield e.message; } } const g = gen(); g.next(); return g.throw(new Error("boom")).value', evalExpect: 'boom', safeExpect: 'boom', category: 'Generators', }, // yield inside for loop — correct iteration count { code: 'function* range(n) { for (let i = 0; i < n; i++) yield i; } return [...range(4)]', evalExpect: [0, 1, 2, 3], safeExpect: [0, 1, 2, 3], category: 'Generators', }, // yield* delegation with next(value) forwarding { code: 'function* inner() { const x = yield 1; return x * 2; } function* outer() { const r = yield* inner(); yield r; } const g = outer(); g.next(); const r = g.next(5); return [r.value, r.done, g.next().done]', evalExpect: [10, false, true], safeExpect: [10, false, true], category: 'Generators', }, // yield* wraps iterators that do not expose Symbol.iterator themselves { code: 'const iterable = {}; iterable[Symbol.iterator] = function() { return { step: 0, next(value) { if (this.step++ === 0) return { value: 1, done: false }; return { value: value * 2, done: true }; } }; }; function* gen() { return yield* iterable; } const g = gen(); const a = g.next(); const b = g.next(7); return [[a.value, a.done], [b.value, b.done]]', evalExpect: [ [1, false], [14, true], ], safeExpect: [ [1, false], [14, true], ], category: 'Generators', }, // yield* forwards return() into wrapped iterators { code: 'const log = []; const iterable = {}; iterable[Symbol.iterator] = function() { return { next() { return { value: 1, done: false }; }, return(value) { log.push(value); return { value: value + 1, done: true }; } }; }; function* gen() { yield* iterable; } const g = gen(); g.next(); const r = g.return(9); return [r.value, r.done, log]', evalExpect: [10, true, [9]], safeExpect: [10, true, [9]], category: 'Generators', }, // yield* forwards throw() into wrapped iterators { code: 'const log = []; const iterable = {}; iterable[Symbol.iterator] = function() { return { next() { return { value: 1, done: false }; }, throw(err) { log.push(err.message); return { value: "caught", done: true }; } }; }; function* gen() { return yield* iterable; } const g = gen(); g.next(); const r = g.throw(new Error("boom")); return [r.value, r.done, log]', evalExpect: ['caught', true, ['boom']], safeExpect: ['caught', true, ['boom']], category: 'Generators', }, // Generator loop with continue and break { code: 'function* gen() { for (let i = 0; i < 5; i++) { if (i === 1) continue; if (i === 3) break; yield i; } } return [...gen()]', evalExpect: [0, 2], safeExpect: [0, 2], category: 'Generators', }, // Generator labeled break exits outer loop { code: 'function* gen() { outer: for (let i = 0; i < 3; i++) { for (let j = 0; j < 3; j++) { if (i === 1 && j === 1) break outer; yield `${i}${j}`; } } } return [...gen()]', evalExpect: ['00', '01', '02', '10'], safeExpect: ['00', '01', '02', '10'], category: 'Generators', }, // Generator labeled statement handles break target { code: 'function* gen() { outer: { yield 1; break outer; yield 2; } yield 3; } return [...gen()]', evalExpect: [1, 3], safeExpect: [1, 3], category: 'Generators', }, // Generator catches internal throws { code: 'function* gen() { try { throw new Error("boom"); } catch (e) { yield e.message; } } return [...gen()]', evalExpect: ['boom'], safeExpect: ['boom'], category: 'Generators', }, // Generator rethrows internal throws when no catch is present { code: 'function* gen() { try { throw new Error("boom"); } finally { } } const g = gen(); try { g.next(); } catch (e) { return e.message; }', evalExpect: 'boom', safeExpect: 'boom', category: 'Generators', }, // Generator labeled statement falls through when not broken { code: 'function* gen() { outer: { yield 1; } return 2; } const g = gen(); return [g.next().value, g.next().value]', evalExpect: [1, 2], safeExpect: [1, 2], category: 'Generators', }, // for-await-of inside sync generator throws { code: 'function* gen() { for await (const x of [1, 2]) { yield x; } } return [...gen()]', evalExpect: 'error', safeExpect: 'error', category: 'Generators', }, ]; // These tests require async compilation (use of await / for-await-of at top level). export const asyncTests: TestCase[] = [ // Async generator { code: 'async function* gen() { yield 1; yield 2; } const g = gen(); return [(await g.next()).value, (await g.next()).value]', evalExpect: [1, 2], safeExpect: [1, 2], category: 'Generators', }, // Async generator with for-await-of { code: 'async function* gen() { yield 1; yield 2; yield 3; } const arr = []; for await (const x of gen()) { arr.push(x); } return arr', evalExpect: [1, 2, 3], safeExpect: [1, 2, 3], category: 'Generators', }, // Async inline generator function expression { code: 'const gen = async function* () { yield 1; yield 2; }; const g = gen(); return [(await g.next()).value, (await g.next()).value]', evalExpect: [1, 2], safeExpect: [1, 2], category: 'Generators', }, // Anonymous async generator IIFE { code: 'return (await (async function* () { yield 1; yield 2; })().next()).value', evalExpect: 1, safeExpect: 1, category: 'Generators', }, // Async generator next(value) injection { code: 'async function* gen() { const x = yield 1; const y = yield 2; return x + y; } const g = gen(); await g.next(); await g.next(10); return (await g.next(20)).value', evalExpect: 30, safeExpect: 30, category: 'Generators', }, // Async generator yield* delegation with next(value) forwarding { code: 'async function* inner() { const x = yield 1; return x * 2; } async function* outer() { const r = yield* inner(); yield r; } const g = outer(); await g.next(); const r = await g.next(5); return [r.value, r.done, (await g.next()).done]', evalExpect: [10, false, true], safeExpect: [10, false, true], category: 'Generators', }, // Async yield* invalid target throws { code: 'async function* gen() { yield* 1; } const g = gen(); await g.next()', evalExpect: 'error', safeExpect: '/not iterable/', category: 'Generators', }, // Async yield* can delegate to sync iterables { code: 'const iterable = {}; iterable[Symbol.iterator] = function() { return { step: 0, next(value) { if (this.step++ === 0) return { value: 1, done: false }; return { value: value * 2, done: true }; } }; }; async function* gen() { return yield* iterable; } const g = gen(); const a = await g.next(); const b = await g.next(7); return [[a.value, a.done], [b.value, b.done]]', evalExpect: [ [1, false], [14, true], ], safeExpect: [ [1, false], [14, true], ], category: 'Generators', }, // Async yield* forwards return() into sync iterators { code: 'const log = []; const iterable = {}; iterable[Symbol.iterator] = function() { return { next() { return { value: 1, done: false }; }, return(value) { log.push(value); return { value: value + 1, done: true }; } }; }; async function* gen() { yield* iterable; } const g = gen(); await g.next(); const r = await g.return(9); return [r.value, r.done, log]', evalExpect: [10, true, [9]], safeExpect: [10, true, [9]], category: 'Generators', }, // Async yield* forwards throw() into sync iterators { code: 'const log = []; const iterable = {}; iterable[Symbol.iterator] = function() { return { next() { return { value: 1, done: false }; }, throw(err) { log.push(err.message); return { value: "caught", done: true }; } }; }; async function* gen() { return yield* iterable; } const g = gen(); await g.next(); const r = await g.throw(new Error("boom")); return [r.value, r.done, log]', evalExpect: ['caught', true, ['boom']], safeExpect: ['caught', true, ['boom']], category: 'Generators', }, // Async yield* return() works even when delegate lacks return() { code: 'const iterable = {}; iterable[Symbol.iterator] = function() { return { next() { return { value: 1, done: false }; } }; }; async function* gen() { yield* iterable; } const g = gen(); await g.next(); const r = await g.return(9); return [r.value, r.done]', evalExpect: [9, true], safeExpect: [9, true], category: 'Generators', }, // Async yield* throw() rethrows when delegate lacks throw() { code: 'const iterable = {}; iterable[Symbol.iterator] = function() { return { next() { return { value: 1, done: false }; } }; }; async function* gen() { try { return yield* iterable; } catch (e) { return e.message; } } const g = gen(); await g.next(); const r = await g.throw(new Error("boom")); return [r.value, r.done]', evalExpect: ['boom', true], safeExpect: ['boom', true], category: 'Generators', }, // Async generator surfaces body errors after resuming { code: 'async function* gen() { yield 1; throw new Error("boom"); } const g = gen(); await g.next(); try { await g.next(); } catch (e) { return e.message; }', evalExpect: 'boom', safeExpect: 'boom', category: 'Generators', }, ]; ================================================ FILE: test/eval/testCases/generators.spec.ts ================================================ 'use strict'; import { run, getState } from './test-utils.js'; import { tests, asyncTests } from './generators.data.js'; describe('Generator Tests', () => { describe('Sync', () => { tests.forEach((test) => { let state = getState(); it(test.code.substring(0, 100), async () => { await run(test, state, false); }); }); }); describe('Async', () => { [...tests, ...asyncTests].forEach((test) => { let state = getState(); it(test.code.substring(0, 100), async () => { await run(test, state, true); }); }); }); }); ================================================ FILE: test/eval/testCases/index.ts ================================================ 'use strict'; // Import and re-export all test arrays export { tests as arithmeticOperators } from './arithmetic-operators.data.js'; export { tests as assignmentOperators } from './assignment-operators.data.js'; export { tests as bitwiseOperators } from './bitwise-operators.data.js'; export { tests as comments } from './comments.data.js'; export { tests as comparisonOperators } from './comparison-operators.data.js'; export { tests as complexExpressions } from './complex-expressions.data.js'; export { tests as conditionals } from './conditionals.data.js'; export { tests as dataTypes } from './data-types.data.js'; export { tests as syntaxErrors } from './syntax-errors.data.js'; export { tests as errorHandling } from './error-handling.data.js'; export { tests as functionReplacements } from './function-replacements.data.js'; export { tests as functions } from './functions.data.js'; export { tests as logicalOperators } from './logical-operators.data.js'; export { tests as loops } from './loops.data.js'; export { tests as objectsAndArrays } from './objects-and-arrays.data.js'; export { tests as operatorPrecedence } from './operator-precedence.data.js'; export { tests as otherOperators } from './other-operators.data.js'; export { tests as security } from './security.data.js'; export { tests as switch_ } from './switch.data.js'; export { tests as templateLiterals } from './template-literals.data.js'; ================================================ FILE: test/eval/testCases/logical-operators.data.ts ================================================ 'use strict'; import { TestCase } from './types.js'; export const tests: TestCase[] = [ { code: '!test2', evalExpect: false, safeExpect: false, category: 'Logical Operators', }, { code: '!!test2', evalExpect: true, safeExpect: true, category: 'Logical Operators', }, { code: '!({}).a?.a', evalExpect: true, safeExpect: true, category: 'Logical Operators', }, { code: '!({a: {a: 1}}).a?.a', evalExpect: false, safeExpect: false, category: 'Logical Operators', }, { code: '!({a: {a: 0}}).a?.a', evalExpect: true, safeExpect: true, category: 'Logical Operators', }, { code: '!({}).a ? true : false', evalExpect: true, safeExpect: true, category: 'Logical Operators', }, { code: 'test2 && true', evalExpect: true, safeExpect: true, category: 'Logical Operators', }, { code: 'test2 || false', evalExpect: 1, safeExpect: 1, category: 'Logical Operators', }, { code: "null ?? 'default'", evalExpect: 'default', safeExpect: 'default', category: 'Logical Operators', }, { code: "undefined ?? 'default'", evalExpect: 'default', safeExpect: 'default', category: 'Logical Operators', }, { code: "0 ?? 'default'", evalExpect: 0, safeExpect: 0, category: 'Logical Operators', }, { code: "'' ?? 'default'", evalExpect: '', safeExpect: '', category: 'Logical Operators', }, { code: "false ?? 'default'", evalExpect: false, safeExpect: false, category: 'Logical Operators', }, { code: "NaN ?? 'default'", evalExpect: 'NaN', safeExpect: 'NaN', category: 'Logical Operators', }, { code: "null ?? null ?? 'default'", evalExpect: 'default', safeExpect: 'default', category: 'Logical Operators', }, { code: '1 ?? 2 ?? 3', evalExpect: 1, safeExpect: 1, category: 'Logical Operators', }, { code: 'null ?? 2 ?? 3', evalExpect: 2, safeExpect: 2, category: 'Logical Operators', }, { code: "({a: 1}).a ?? 'default'", evalExpect: 1, safeExpect: 1, category: 'Logical Operators', }, { code: "({}).a ?? 'default'", evalExpect: 'default', safeExpect: 'default', category: 'Logical Operators', }, { code: "({}).a?.b ?? 'default'", evalExpect: 'default', safeExpect: 'default', category: 'Logical Operators', }, { code: "null?.a ?? 'default'", evalExpect: 'default', safeExpect: 'default', category: 'Logical Operators', }, { code: "({a: {b: 1}}).a?.b ?? 'default'", evalExpect: 1, safeExpect: 1, category: 'Logical Operators', }, { code: "({a: null}).a?.b ?? 'default'", evalExpect: 'default', safeExpect: 'default', category: 'Logical Operators', }, { code: 'null ?? 1 || 2', evalExpect: 1, safeExpect: 1, category: 'Logical Operators', }, { code: '0 || null ?? 1', evalExpect: 1, safeExpect: 1, category: 'Logical Operators', }, { code: "(null ?? false) || 'fallback'", evalExpect: 'fallback', safeExpect: 'fallback', category: 'Logical Operators', }, { code: "null ?? 0 || 'fallback'", evalExpect: 'error', safeExpect: 'fallback', category: 'Logical Operators', }, { code: "true && null ?? 'default'", evalExpect: 'error', safeExpect: 'default', category: 'Logical Operators', }, { code: 'let x = 0; (1 ?? (x = 1)); return x', evalExpect: 0, safeExpect: 0, category: 'Logical Operators', }, { code: 'let x = 0; (null ?? (x = 1)); return x', evalExpect: 1, safeExpect: 1, category: 'Logical Operators', }, { code: 'let x = 0; (undefined ?? (x = 1)); return x', evalExpect: 1, safeExpect: 1, category: 'Logical Operators', }, { code: 'let x = 0; (0 ?? (x = 1)); return x', evalExpect: 0, safeExpect: 0, category: 'Logical Operators', }, { code: "let x = 0; ('' ?? (x = 1)); return x", evalExpect: 0, safeExpect: 0, category: 'Logical Operators', }, { code: 'let x = 0; (false ?? (x = 1)); return x', evalExpect: 0, safeExpect: 0, category: 'Logical Operators', }, { code: "({a: null ?? 'default'}).a", evalExpect: 'default', safeExpect: 'default', category: 'Logical Operators', }, { code: '[null ?? 1, 2 ?? 3][0]', evalExpect: 1, safeExpect: 1, category: 'Logical Operators', }, { code: '[null ?? 1, undefined ?? 3][1]', evalExpect: 3, safeExpect: 3, category: 'Logical Operators', }, { code: "let outputs = {}; return (outputs['classify']?.intent ?? null)", evalExpect: null, safeExpect: null, category: 'Logical Operators', }, { code: "let outputs = {classify: {intent: 'greeting'}}; return (outputs['classify']?.intent ?? null)", evalExpect: 'greeting', safeExpect: 'greeting', category: 'Logical Operators', }, { code: 'null ?? 5 ? "yes" : "no"', evalExpect: 'yes', safeExpect: 'yes', category: 'Logical Operators', }, { code: 'null ?? 0 ? "yes" : "no"', evalExpect: 'no', safeExpect: 'no', category: 'Logical Operators', }, { code: '!~0', evalExpect: false, safeExpect: false, category: 'Logical Operators', }, { code: 'true && true && true', evalExpect: true, safeExpect: true, category: 'Logical Operators', }, { code: 'false || false || true', evalExpect: true, safeExpect: true, category: 'Logical Operators', }, { code: "null ?? null ?? null ?? 'default'", evalExpect: 'default', safeExpect: 'default', category: 'Logical Operators', }, { code: '1 || 2 && 3', evalExpect: 1, safeExpect: 1, category: 'Logical Operators', }, { code: '0 && 1 || 2', evalExpect: 2, safeExpect: 2, category: 'Logical Operators', }, { code: 'true || false && false', evalExpect: true, safeExpect: true, category: 'Logical Operators', }, { code: '!true', evalExpect: false, safeExpect: false, category: 'Logical Operators', }, { code: '!false', evalExpect: true, safeExpect: true, category: 'Logical Operators', }, ]; ================================================ FILE: test/eval/testCases/logical-operators.spec.ts ================================================ 'use strict'; import { run, getState } from './test-utils.js'; import { tests } from './logical-operators.data.js'; describe('Logical Operators Tests', () => { describe('Sync', () => { tests.forEach((test) => { let state = getState(); it(test.code.substring(0, 100), async () => { await run(test, state, false); }); }); }); describe('Async', () => { tests.forEach((test) => { let state = getState(); it(test.code.substring(0, 100), async () => { await run(test, state, true); }); }); }); }); ================================================ FILE: test/eval/testCases/loops.data.ts ================================================ 'use strict'; import { TestCase } from './types.js'; export const tests: TestCase[] = [ { code: 'let x; for(let i = 0; i < 2; i++){ x = i }; return x;', evalExpect: 1, safeExpect: 1, category: 'Loops', }, { code: 'let x; for(let i = 0; i < 2; i++){ x = i; break; }; return x;', evalExpect: 0, safeExpect: 0, category: 'Loops', }, { code: 'let x; for(let i = 0; i < 2; i++){ x = i; continue; x++ }; return x;', evalExpect: 1, safeExpect: 1, category: 'Loops', }, { code: 'break;', evalExpect: 'error', safeExpect: 'error', category: 'Loops', }, { code: 'continue;', evalExpect: 'error', safeExpect: 'error', category: 'Loops', }, { code: 'let sum = 0; for (let i = 0; i < 5; i++) { if (i === 2) continue; sum += i; }; return sum;', evalExpect: 8, safeExpect: 8, category: 'Loops', }, { code: 'let sum = 0; for (let i = 0; i < 10; i++) { if (i === 3) break; sum += i; }; return sum;', evalExpect: 3, safeExpect: 3, category: 'Loops', }, { code: 'let sum = 0; for (let i = 0; i < 5; i++) { if (i > 0) { if (i === 2) continue; } sum += i; }; return sum;', evalExpect: 8, safeExpect: 8, category: 'Loops', }, { code: 'let sum = 0; for (let i = 0; i < 5; i++) { if (i === 2) { continue; } sum += i; }; return sum;', evalExpect: 8, safeExpect: 8, category: 'Loops', }, { code: 'let x = 0; while (x < 5) { x++; if (x === 3) continue; if (x === 4) break; }; return x;', evalExpect: 4, safeExpect: 4, category: 'Loops', }, { code: 'let sum = 0; for (let i = 0; i < 5; i++) { sum += i === 2 ? continue : i; }; return sum;', evalExpect: 'error', safeExpect: 'error', category: 'Loops', }, { code: 'let x = 2; while(--x){ }; return x;', evalExpect: 0, safeExpect: 0, category: 'Loops', }, { code: 'let x = 1; do {x++} while(x < 1); return x;', evalExpect: 2, safeExpect: 2, category: 'Loops', }, { code: 'for(let i of [1,2]){ return i };', evalExpect: 1, safeExpect: 1, category: 'Loops', }, { code: 'let arr = [1,2]; for(let i of arr){ return i };', evalExpect: 1, safeExpect: 1, category: 'Loops', }, { code: 'for(let i in [1,2]){ return i };', evalExpect: '0', safeExpect: '0', category: 'Loops', }, { code: 'let i = 1; {let j = 1; i += j;}; return i', evalExpect: 2, safeExpect: 2, category: 'Loops', }, { code: 'let c = 0; for (let i = 0; i < 10; i++) {c++} return c', evalExpect: 10, safeExpect: 10, category: 'Loops', }, { code: "outer: for(let i = 0; i < 3; i++) { for(let j = 0; j < 3; j++) { if(i === 1 && j === 1) break outer; } } return 'done'", evalExpect: 'done', safeExpect: 'done', category: 'Loops', }, { code: 'let hits = ""; outer: for (let i = 0; i < 3; i++) { for (let j = 0; j < 3; j++) { hits += `${i}${j}|`; continue outer; } } return hits;', evalExpect: '00|10|20|', safeExpect: '00|10|20|', category: 'Loops', }, { code: 'let out = ""; outer: { out += "a"; break outer; out += "b"; } return out;', evalExpect: 'a', safeExpect: 'a', category: 'Loops', }, { code: 'let out = ""; outer: switch (1) { case 1: out = "ok"; break outer; default: out = "bad"; } return out;', evalExpect: 'ok', safeExpect: 'ok', category: 'Loops', }, { code: 'let out = ""; outer: inner: for (let i = 0; i < 3; i++) { out += i; if (i < 2) continue outer; out += "!"; } return out;', evalExpect: '012!', safeExpect: '012!', category: 'Loops', }, { code: 'let out = ""; outer: inner: for (let i = 0; i < 3; i++) { out += i; if (i < 2) continue inner; out += "!"; } return out;', evalExpect: '012!', safeExpect: '012!', category: 'Loops', }, { code: 'let out = ""; first: second: { out += "a"; break first; out += "b"; } return out;', evalExpect: 'a', safeExpect: 'a', category: 'Loops', }, { code: 'let out = ""; first: second: switch (1) { case 1: out += "a"; break second; default: out += "b"; } return out;', evalExpect: 'a', safeExpect: 'a', category: 'Loops', }, { code: 'let out = ""; outer: if (true) { out += "a"; break outer; out += "b"; } return out;', evalExpect: 'a', safeExpect: 'a', category: 'Loops', }, { code: 'let out = ""; outer: try { out += "a"; break outer; } finally { out += "f"; } return out;', evalExpect: 'af', safeExpect: 'af', category: 'Loops', }, { code: 'let out = ""; outer: while (true) { out += "a"; break outer; } return out;', evalExpect: 'a', safeExpect: 'a', category: 'Loops', }, { code: 'let out = ""; outer: do { out += "a"; continue outer; out += "b"; } while (false); return out;', evalExpect: 'a', safeExpect: 'a', category: 'Loops', }, { code: 'let out = ""; outer: for (const value of [1, 2, 3]) { out += value; if (value === 2) break outer; } return out;', evalExpect: '12', safeExpect: '12', category: 'Loops', }, { code: 'let out = ""; outer: for (const key in { a: 1, b: 2, c: 3 }) { out += key; if (key === "b") break outer; } return out;', evalExpect: 'ab', safeExpect: 'ab', category: 'Loops', }, { code: 'let hits = ""; outer: for (let i = 0; i < 3; i++) { switch (i) { case 1: hits += "s"; continue outer; default: hits += i; } hits += "|"; } return hits;', evalExpect: '0|s2|', safeExpect: '0|s2|', category: 'Loops', }, { code: 'let hits = ""; outer: for (let i = 0; i < 3; i++) { inner: for (let j = 0; j < 3; j++) { if (j === 1) break inner; if (i === 2) break outer; hits += `${i}${j}|`; } } return hits;', evalExpect: '00|10|', safeExpect: '00|10|', category: 'Loops', }, { code: 'let log = ""; outer: { try { log += "a"; break outer; } finally { log += "f"; } log += "x"; } return log;', evalExpect: 'af', safeExpect: 'af', category: 'Loops', }, { code: 'let log = ""; outer: for (let i = 0; i < 3; i++) { for (let j = 0; j < 3; j++) { try { log += `${i}${j}|`; if (j === 1) break; } finally { if (j === 1) continue outer; } log += "x"; } log += "y"; } return log;', evalExpect: '00|x01|10|x11|20|x21|', safeExpect: '00|x01|10|x11|20|x21|', category: 'Loops', }, { code: 'let log = ""; outer: for (let i = 0; i < 3; i++) { try { switch (i) { case 2: log += "b"; break outer; default: log += i; } } finally { log += "f"; } log += "x"; } return log;', evalExpect: '0fx1fxbf', safeExpect: '0fx1fxbf', category: 'Loops', }, { code: "for(let i = 0, j = 10; i < 3; i++, j--) { } return 'done'", evalExpect: 'done', safeExpect: 'done', category: 'Loops', }, { code: 'let i = 0; for (; i < 3;) { i++; } return i;', evalExpect: 3, safeExpect: 3, category: 'Loops', }, { code: 'let i = 0; for (;;) { i++; if (i === 3) break; } return i;', evalExpect: 3, safeExpect: 3, category: 'Loops', }, { code: 'let total = 0; for (; total < 3; total++) {} return total;', evalExpect: 3, safeExpect: 3, category: 'Loops', }, { code: 'let x = 0; do x++; while (x < 3); return x;', evalExpect: 3, safeExpect: 3, category: 'Loops', }, ]; ================================================ FILE: test/eval/testCases/loops.spec.ts ================================================ 'use strict'; import { run, getState } from './test-utils.js'; import { tests } from './loops.data.js'; describe('Loops Tests', () => { describe('Sync', () => { tests.forEach((test) => { let state = getState(); it(test.code.substring(0, 100), async () => { await run(test, state, false); }); }); }); describe('Async', () => { tests.forEach((test) => { let state = getState(); it(test.code.substring(0, 100), async () => { await run(test, state, true); }); }); }); }); ================================================ FILE: test/eval/testCases/objects-and-arrays.data.ts ================================================ 'use strict'; import { TestCase } from './types.js'; export const tests: TestCase[] = [ { code: 'a.b.c', evalExpect: 2, safeExpect: 2, category: 'Objects & Arrays', }, { code: '[test2, 2]', evalExpect: [1, 2], safeExpect: [1, 2], category: 'Objects & Arrays', }, { code: '{"aa": test[0](), b: test2 * 3}', evalExpect: { aa: 1, b: 3, }, safeExpect: { aa: 1, b: 3, }, category: 'Objects & Arrays', }, { code: '{"\\\\":"\\\\"}', evalExpect: { '\\': '\\', }, safeExpect: { '\\': '\\', }, category: 'Objects & Arrays', }, { code: 'Object.keys({a:1})', evalExpect: ['a'], safeExpect: ['a'], category: 'Objects & Arrays', }, { code: '[1, ...[2, [test2, 4]], 5]', evalExpect: [1, 2, [1, 4], 5], safeExpect: [1, 2, [1, 4], 5], category: 'Objects & Arrays', }, { code: 'const obj1 = {a: 1, b: 2}; const obj2 = {c: 3, ...obj1}; return obj2.a', evalExpect: 1, safeExpect: 1, category: 'Objects & Arrays', }, { code: 'const obj1 = {a: 1, b: 2}; const obj2 = {...obj1, b: 5}; return obj2.b', evalExpect: 5, safeExpect: 5, category: 'Objects & Arrays', }, { code: 'const obj1 = {x: 10}; const obj2 = {y: 20}; const obj3 = {...obj1, ...obj2}; return obj3.x + obj3.y', evalExpect: 30, safeExpect: 30, category: 'Objects & Arrays', }, { code: 'const arr1 = [1, 2, 3]; const arr2 = [...arr1, 4, 5]; return arr2.length', evalExpect: 5, safeExpect: 5, category: 'Objects & Arrays', }, { code: 'const arr1 = [1, 2]; const arr2 = [3, 4]; return [...arr1, ...arr2]', evalExpect: [1, 2, 3, 4], safeExpect: [1, 2, 3, 4], category: 'Objects & Arrays', }, { code: 'const a = [1]; const b = [2]; const c = [3]; return [...a, ...b, ...c]', evalExpect: [1, 2, 3], safeExpect: [1, 2, 3], category: 'Objects & Arrays', }, { code: 'const data = []; return {data}', evalExpect: { data: [] }, safeExpect: { data: [] }, category: 'Objects & Arrays', }, { code: 'const x = 1; const y = 2; return {x, y}', evalExpect: { x: 1, y: 2 }, safeExpect: { x: 1, y: 2 }, category: 'Objects & Arrays', }, { code: 'const name = "Alice"; const age = 30; return {name, age, city: "NYC"}', evalExpect: { name: 'Alice', age: 30, city: 'NYC' }, safeExpect: { name: 'Alice', age: 30, city: 'NYC' }, category: 'Objects & Arrays', }, { code: 'const a = 1; const obj = {a, b: a + 1}; return obj.a + obj.b', evalExpect: 3, safeExpect: 3, category: 'Objects & Arrays', }, // Computed property names - basic { code: '({["a" + "b"]: 1})', evalExpect: { ab: 1 }, safeExpect: { ab: 1 }, category: 'Objects & Arrays', }, { code: 'let k = "x"; return ({[k]: 42})', evalExpect: { x: 42 }, safeExpect: { x: 42 }, category: 'Objects & Arrays', }, { code: '({[1 + 2]: "three"})', evalExpect: { 3: 'three' }, safeExpect: { 3: 'three' }, category: 'Objects & Arrays', }, { code: 'let n = "world"; return ({[`hello_${n}`]: 1})', evalExpect: { hello_world: 1 }, safeExpect: { hello_world: 1 }, category: 'Objects & Arrays', }, { code: 'function k() { return "key" }; return ({[k()]: "val"})', evalExpect: { key: 'val' }, safeExpect: { key: 'val' }, category: 'Objects & Arrays', }, { code: 'let t = true; return ({[t ? "a" : "b"]: 1})', evalExpect: { a: 1 }, safeExpect: { a: 1 }, category: 'Objects & Arrays', }, // Computed property names - multiple and mixed { code: 'let a = "x", b = "y"; return ({[a]: 1, [b]: 2})', evalExpect: { x: 1, y: 2 }, safeExpect: { x: 1, y: 2 }, category: 'Objects & Arrays', }, { code: '({a: 1, ["b"]: 2, c: 3})', evalExpect: { a: 1, b: 2, c: 3 }, safeExpect: { a: 1, b: 2, c: 3 }, category: 'Objects & Arrays', }, { code: '({a: 1, ["a"]: 2})', evalExpect: { a: 2 }, safeExpect: { a: 2 }, category: 'Objects & Arrays', }, { code: 'let arr = ["key"]; return ({[arr[0]]: "val"})', evalExpect: { key: 'val' }, safeExpect: { key: 'val' }, category: 'Objects & Arrays', }, // Computed property names - edge cases { code: 'let c = 0; function k() { c++; return "a" } let o = {[k()]: 1}; return c', evalExpect: 1, safeExpect: 1, category: 'Objects & Arrays', }, { code: '({[""]: 1})', evalExpect: { '': 1 }, safeExpect: { '': 1 }, category: 'Objects & Arrays', }, { code: '({[0]: "zero"})', evalExpect: { 0: 'zero' }, safeExpect: { 0: 'zero' }, category: 'Objects & Arrays', }, { code: '({[true]: 1})', evalExpect: { true: 1 }, safeExpect: { true: 1 }, category: 'Objects & Arrays', }, { code: '({[null]: 1})', evalExpect: { null: 1 }, safeExpect: { null: 1 }, category: 'Objects & Arrays', }, { code: '({[undefined]: 1})', evalExpect: { undefined: 1 }, safeExpect: { undefined: 1 }, category: 'Objects & Arrays', }, // Computed property names - with spread { code: 'let rest = {b: 2}; return ({["a"]: 1, ...rest})', evalExpect: { a: 1, b: 2 }, safeExpect: { a: 1, b: 2 }, category: 'Objects & Arrays', }, { code: 'let rest = {a: 1}; return ({...rest, ["b"]: 2})', evalExpect: { a: 1, b: 2 }, safeExpect: { a: 1, b: 2 }, category: 'Objects & Arrays', }, { code: 'let rest = {a: 1}; return ({...rest, ["a"]: 2})', evalExpect: { a: 2 }, safeExpect: { a: 2 }, category: 'Objects & Arrays', }, { code: 'let o = {k: "deep"}; return ({[o.k]: 1})', evalExpect: { deep: 1 }, safeExpect: { deep: 1 }, category: 'Objects & Arrays', }, { code: '({[(0, "a")]: 1})', evalExpect: { a: 1 }, safeExpect: { a: 1 }, category: 'Objects & Arrays', }, // Computed method names { code: 'let m = "greet"; let o = {[m]() { return "hi" }}; return o.greet()', evalExpect: 'hi', safeExpect: 'hi', category: 'Objects & Arrays', }, { code: 'let o = {["say"]() { return "hello" }}; return o.say()', evalExpect: 'hello', safeExpect: 'hello', category: 'Objects & Arrays', }, { code: 'let o = {["a" + "b"]() { return 1 }}; return o.ab()', evalExpect: 1, safeExpect: 1, category: 'Objects & Arrays', }, { code: 'let o = {x: 10, ["getX"]() { return this.x }}; return o.getX()', evalExpect: 10, safeExpect: 10, category: 'Objects & Arrays', }, { code: '[1,2,3,].length', evalExpect: 3, safeExpect: 3, category: 'Objects & Arrays', }, { code: 'Object.keys({a:1,b:2,}).length', evalExpect: 2, safeExpect: 2, category: 'Objects & Arrays', }, { code: '[,].length', evalExpect: 1, safeExpect: 1, category: 'Objects & Arrays', }, { code: '0 in [,]', evalExpect: false, safeExpect: false, category: 'Objects & Arrays', }, { code: '[,,].length', evalExpect: 2, safeExpect: 2, category: 'Objects & Arrays', }, { code: '[1,,].length', evalExpect: 2, safeExpect: 2, category: 'Objects & Arrays', }, { code: '1 in [1,,]', evalExpect: false, safeExpect: false, category: 'Objects & Arrays', }, { code: '[1,,2].length', evalExpect: 3, safeExpect: 3, category: 'Objects & Arrays', }, { code: '1 in [1,,2]', evalExpect: false, safeExpect: false, category: 'Objects & Arrays', }, { code: '[1,,2][2]', evalExpect: 2, safeExpect: 2, category: 'Objects & Arrays', }, ]; ================================================ FILE: test/eval/testCases/objects-and-arrays.spec.ts ================================================ 'use strict'; import { run, getState } from './test-utils.js'; import { tests } from './objects-and-arrays.data.js'; describe('Objects And Arrays Tests', () => { describe('Sync', () => { tests.forEach((test) => { let state = getState(); it(test.code.substring(0, 100), async () => { await run(test, state, false); }); }); }); describe('Async', () => { tests.forEach((test) => { let state = getState(); it(test.code.substring(0, 100), async () => { await run(test, state, true); }); }); }); }); ================================================ FILE: test/eval/testCases/operator-precedence.data.ts ================================================ 'use strict'; import { TestCase } from './types.js'; export const tests: TestCase[] = [ { code: "test2 !== '1' && false", evalExpect: false, safeExpect: false, category: 'Operator Precedence', }, { code: 'true && true || false', evalExpect: true, safeExpect: true, category: 'Operator Precedence', }, { code: 'true || true && false', evalExpect: true, safeExpect: true, category: 'Operator Precedence', }, { code: '1 && 2 == 1', evalExpect: false, safeExpect: false, category: 'Operator Precedence', }, { code: '1 + 2 === 1 + 2 === 1 && 2', evalExpect: false, safeExpect: false, category: 'Operator Precedence', }, { code: 'true === 1 && 2', evalExpect: false, safeExpect: false, category: 'Operator Precedence', }, { code: '-1 < 1 && 2 > 1', evalExpect: true, safeExpect: true, category: 'Operator Precedence', }, { code: '!5 > 3', evalExpect: false, safeExpect: false, category: 'Operator Precedence', }, { code: '!5 < 3', evalExpect: true, safeExpect: true, category: 'Operator Precedence', }, { code: '!0 === true', evalExpect: true, safeExpect: true, category: 'Operator Precedence', }, { code: '!false && true', evalExpect: true, safeExpect: true, category: 'Operator Precedence', }, { code: '!true || true', evalExpect: true, safeExpect: true, category: 'Operator Precedence', }, { code: '!false && false || true', evalExpect: true, safeExpect: true, category: 'Operator Precedence', }, { code: '5 > 3 > 1', evalExpect: false, safeExpect: false, category: 'Operator Precedence', }, { code: '1 < 2 < 3', evalExpect: true, safeExpect: true, category: 'Operator Precedence', }, { code: '5 > 3 === true', evalExpect: true, safeExpect: true, category: 'Operator Precedence', }, { code: '1 | 2 && 3', evalExpect: 3, safeExpect: 3, category: 'Operator Precedence', }, { code: 'true && 1 | 2', evalExpect: 3, safeExpect: 3, category: 'Operator Precedence', }, { code: '4 & 5 || 0', evalExpect: 4, safeExpect: 4, category: 'Operator Precedence', }, { code: '2 + 3 << 1', evalExpect: 10, safeExpect: 10, category: 'Operator Precedence', }, { code: '8 >> 1 + 1', evalExpect: 2, safeExpect: 2, category: 'Operator Precedence', }, { code: '1 << 2 * 2', evalExpect: 16, safeExpect: 16, category: 'Operator Precedence', }, { code: '5 & 3 | 2', evalExpect: 3, safeExpect: 3, category: 'Operator Precedence', }, { code: '8 | 4 & 2', evalExpect: 8, safeExpect: 8, category: 'Operator Precedence', }, { code: '15 ^ 3 & 7', evalExpect: 12, safeExpect: 12, category: 'Operator Precedence', }, // Exponentiation precedence tests { code: '2 * 3 ** 2', evalExpect: 18, safeExpect: 18, category: 'Operator Precedence', }, { code: '10 / 2 ** 3', evalExpect: 1.25, safeExpect: 1.25, category: 'Operator Precedence', }, { code: '2 + 3 ** 2', evalExpect: 11, safeExpect: 11, category: 'Operator Precedence', }, { code: '2 * 3 * 2 ** 3', evalExpect: 48, safeExpect: 48, category: 'Operator Precedence', }, { code: '-2 ** 2', evalExpect: 'error', safeExpect: 'error', category: 'Operator Precedence', }, // Exponentiation right-associativity tests { code: '2 ** 3 ** 2', evalExpect: 512, safeExpect: 512, category: 'Operator Precedence', }, { code: '2 ** 2 ** 3', evalExpect: 256, safeExpect: 256, category: 'Operator Precedence', }, { code: '3 ** 2 ** 2', evalExpect: 81, safeExpect: 81, category: 'Operator Precedence', }, { code: '4 / 2 ** 2 * 3', evalExpect: 3, safeExpect: 3, category: 'Operator Precedence', }, { code: '100 - 10 ** 2', evalExpect: 0, safeExpect: 0, category: 'Operator Precedence', }, ]; ================================================ FILE: test/eval/testCases/operator-precedence.spec.ts ================================================ 'use strict'; import { run, getState } from './test-utils.js'; import { tests } from './operator-precedence.data.js'; describe('Operator Precedence Tests', () => { describe('Sync', () => { tests.forEach((test) => { let state = getState(); it(test.code.substring(0, 100), async () => { await run(test, state, false); }); }); }); describe('Async', () => { tests.forEach((test) => { let state = getState(); it(test.code.substring(0, 100), async () => { await run(test, state, true); }); }); }); }); ================================================ FILE: test/eval/testCases/other-operators.data.ts ================================================ 'use strict'; import { TestCase } from './types.js'; export const tests: TestCase[] = [ { code: "typeof '1'", evalExpect: 'string', safeExpect: 'string', category: 'Other Operators', }, { code: "typeof z === 'undefined'", evalExpect: true, safeExpect: true, category: 'Other Operators', }, { code: '{} instanceof Object', evalExpect: 'error', safeExpect: true, category: 'Other Operators', }, { code: '{} instanceof undefined', evalExpect: 'error', safeExpect: 'error', category: 'Other Operators', }, { code: "'a' in {a: 1}", evalExpect: true, safeExpect: true, category: 'Other Operators', }, { code: '1,2', evalExpect: 2, safeExpect: 2, category: 'Other Operators', }, { code: "void 2 == '2'", evalExpect: false, safeExpect: false, category: 'Other Operators', }, { code: "void (2 == '2')", category: 'Other Operators', }, { code: 'new Date(0).toISOString()', evalExpect: '1970-01-01T00:00:00.000Z', safeExpect: '1970-01-01T00:00:00.000Z', category: 'Other Operators', }, { code: 'function E(a) { this.scope = a.context }; return new E(isNaN).scope?.Function?.name', category: 'Other Operators', }, { code: 'typeof 5 + "2"', evalExpect: 'number2', safeExpect: 'number2', category: 'Other Operators', }, { code: 'typeof (5 + 2)', evalExpect: 'number', safeExpect: 'number', category: 'Other Operators', }, { code: 'typeof 5 === "number"', evalExpect: true, safeExpect: true, category: 'Other Operators', }, { code: 'void 0 === undefined', evalExpect: true, safeExpect: true, category: 'Other Operators', }, { code: 'void 5 + 10', evalExpect: 'NaN', safeExpect: 'NaN', category: 'Other Operators', }, { code: 'typeof void 0', evalExpect: 'undefined', safeExpect: 'undefined', category: 'Other Operators', }, { code: 'null?.[0]', category: 'Other Operators', }, { code: 'null?.fn?.()', category: 'Other Operators', }, ]; ================================================ FILE: test/eval/testCases/other-operators.spec.ts ================================================ 'use strict'; import { run, getState } from './test-utils.js'; import { tests } from './other-operators.data.js'; describe('Other Operators Tests', () => { describe('Sync', () => { tests.forEach((test) => { let state = getState(); it(test.code.substring(0, 100), async () => { await run(test, state, false); }); }); }); describe('Async', () => { tests.forEach((test) => { let state = getState(); it(test.code.substring(0, 100), async () => { await run(test, state, true); }); }); }); }); ================================================ FILE: test/eval/testCases/security.data.ts ================================================ import { TestCase } from './types'; export const tests: TestCase[] = [ { code: 'globalThis.constructor.name', evalExpect: 'Window', safeExpect: 'SandboxGlobal', category: 'Security', }, { code: 'this.constructor.name', evalExpect: 'Window', safeExpect: 'SandboxGlobal', category: 'Security', }, { code: 'eval.name', evalExpect: 'eval', safeExpect: 'sandboxEval', category: 'Security', }, { code: 'return eval("1+1; 2+2;")', evalExpect: 4, safeExpect: 4, category: 'Security', }, { code: 'Function.name', evalExpect: 'Function', safeExpect: 'SandboxFunction', category: 'Security', }, { code: 'bypassed=1', evalExpect: 'error', safeExpect: '/bypassed is not defined/', category: 'Security', }, { code: '`${`${bypassed=1}`}`', evalExpect: 'error', safeExpect: '/bypassed is not defined/', category: 'Security', }, { code: '[].filter.constructor("return \'ok\'")()', evalExpect: 'ok', safeExpect: 'ok', category: 'Security', }, { code: "[].filter.constructor('return bypassed')()", evalExpect: false, safeExpect: '/bypassed is not defined/', category: 'Security', }, { code: "[].filter.constructor('return bypassed=1')()", evalExpect: 'bypassed', safeExpect: '/bypassed is not defined/', category: 'Security', }, { code: "[].filter.constructor.apply(null,['return bypassed=1'])()", evalExpect: 'bypassed', safeExpect: '/bypassed is not defined/', category: 'Security', }, { code: "[].filter.constructor('return this.bypassed=1')()", evalExpect: 'bypassed', safeExpect: 1, category: 'Security', }, { code: "[].filter.constructor('return this.constructor.name')()", evalExpect: 'Window', safeExpect: 'SandboxGlobal', category: 'Security', }, { code: "[].filter.constructor.call(this,'bypassed = 1')()", evalExpect: 'bypassed', safeExpect: '/bypassed is not defined/', category: 'Security', }, { code: '[].filter.constructor(\'return eval("bypassed = 1")\')()', evalExpect: 'bypassed', safeExpect: '/bypassed is not defined/', category: 'Security', }, { code: '[+!+[]]+[]', evalExpect: '1', safeExpect: '1', category: 'Security', }, { code: '[][(![]+[])[+!+[]]+(!![]+[])[+[]]][([][(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+[]]+([][(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]](([][(!![]+[])[!+[]+!+[]+!+[]]+([][[]]+[])[+!+[]]+(!![]+[])[+[]]+(!![]+[])[+!+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(!![]+[])[!+[]+!+[]+!+[]]+(![]+[])[!+[]+!+[]+!+[]]]()+[])[!+[]+!+[]]+(+[![]]+[+(+!+[]+(!+[]+[])[!+[]+!+[]+!+[]]+[+!+[]]+[+[]]+[+[]]+[+[]])])[+!+[]+[+[]]]+(+(!+[]+!+[]+[+!+[]]+[+!+[]]))[(!![]+[])[+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+([]+[])[([][(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+[]]+([][(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]][([][[]]+[])[+!+[]]+(![]+[])[+!+[]]+((+[])[([][(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+[]]+([][(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]]+[])[+!+[]+[+!+[]]]+(!![]+[])[!+[]+!+[]+!+[]]]](!+[]+!+[]+!+[]+[+!+[]])[+!+[]]+(![]+[])[+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[!+[]+!+[]+!+[]]+([][[]]+[])[!+[]+!+[]]+([]+[])[(![]+[])[+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(!![]+[])[+[]]+([][(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]]()[+!+[]+[+!+[]]]+[+!+[]])()', evalExpect: 'bypassed', safeExpect: '/bypassed is not defined/', ignoreTime: true, category: 'Security', }, { code: 'Object.entries(this).find(a => a[0] === "Function")?.at(1)("bypassed=1")()', evalExpect: 'bypassed', safeExpect: '/bypassed is not defined/', category: 'Security', }, { code: 'Object.values(this).find(a => {if(a?.name?.endsWith("Function")) return a.call("", "return bypassed = 1")()})', evalExpect: 'bypassed', safeExpect: '/bypassed is not defined/', category: 'Security', }, { code: '(async () => (await (async () => Function(\'return Object.values(this).at(Object.values(this).findIndex(a => {if(a?.name?.endsWith("Function")) return true}))\')())()).name)()', evalExpect: 'Function', safeExpect: 'SandboxFunction', category: 'Security', }, { code: '[].constructor.constructor.constructor.name', evalExpect: 'Function', safeExpect: 'SandboxFunction', category: 'Security', }, { code: '(()=>{}).constructor.name', evalExpect: 'Function', safeExpect: 'SandboxFunction', category: 'Security', }, { code: '(async ()=>{}).constructor.name', evalExpect: 'AsyncFunction', safeExpect: 'SandboxAsyncFunction', category: 'Security', }, { code: '(function*(){}).constructor.name', evalExpect: 'GeneratorFunction', safeExpect: 'SandboxGeneratorFunction', category: 'Security', }, { code: '(async function*(){}).constructor.name', evalExpect: 'AsyncGeneratorFunction', safeExpect: 'SandboxAsyncGeneratorFunction', category: 'Security', }, { code: "(async ()=>{}).constructor('return this')().then((a) => a.constructor.name)", evalExpect: 'Window', safeExpect: 'SandboxGlobal', category: 'Security', }, { code: '[].anything = 1', evalExpect: 1, safeExpect: 1, category: 'Security', }, { code: '[].filter = 1', evalExpect: 1, safeExpect: "/Override prototype property 'filter' not allowed/", category: 'Security', }, { code: '[].constructor.prototype.flatMap = 1', evalExpect: 1, safeExpect: '/Access to prototype of global object is not permitted/', category: 'Security', }, { code: '[].__proto__.flatMap = 1', evalExpect: 1, safeExpect: '/Access to prototype of global object is not permitted/', category: 'Security', }, { code: 'let p; p = [].constructor.prototype; p.flatMap = 2; return [].flatMap;', evalExpect: 2, safeExpect: '/Access to prototype of global object is not permitted/', category: 'Security', }, { code: 'Object.anything = 1', evalExpect: 'error', safeExpect: "/Cannot assign property 'anything' of a global object/", category: 'Security', }, { code: 'Object.freeze = 1', evalExpect: 'error', safeExpect: '/Static method or property access not permitted/', category: 'Security', }, { code: '{}.constructor.anything1 = 1', evalExpect: 1, safeExpect: "/Cannot assign property 'anything1' of a global object/", category: 'Security', }, { code: '{}.constructor.freeze = 1', evalExpect: 1, safeExpect: '/Static method or property access not permitted/', category: 'Security', }, { code: '{}.constructor.constructor.anything = 1', evalExpect: 1, safeExpect: "/Cannot assign property 'anything' of a global object/", category: 'Security', }, { code: '(() => {}).anything = 1', evalExpect: 1, safeExpect: 1, category: 'Security', }, { code: 'Object.name', evalExpect: 'error', safeExpect: 'Object', category: 'Security', }, { code: "Object.assign(Object, {}) || 'ok'", evalExpect: 'error', safeExpect: '/Static method or property access not permitted/', category: 'Security', }, { code: "({}).__defineGetter__('a', () => 1 ) || 'ok'", evalExpect: 'ok', safeExpect: '/Method or property access not permitted/', category: 'Security', }, { code: "(() => {}).__defineGetter__('a', () => 1 ) || 'ok'", evalExpect: 'ok', safeExpect: '/Method or property access not permitted/', category: 'Security', }, { code: "({}).toString.__defineGetter__('a', () => 1 ) || 'ok'", evalExpect: 'ok', safeExpect: '/Method or property access not permitted/', category: 'Security', }, { code: `const arr=[Array.prototype]; arr[0].polluted = 1; return [].polluted;`, evalExpect: 1, safeExpect: '/Access to prototype of global object is not permitted/', category: 'Security', }, { code: `(async () => Array.prototype)().then((a) => a.polluted = 'pwned').then(() => [].polluted)`, evalExpect: 'ok', safeExpect: '/Access to prototype of global object is not permitted/', category: 'Security', }, { code: `function x() {}; x.prototype.permitted = true; return new x().permitted;`, evalExpect: true, safeExpect: true, category: 'Security', }, { code: `function x() {}; const y = new x(); y.__proto__.permitted = true; return y.permitted;`, evalExpect: true, safeExpect: true, category: 'Security', }, { code: `function x() {}; const y = new x(); y.__proto__.__proto__.forbidden = true; return y.forbidden;`, evalExpect: true, safeExpect: '/Access to prototype of global object is not permitted/', category: 'Security', }, { code: `hasOwnProperty = 1; return ({}).hasOwnProperty`, evalExpect: 'error', safeExpect: '/hasOwnProperty is not defined/', category: 'Security', }, { code: `Object.values(this).includes(Function)`, evalExpect: true, safeExpect: true, category: 'Security', }, { code: `Object.values(this).at(1)('bypassed=1')()`, evalExpect: 'bypassed', safeExpect: '/bypassed is not defined/', category: 'Security', }, { code: ` const p = (async function () {})(); ({ "finally": p.finally, ...Object.fromEntries([['then', ...Object.values(this).slice(1)]]), }).finally('bypassed=1')(); `, evalExpect: 'bypassed', safeExpect: '/bypassed is not defined/', category: 'Security', }, { code: `const a = Function; a.anything = 1; return a.anything;`, evalExpect: 1, safeExpect: "/Cannot assign property 'anything' of a global object/", category: 'Security', }, { code: 'this.Function = 1', evalExpect: 1, safeExpect: "/Cannot assign property 'Function' of a global object/", category: 'Security', }, { code: ` const callOp = (function fn() { return fn.caller; })(); function makeContext(capture = () => {}) { return { ctx: { options: 0 }, evals: { get: capture } }; } function leakStatic(obj, prop) { let leaked; callOp({ done() {}, a() {}, b: [], obj: { context: obj, prop, get() {} }, context: makeContext((fn) => (leaked = fn, () => 1)) }); return leaked; } function callDirect(fn, args) { let value; callOp({ done(_, result) { value = result; }, a() {}, b: args, obj: fn, context: makeContext() }); return value; } callDirect(leakStatic(Object, 'defineProperty'), [ leakStatic, 'call', callDirect(leakStatic(Object, 'getOwnPropertyDescriptor'), [ callDirect(leakStatic(Object, 'getPrototypeOf'), [() => 0]), 'constructor' ]) ]); let hostFn; callOp({ done(_, result) { hostFn = result; }, a: leakStatic, b: [], obj: { context: 'return globalThis.bypassed = 1', get() {} }, context: makeContext() }); return hostFn(); `, evalExpect: 'error', safeExpect: "/Access to 'caller' property is not permitted/", category: 'Security', }, ]; ================================================ FILE: test/eval/testCases/security.spec.ts ================================================ 'use strict'; import { run, getState } from './test-utils.js'; import { tests } from './security.data.js'; describe('Security Tests', () => { describe('Sync', () => { tests.forEach((test) => { let state = getState(); it(test.code.substring(0, 100), async () => { await run(test, state, false); }); }); }); describe('Async', () => { tests.forEach((test) => { let state = getState(); it(test.code.substring(0, 100), async () => { await run(test, state, true); }); }); }); }); ================================================ FILE: test/eval/testCases/switch.data.ts ================================================ 'use strict'; import { TestCase } from './types.js'; export const tests: TestCase[] = [ { code: 'let a = 1; let b = 2; switch(1) {case a: b = 1; case b: return b; default: return 0;}', evalExpect: 1, safeExpect: 1, category: 'Switch', }, { code: 'let b = 1; switch(1) {case 1: b = 2; break; case 2: b = 3; default: b = 4}; return b', evalExpect: 2, safeExpect: 2, category: 'Switch', }, { code: 'let b = 1; switch(3) {case 1: b = 2; break; case 2: b = 3; default: b = 4}; return b', evalExpect: 4, safeExpect: 4, category: 'Switch', }, { code: 'let b = 1; switch(1) {case 1:b = 2; case 2: b = 3; default: b = 4}; return b', evalExpect: 4, safeExpect: 4, category: 'Switch', }, { code: 'let a = 1; switch(a) {case 1: return 2}; return 1', evalExpect: 2, safeExpect: 2, category: 'Switch', }, { code: 'switch (1) { case 1: { return 42; } default: return 0; }', evalExpect: 42, safeExpect: 42, category: 'Switch', }, { code: 'switch (1) { case 1: case 2: return 7; default: return 0; }', evalExpect: 7, safeExpect: 7, category: 'Switch', }, ]; ================================================ FILE: test/eval/testCases/switch.spec.ts ================================================ 'use strict'; import { run, getState } from './test-utils.js'; import { tests } from './switch.data.js'; describe('Switch Tests', () => { describe('Sync', () => { tests.forEach((test) => { let state = getState(); it(test.code.substring(0, 100), async () => { await run(test, state, false); }); }); }); describe('Async', () => { tests.forEach((test) => { let state = getState(); it(test.code.substring(0, 100), async () => { await run(test, state, true); }); }); }); }); ================================================ FILE: test/eval/testCases/syntax-errors.data.ts ================================================ 'use strict'; import { TestCase } from './types.js'; function toEvalTest(code: string, safeExpect: string): TestCase { return { code: `return eval(${JSON.stringify(code)});`, evalExpect: 'error', safeExpect, category: 'Syntax Errors', }; } export const tests: TestCase[] = [ // Entry and delimiter guards toEvalTest("const value = 'oops", "/Unclosed '\\''/"), toEvalTest('const value = "oops', '/Unclosed \'"/'), toEvalTest('const value = `oops', "/Unclosed '`'/"), toEvalTest('const value = 1; /* open comment', "/Unclosed comment '\\/\\*'/"), // Expression and property guards toEvalTest('const value =', '/Unexpected end of expression/'), toEvalTest('const value = [1 2];', '/Unexpected token/'), toEvalTest('const value = { a: 1 b: 2 };', '/Unexpected token/'), toEvalTest('fn(, value);', '/Unexpected end of expression/'), toEvalTest('const obj = { [key] };', '/Unexpected token in computed property/'), toEvalTest( 'return -2 ** 3;', '/Unary operator used immediately before exponentiation expression/', ), toEvalTest('value.', '/Unexpected token after prop: \\./'), toEvalTest('const value = for;', "/Unexpected token 'for'/"), toEvalTest('return 1 + ;', '/Unexpected end of expression/'), toEvalTest('return 1,;', '/Unexpected token/'), toEvalTest('return value ? truthy :;', '/Unexpected end of expression/'), toEvalTest('({}).a?.;', '/Unexpected token/'), toEvalTest('({}).a?.[;', "/Unclosed '\\['/"), toEvalTest('a?.b = 1;', '/Invalid left-hand side in assignment/'), toEvalTest('new Map?.();', '/optional chain|Unexpected/'), toEvalTest('a[];', '/Unexpected end of expression/'), toEvalTest('a[;', "/Unclosed '\\['/"), toEvalTest('return (1 + 2;', "/Unclosed '\\('/"), toEvalTest('const value = [1, 2;', "/Unclosed '\\['/"), toEvalTest('const value = { a: 1;', "/Unclosed '\\{'/"), toEvalTest('fn(value;', "/Unclosed '\\('/"), // Statement-form guards toEvalTest('switch (value) case 1: break;', '/Invalid switch/'), toEvalTest( 'switch (value) { default: break; default: break; }', '/Only one default switch case allowed/', ), toEvalTest('function fn(for) { return for; }', "/Unexpected token 'for'/"), toEvalTest('async function fn(await) {}', "/Unexpected token 'await'/"), toEvalTest('const [value];', '/Destructuring declaration requires an initializer/'), toEvalTest('const [value + 1] = arr;', '/Invalid destructuring target/'), toEvalTest('const { a: value + 1 } = obj;', '/Invalid destructuring target/'), toEvalTest( 'function fn(...rest, last) { return rest; }', '/Rest parameter must be last formal parameter/', ), toEvalTest('(first, ...rest, last) => first', '/Rest parameter must be last formal parameter/'), toEvalTest('for (const [a, b] from pairs) {}', '/Invalid for loop definition/'), toEvalTest('for (let i = 0; i < 2) {}', '/Invalid for loop definition/'), toEvalTest('if (value) else return 1;', '/Unexpected token/'), toEvalTest('while () {}', '/Unexpected end of expression/'), toEvalTest('do { value++; }', "/Unclosed '\\('/"), toEvalTest('try {} catch () {}', "/Unexpected token '\\)'/"), toEvalTest('try { value++; }', '/Missing catch or finally after try/'), toEvalTest('try {} catch (for) {}', "/Unexpected token 'for'/"), toEvalTest('try {} finally', '/Unexpected token/'), toEvalTest('throw ;', '/Unexpected end of expression/'), toEvalTest('new ;', '/Unexpected end of expression/'), toEvalTest('typeof ;', '/Unexpected end of expression/'), toEvalTest('delete ;', '/Unexpected end of expression/'), toEvalTest('void ;', '/Unexpected end of expression/'), toEvalTest('"a" in ;', '/Unexpected end of expression/'), toEvalTest('value instanceof ;', '/Unexpected end of expression/'), toEvalTest('const value = [...];', '/Unexpected end of expression/'), toEvalTest('const value = {...};', '/Unexpected end of expression/'), toEvalTest('fn(...);', '/Unexpected end of expression/'), toEvalTest('while (true) { break 1; }', "/Unexpected token '1'/"), toEvalTest('while (true) { continue 1; }', "/Unexpected token '1'/"), // Known parser gaps toEvalTest('for (const { a } of) {}', '/Unexpected end of expression/'), toEvalTest('const { ...rest, value } = obj;', '/Rest element must be last element/'), toEvalTest( 'function fn(...[rest], last) { return last; }', '/Rest parameter must be last formal parameter/', ), toEvalTest('yield 1;', '/Unexpected token/'), toEvalTest('function* gen(){ yield* ; }', '/Unexpected end of expression/'), toEvalTest('async function fn(){ await ; }', '/Unexpected end of expression/'), toEvalTest('async function* gen(){ yield* ; }', '/Unexpected end of expression/'), toEvalTest('switch (x) { case: break; }', '/Unexpected end of expression/'), toEvalTest('switch (x) { case 1 break; }', '/switch|case|Unexpected/'), toEvalTest('1++;', '/Invalid left-hand side expression in postfix operation/'), toEvalTest('--1;', '/left-hand side|Unexpected/'), toEvalTest('async function run() { for await (const key in obj) {} }', "/Unexpected token 'in'/"), toEvalTest( 'async function run(){ for await (const item of) {} }', '/Unexpected end of expression/', ), toEvalTest('const re = /(/;', '/Invalid regular expression/'), toEvalTest('const re = /a/zz;', '/Invalid flags/'), toEvalTest('tag`${}`;', '/Unexpected end of expression/'), toEvalTest('tag`${value`;', '/Unclosed/'), toEvalTest('({a:1,,b:2})', '/Unexpected token ,/'), ]; ================================================ FILE: test/eval/testCases/syntax-errors.spec.ts ================================================ 'use strict'; import { run, getState } from './test-utils.js'; import { tests } from './syntax-errors.data.js'; describe('Eval Syntax Errors Tests', () => { describe('Sync', () => { tests.forEach((test) => { let state = getState(); it(test.code.substring(0, 100), async () => { await run(test, state, false); }); }); }); describe('Async', () => { tests.forEach((test) => { let state = getState(); it(test.code.substring(0, 100), async () => { await run(test, state, true); }); }); }); }); ================================================ FILE: test/eval/testCases/template-literals.data.ts ================================================ 'use strict'; import { TestCase } from './types.js'; export const tests: TestCase[] = [ { code: 'const a = 1; const b = 2; return `${a} + ${b} = ${a+b}`', evalExpect: '1 + 2 = 3', safeExpect: '1 + 2 = 3', category: 'Template Literals', }, { code: "const name = 'world'; return `hello ${name}`", evalExpect: 'hello world', safeExpect: 'hello world', category: 'Template Literals', }, { code: 'function tag(strings, ...values) { return strings[0] + values[0] + strings[1]; } return tag`hello ${"world"}`', evalExpect: 'hello world', safeExpect: 'hello world', category: 'Template Literals', }, { code: 'function tag(strings, ...values) { return values.reduce((acc, val, i) => acc + val + strings[i + 1], strings[0]); } return tag`a${1}b${2}c${3}d`', evalExpect: 'a1b2c3d', safeExpect: 'a1b2c3d', category: 'Template Literals', }, { code: 'const tagging = () => tag; function tag(strings, ...values) { return values.reduce((acc, val, i) => acc + val + strings[i + 1], strings[0]); } return tagging()`a${1}b${2}c${3}d`', evalExpect: 'a1b2c3d', safeExpect: 'a1b2c3d', category: 'Template Literals', }, { code: 'function tag(strings) { return strings[0]; } return tag`static template`', evalExpect: 'static template', safeExpect: 'static template', category: 'Template Literals', }, { code: 'const tag = (strings, ...values) => strings.length; return tag`test`', evalExpect: 1, safeExpect: 1, category: 'Template Literals', }, { code: 'const multiply = (strings, a, b) => a * b; return multiply`${2} * ${3}`', evalExpect: 6, safeExpect: 6, category: 'Template Literals', }, { code: 'const tag = (s, ...v) => v.length; return tag`${1}${2}${3}`', evalExpect: 3, safeExpect: 3, category: 'Template Literals', }, { code: 'const obj = { tag(s, v) { return v * 2; } }; return obj.tag`${5}`', evalExpect: 10, safeExpect: 10, category: 'Template Literals', }, { code: 'const tag = s => s.join("-"); return tag`a${"b"}c`', evalExpect: 'a-c', safeExpect: 'a-c', category: 'Template Literals', }, { code: 'const inner = "world"; return `hello ${`${inner.toUpperCase()}!`}`', evalExpect: 'hello WORLD!', safeExpect: 'hello WORLD!', category: 'Template Literals', }, { code: 'const values = [1, 2]; return `${values.map(v => `${v * 2}`).join(",")}`', evalExpect: '2,4', safeExpect: '2,4', category: 'Template Literals', }, ]; ================================================ FILE: test/eval/testCases/template-literals.spec.ts ================================================ 'use strict'; import { run, getState } from './test-utils.js'; import { tests } from './template-literals.data.js'; describe('Template Literals Tests', () => { describe('Sync', () => { tests.forEach((test) => { let state = getState(); it(test.code.substring(0, 100), async () => { await run(test, state, false); }); }); }); describe('Async', () => { tests.forEach((test) => { let state = getState(); it(test.code.substring(0, 100), async () => { await run(test, state, true); }); }); }); }); ================================================ FILE: test/eval/testCases/test-utils.ts ================================================ 'use strict'; import Sandbox, { LocalScope } from '../../../src/Sandbox.js'; import { TestCase } from './types.js'; declare global { var bypassed: boolean; } class TestError { constructor(public error: Error | null | undefined) {} } export async function run(test: TestCase, state: any, isAsync: boolean) { globalThis.bypassed = false; let ret; const sandbox = new Sandbox(); try { const c = `${test.code.includes(';') || test.code.startsWith('throw') ? '' : 'return '}${test.code}`; let fn = isAsync ? sandbox.compileAsync(c, true) : sandbox.compile(c, true); ret = await fn(state, new LocalScope()).run(); } catch (e: any) { ret = new TestError(e); } let res; try { res = await ret; } catch (e: any) { res = new TestError(e); } expect(globalThis.bypassed).toBe(false); if (test.safeExpect === 'error') { expect(res).toBeInstanceOf(TestError); } else if (typeof test.safeExpect === 'string' && test.safeExpect.startsWith('/')) { const regex = new RegExp(test.safeExpect.slice(1, -1)); expect(res).toBeInstanceOf(TestError); expect((res as TestError).error?.message).toMatch(regex); } else if (test.safeExpect === 'NaN') { expect(res).toBeNaN(); } else { expect(res).toEqual(test.safeExpect); } } export function getState() { const a = { type: 'Sandbox', test: [ (a: any, b: any) => { return 1; }, ], test2: 1, a: { b: { c: 2 } }, }; Object.setPrototypeOf(a, LocalScope.prototype); return a; } ================================================ FILE: test/eval/testCases/types.ts ================================================ export interface TestCase { code: string; evalExpect?: unknown; safeExpect?: unknown; category: string; ignoreTime?: boolean; } ================================================ FILE: test/eval/tests.json ================================================ [ { "code": "`${type}`", "evalExpect": "eval", "safeExpect": "Sandbox", "category": "Data Types" }, { "code": "test2", "evalExpect": 1, "safeExpect": 1, "category": "Data Types" }, { "code": "2.2204460492503130808472633361816E-16", "evalExpect": 2.220446049250313e-16, "safeExpect": 2.220446049250313e-16, "category": "Data Types" }, { "code": "\"test2\"", "evalExpect": "test2", "safeExpect": "test2", "category": "Data Types" }, { "code": "`test2 is ${`also ${test2}`}`", "evalExpect": "test2 is also 1", "safeExpect": "test2 is also 1", "category": "Data Types" }, { "code": "\"\\\\\"", "evalExpect": "\\", "safeExpect": "\\", "category": "Data Types" }, { "code": "`\\\\$$\\${${`\\\\\\`${'ok'}`}\\\\}`", "evalExpect": "\\$$${\\`ok\\}", "safeExpect": "\\$$${\\`ok\\}", "category": "Data Types" }, { "code": "[\"\\\\\", \"\\xd9\", \"\\n\", \"\\r\", \"\\u2028\", \"\\u2029\"]", "evalExpect": ["\\", "Ù", "\n", "\r", "
", "
"], "safeExpect": ["\\", "Ù", "\n", "\r", "
", "
"], "category": "Data Types" }, { "code": "/a/.test('a')", "evalExpect": true, "safeExpect": true, "category": "Data Types" }, { "code": "/a/i.test('A')", "evalExpect": true, "safeExpect": true, "category": "Data Types" }, { "code": "let reg = /a/g; reg.exec('aaa'); return reg.exec('aaa').index", "evalExpect": 1, "safeExpect": 1, "category": "Data Types" }, { "code": "(1n + 0x1n).toString()", "evalExpect": "2", "safeExpect": "2", "category": "Data Types" }, { "code": "0b1010", "evalExpect": 10, "safeExpect": 10, "category": "Data Types" }, { "code": "0B1111", "evalExpect": 15, "safeExpect": 15, "category": "Data Types" }, { "code": "0b1010n.toString()", "evalExpect": "10", "safeExpect": "10", "category": "Data Types" }, { "code": "0b1_000", "evalExpect": 8, "safeExpect": 8, "category": "Data Types" }, { "code": "1_000", "evalExpect": 1000, "safeExpect": 1000, "category": "Data Types" }, { "code": "0b0", "evalExpect": 0, "safeExpect": 0, "category": "Data Types" }, { "code": "0o17", "evalExpect": 15, "safeExpect": 15, "category": "Data Types" }, { "code": "0O77", "evalExpect": 63, "safeExpect": 63, "category": "Data Types" }, { "code": "0o17n.toString()", "evalExpect": "15", "safeExpect": "15", "category": "Data Types" }, { "code": "0o7_777", "evalExpect": 4095, "safeExpect": 4095, "category": "Data Types" }, { "code": "0o0", "evalExpect": 0, "safeExpect": 0, "category": "Data Types" }, { "code": "0b1010 + 0o17", "evalExpect": 25, "safeExpect": 25, "category": "Data Types" }, { "code": "(0b1010n + 0o17n).toString()", "evalExpect": "25", "safeExpect": "25", "category": "Data Types" }, { "code": "String(BigInt('12345'))", "evalExpect": "12345", "safeExpect": "12345", "category": "Data Types" }, { "code": "(123_456_789n).toString()", "evalExpect": "123456789", "safeExpect": "123456789", "category": "Data Types" }, { "code": "/test/gi.test('test')", "evalExpect": true, "safeExpect": true, "category": "Data Types" }, { "code": "/[a-z]+/i.test('Hello')", "evalExpect": true, "safeExpect": true, "category": "Data Types" }, { "code": "NaN", "evalExpect": "NaN", "safeExpect": "NaN", "category": "Data Types" }, { "code": "typeof Infinity === 'number'", "evalExpect": true, "safeExpect": true, "category": "Data Types" }, { "code": "typeof undefined === 'undefined'", "evalExpect": true, "safeExpect": true, "category": "Data Types" }, { "code": "null", "evalExpect": null, "safeExpect": null, "category": "Data Types" }, { "code": "true", "evalExpect": true, "safeExpect": true, "category": "Data Types" }, { "code": "false", "evalExpect": false, "safeExpect": false, "category": "Data Types" }, { "code": "undefined === undefined", "evalExpect": true, "safeExpect": true, "category": "Data Types" }, { "code": "globalThis.constructor.name", "evalExpect": "Window", "safeExpect": "SandboxGlobal", "category": "Security" }, { "code": "this.constructor.name", "evalExpect": "Window", "safeExpect": "SandboxGlobal", "category": "Security" }, { "code": "eval.name", "evalExpect": "eval", "safeExpect": "sandboxEval", "category": "Security" }, { "code": "return eval(\"1+1; 2+2;\")", "evalExpect": 4, "safeExpect": 4, "category": "Security" }, { "code": "Function.name", "evalExpect": "Function", "safeExpect": "SandboxFunction", "category": "Security" }, { "code": "bypassed=1", "evalExpect": "error", "safeExpect": "/bypassed is not defined/", "category": "Security" }, { "code": "`${`${bypassed=1}`}`", "evalExpect": "error", "safeExpect": "/bypassed is not defined/", "category": "Security" }, { "code": "[].filter.constructor(\"return 'ok'\")()", "evalExpect": "ok", "safeExpect": "ok", "category": "Security" }, { "code": "[].filter.constructor('return bypassed')()", "evalExpect": false, "safeExpect": "/bypassed is not defined/", "category": "Security" }, { "code": "[].filter.constructor('return bypassed=1')()", "evalExpect": "bypassed", "safeExpect": "/bypassed is not defined/", "category": "Security" }, { "code": "[].filter.constructor.apply(null,['return bypassed=1'])()", "evalExpect": "bypassed", "safeExpect": "/bypassed is not defined/", "category": "Security" }, { "code": "[].filter.constructor('return this.bypassed=1')()", "evalExpect": "bypassed", "safeExpect": 1, "category": "Security" }, { "code": "[].filter.constructor('return this.constructor.name')()", "evalExpect": "Window", "safeExpect": "SandboxGlobal", "category": "Security" }, { "code": "[].filter.constructor.call(this,'bypassed = 1')()", "evalExpect": "bypassed", "safeExpect": "/bypassed is not defined/", "category": "Security" }, { "code": "[].filter.constructor('return eval(\"bypassed = 1\")')()", "evalExpect": "bypassed", "safeExpect": "/bypassed is not defined/", "category": "Security" }, { "code": "[+!+[]]+[]", "evalExpect": "1", "safeExpect": "1", "category": "Security" }, { "code": "[][(![]+[])[+!+[]]+(!![]+[])[+[]]][([][(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+[]]+([][(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]](([][(!![]+[])[!+[]+!+[]+!+[]]+([][[]]+[])[+!+[]]+(!![]+[])[+[]]+(!![]+[])[+!+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(!![]+[])[!+[]+!+[]+!+[]]+(![]+[])[!+[]+!+[]+!+[]]]()+[])[!+[]+!+[]]+(+[![]]+[+(+!+[]+(!+[]+[])[!+[]+!+[]+!+[]]+[+!+[]]+[+[]]+[+[]]+[+[]])])[+!+[]+[+[]]]+(+(!+[]+!+[]+[+!+[]]+[+!+[]]))[(!![]+[])[+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+([]+[])[([][(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+[]]+([][(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]][([][[]]+[])[+!+[]]+(![]+[])[+!+[]]+((+[])[([][(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+[]]+([][(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]]+[])[+!+[]+[+!+[]]]+(!![]+[])[!+[]+!+[]+!+[]]]](!+[]+!+[]+!+[]+[+!+[]])[+!+[]]+(![]+[])[+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[!+[]+!+[]+!+[]]+([][[]]+[])[!+[]+!+[]]+([]+[])[(![]+[])[+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(!![]+[])[+[]]+([][(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]]()[+!+[]+[+!+[]]]+[+!+[]])()", "evalExpect": "bypassed", "safeExpect": "/bypassed is not defined/", "ignoreTime": true, "category": "Security" }, { "code": "Object.entries(this).find(a => a[0] === \"Function\")?.at(1)(\"bypassed=1\")()", "evalExpect": "bypassed", "safeExpect": "/bypassed is not defined/", "category": "Security" }, { "code": "Object.values(this).find(a => {if(a?.name?.endsWith(\"Function\")) return a.call(\"\", \"return bypassed = 1\")()})", "evalExpect": "bypassed", "safeExpect": "/bypassed is not defined/", "category": "Security" }, { "code": "(async () => (await (async () => Function('return Object.values(this).at(Object.values(this).findIndex(a => {if(a?.name?.endsWith(\"Function\")) return true}))')())()).name)()", "evalExpect": "Function", "safeExpect": "SandboxFunction", "category": "Security" }, { "code": "[].constructor.constructor.constructor.name", "evalExpect": "Function", "safeExpect": "SandboxFunction", "category": "Security" }, { "code": "(()=>{}).constructor.name", "evalExpect": "Function", "safeExpect": "SandboxFunction", "category": "Security" }, { "code": "(async ()=>{}).constructor.name", "evalExpect": "AsyncFunction", "safeExpect": "SandboxAsyncFunction", "category": "Security" }, { "code": "(function*(){}).constructor.name", "evalExpect": "GeneratorFunction", "safeExpect": "SandboxGeneratorFunction", "category": "Security" }, { "code": "(async function*(){}).constructor.name", "evalExpect": "AsyncGeneratorFunction", "safeExpect": "SandboxAsyncGeneratorFunction", "category": "Security" }, { "code": "(async ()=>{}).constructor('return this')().then((a) => a.constructor.name)", "evalExpect": "Window", "safeExpect": "SandboxGlobal", "category": "Security" }, { "code": "[].anything = 1", "evalExpect": 1, "safeExpect": 1, "category": "Security" }, { "code": "[].filter = 1", "evalExpect": 1, "safeExpect": "/Override prototype property 'filter' not allowed/", "category": "Security" }, { "code": "[].constructor.prototype.flatMap = 1", "evalExpect": 1, "safeExpect": "/Access to prototype of global object is not permitted/", "category": "Security" }, { "code": "[].__proto__.flatMap = 1", "evalExpect": 1, "safeExpect": "/Access to prototype of global object is not permitted/", "category": "Security" }, { "code": "let p; p = [].constructor.prototype; p.flatMap = 2; return [].flatMap;", "evalExpect": 2, "safeExpect": "/Access to prototype of global object is not permitted/", "category": "Security" }, { "code": "Object.anything = 1", "evalExpect": "error", "safeExpect": "/Cannot assign property 'anything' of a global object/", "category": "Security" }, { "code": "Object.freeze = 1", "evalExpect": "error", "safeExpect": "/Static method or property access not permitted/", "category": "Security" }, { "code": "{}.constructor.anything1 = 1", "evalExpect": 1, "safeExpect": "/Cannot assign property 'anything1' of a global object/", "category": "Security" }, { "code": "{}.constructor.freeze = 1", "evalExpect": 1, "safeExpect": "/Static method or property access not permitted/", "category": "Security" }, { "code": "{}.constructor.constructor.anything = 1", "evalExpect": 1, "safeExpect": "/Cannot assign property 'anything' of a global object/", "category": "Security" }, { "code": "(() => {}).anything = 1", "evalExpect": 1, "safeExpect": 1, "category": "Security" }, { "code": "Object.name", "evalExpect": "error", "safeExpect": "Object", "category": "Security" }, { "code": "Object.assign(Object, {}) || 'ok'", "evalExpect": "error", "safeExpect": "/Static method or property access not permitted/", "category": "Security" }, { "code": "({}).__defineGetter__('a', () => 1 ) || 'ok'", "evalExpect": "ok", "safeExpect": "/Method or property access not permitted/", "category": "Security" }, { "code": "(() => {}).__defineGetter__('a', () => 1 ) || 'ok'", "evalExpect": "ok", "safeExpect": "/Method or property access not permitted/", "category": "Security" }, { "code": "({}).toString.__defineGetter__('a', () => 1 ) || 'ok'", "evalExpect": "ok", "safeExpect": "/Method or property access not permitted/", "category": "Security" }, { "code": "const arr=[Array.prototype]; arr[0].polluted = 1; return [].polluted;", "evalExpect": 1, "safeExpect": "/Access to prototype of global object is not permitted/", "category": "Security" }, { "code": "(async () => Array.prototype)().then((a) => a.polluted = 'pwned').then(() => [].polluted)", "evalExpect": "ok", "safeExpect": "/Access to prototype of global object is not permitted/", "category": "Security" }, { "code": "function x() {}; x.prototype.permitted = true; return new x().permitted;", "evalExpect": true, "safeExpect": true, "category": "Security" }, { "code": "function x() {}; const y = new x(); y.__proto__.permitted = true; return y.permitted;", "evalExpect": true, "safeExpect": true, "category": "Security" }, { "code": "function x() {}; const y = new x(); y.__proto__.__proto__.forbidden = true; return y.forbidden;", "evalExpect": true, "safeExpect": "/Access to prototype of global object is not permitted/", "category": "Security" }, { "code": "hasOwnProperty = 1; return ({}).hasOwnProperty", "evalExpect": "error", "safeExpect": "/hasOwnProperty is not defined/", "category": "Security" }, { "code": "Object.values(this).includes(Function)", "evalExpect": true, "safeExpect": true, "category": "Security" }, { "code": "Object.values(this).at(1)('bypassed=1')()", "evalExpect": "bypassed", "safeExpect": "/bypassed is not defined/", "category": "Security" }, { "code": "\nconst p = (async function () {})();\n({\n \"finally\": p.finally,\n ...Object.fromEntries([['then', ...Object.values(this).slice(1)]]),\n}).finally('bypassed=1')();\n", "evalExpect": "bypassed", "safeExpect": "/bypassed is not defined/", "category": "Security" }, { "code": "const a = Function; a.anything = 1; return a.anything;", "evalExpect": 1, "safeExpect": "/Cannot assign property 'anything' of a global object/", "category": "Security" }, { "code": "this.Function = 1", "evalExpect": 1, "safeExpect": "/Cannot assign property 'Function' of a global object/", "category": "Security" }, { "code": "\nconst callOp = (function fn() { return fn.caller; })();\n\nfunction makeContext(capture = () => {}) {\n return { ctx: { options: 0 }, evals: { get: capture } };\n}\n\nfunction leakStatic(obj, prop) {\n let leaked;\n callOp({\n done() {},\n a() {},\n b: [],\n obj: { context: obj, prop, get() {} },\n context: makeContext((fn) => (leaked = fn, () => 1))\n });\n return leaked;\n}\n\nfunction callDirect(fn, args) {\n let value;\n callOp({\n done(_, result) { value = result; },\n a() {},\n b: args,\n obj: fn,\n context: makeContext()\n });\n return value;\n}\n\ncallDirect(leakStatic(Object, 'defineProperty'), [\n leakStatic,\n 'call',\n callDirect(leakStatic(Object, 'getOwnPropertyDescriptor'), [\n callDirect(leakStatic(Object, 'getPrototypeOf'), [() => 0]),\n 'constructor'\n ])\n]);\n\nlet hostFn;\ncallOp({\n done(_, result) { hostFn = result; },\n a: leakStatic,\n b: [],\n obj: {\n context: 'return globalThis.bypassed = 1',\n get() {}\n },\n context: makeContext()\n});\n\nreturn hostFn();\n", "evalExpect": "error", "safeExpect": "/Access to 'caller' property is not permitted/", "category": "Security" }, { "code": "1+1", "evalExpect": 2, "safeExpect": 2, "category": "Arithmetic Operators" }, { "code": "1 * 2 + 3 * (4 + 5) * 6", "evalExpect": 164, "safeExpect": 164, "category": "Arithmetic Operators" }, { "code": "(test2 * (2 + 3 * (4 + 5))) * 6", "evalExpect": 174, "safeExpect": 174, "category": "Arithmetic Operators" }, { "code": "1+2*4/5-6+7/8 % 9+10-11-12/13*14", "evalExpect": -16.448076923076925, "safeExpect": -16.448076923076925, "category": "Arithmetic Operators" }, { "code": "test2 **= 0", "evalExpect": 1, "safeExpect": 1, "category": "Arithmetic Operators" }, { "code": "2 ** 3", "evalExpect": 8, "safeExpect": 8, "category": "Arithmetic Operators" }, { "code": "10 ** 0", "evalExpect": 1, "safeExpect": 1, "category": "Arithmetic Operators" }, { "code": "2 ** 10", "evalExpect": 1024, "safeExpect": 1024, "category": "Arithmetic Operators" }, { "code": "3 ** 2 ** 2", "evalExpect": 81, "safeExpect": 81, "category": "Arithmetic Operators" }, { "code": "test2 %= 1", "evalExpect": 0, "safeExpect": 0, "category": "Arithmetic Operators" }, { "code": "+'1'", "evalExpect": 1, "safeExpect": 1, "category": "Arithmetic Operators" }, { "code": "-'1'", "evalExpect": -1, "safeExpect": -1, "category": "Arithmetic Operators" }, { "code": "var i = 1; return i + 1", "evalExpect": "error", "safeExpect": 2, "category": "Arithmetic Operators" }, { "code": "let j = 1; return j + 1", "evalExpect": "error", "safeExpect": 2, "category": "Arithmetic Operators" }, { "code": "const k = 1; return k + 1", "evalExpect": "error", "safeExpect": 2, "category": "Arithmetic Operators" }, { "code": "null?.a + 5", "evalExpect": "NaN", "safeExpect": "NaN", "category": "Arithmetic Operators" }, { "code": "({}).a ?? 10 + 5", "evalExpect": 15, "safeExpect": 15, "category": "Arithmetic Operators" }, { "code": "let x = 5; return x++ + 2", "evalExpect": 7, "safeExpect": 7, "category": "Arithmetic Operators" }, { "code": "let y = 5; return ++y + 2", "evalExpect": 8, "safeExpect": 8, "category": "Arithmetic Operators" }, { "code": "+-5", "evalExpect": -5, "safeExpect": -5, "category": "Arithmetic Operators" }, { "code": "~-1", "evalExpect": 0, "safeExpect": 0, "category": "Arithmetic Operators" }, { "code": "(1, 2) + (3, 4)", "evalExpect": 6, "safeExpect": 6, "category": "Arithmetic Operators" }, { "code": "let z = 5; return --z * 2", "evalExpect": 8, "safeExpect": 8, "category": "Arithmetic Operators" }, { "code": "let z = 5; return z-- * 2", "evalExpect": 10, "safeExpect": 10, "category": "Arithmetic Operators" }, { "code": "1 + 2 + 3 + 4", "evalExpect": 10, "safeExpect": 10, "category": "Arithmetic Operators" }, { "code": "10 - 5 - 2", "evalExpect": 3, "safeExpect": 3, "category": "Arithmetic Operators" }, { "code": "2 * 3 * 4", "evalExpect": 24, "safeExpect": 24, "category": "Arithmetic Operators" }, { "code": "100 / 5 / 2", "evalExpect": 10, "safeExpect": 10, "category": "Arithmetic Operators" }, { "code": "17 % 5 % 2", "evalExpect": 0, "safeExpect": 0, "category": "Arithmetic Operators" }, { "code": "let a = 1, b = 2, c = 3; return a + b + c", "evalExpect": 6, "safeExpect": 6, "category": "Arithmetic Operators" }, { "code": "const a = 1, b = 2; return a * b", "evalExpect": 2, "safeExpect": 2, "category": "Arithmetic Operators" }, { "code": "(false ? 1 : 2) + (true ? 3 : 4)", "evalExpect": 5, "safeExpect": 5, "category": "Arithmetic Operators" }, { "code": "let x = 5; x++; return x", "evalExpect": 6, "safeExpect": 6, "category": "Arithmetic Operators" }, { "code": "let x = 5; return ++x", "evalExpect": 6, "safeExpect": 6, "category": "Arithmetic Operators" }, { "code": "let x = 5; x--; return x", "evalExpect": 4, "safeExpect": 4, "category": "Arithmetic Operators" }, { "code": "let x = 5; return --x", "evalExpect": 4, "safeExpect": 4, "category": "Arithmetic Operators" }, { "code": "test.b = test2 - test[0]() - test[0]()", "evalExpect": -1, "safeExpect": -1, "category": "Assignment Operators" }, { "code": "test2++", "evalExpect": 1, "safeExpect": 1, "category": "Assignment Operators" }, { "code": "++test2", "evalExpect": 2, "safeExpect": 2, "category": "Assignment Operators" }, { "code": "test2 = 1", "evalExpect": 1, "safeExpect": 1, "category": "Assignment Operators" }, { "code": "test2 += 1", "evalExpect": 2, "safeExpect": 2, "category": "Assignment Operators" }, { "code": "test2 -= 1", "evalExpect": 0, "safeExpect": 0, "category": "Assignment Operators" }, { "code": "test2 *= 2", "evalExpect": 2, "safeExpect": 2, "category": "Assignment Operators" }, { "code": "test2 /= 2", "evalExpect": 0.5, "safeExpect": 0.5, "category": "Assignment Operators" }, { "code": "let a = 1; let a = 2; return a", "evalExpect": "error", "safeExpect": "/Identifier 'a' has already been declared/", "category": "Assignment Operators" }, { "code": "var z = 1; var z = 2; return z", "evalExpect": 2, "safeExpect": 2, "category": "Assignment Operators" }, { "code": "let y = 1; var y = 2; return y", "evalExpect": "error", "safeExpect": "/Identifier 'y' has already been declared/", "category": "Assignment Operators" }, { "code": "var z = globalThis; let y = globalThis; return z.constructor.name === 'SandboxGlobal' && z === y", "evalExpect": true, "safeExpect": true, "category": "Assignment Operators" }, { "code": "var v = 1; var v = Object; return v.name", "evalExpect": "Object", "safeExpect": "Object", "category": "Assignment Operators" }, { "code": "let a = 1; {let a = 2}; return a", "evalExpect": 1, "safeExpect": 1, "category": "Assignment Operators" }, { "code": "this = 1", "evalExpect": "error", "safeExpect": "/\"this\" cannot be assigned/", "category": "Assignment Operators" }, { "code": "const l = 1; return l = 2", "evalExpect": "error", "safeExpect": "/Assignment to constant variable/", "category": "Assignment Operators" }, { "code": "delete 1", "evalExpect": true, "safeExpect": true, "category": "Assignment Operators" }, { "code": "let a = {b: 1}; return delete a.b", "evalExpect": true, "safeExpect": true, "category": "Assignment Operators" }, { "code": "let b = {a: 1}; return delete b", "evalExpect": false, "safeExpect": false, "category": "Assignment Operators" }, { "code": "let x = 5; x <<= 1; return x", "evalExpect": 10, "safeExpect": 10, "category": "Assignment Operators" }, { "code": "let x = 8; x >>= 1; return x", "evalExpect": 4, "safeExpect": 4, "category": "Assignment Operators" }, { "code": "let obj = {a: 1}; delete obj.a; return obj.a === undefined", "evalExpect": true, "safeExpect": true, "category": "Assignment Operators" }, { "code": "delete {a: 1}.a", "evalExpect": true, "safeExpect": true, "category": "Assignment Operators" }, { "code": "let x = 10; x <<= 2; return x;", "evalExpect": 40, "safeExpect": 40, "category": "Assignment Operators" }, { "code": "let x = 16; x >>= 2; return x;", "evalExpect": 4, "safeExpect": 4, "category": "Assignment Operators" }, { "code": "(1 == 1) == true", "evalExpect": true, "safeExpect": true, "category": "Assignment Operators" }, { "code": "let x = (1, 2, 3); return x", "evalExpect": 3, "safeExpect": 3, "category": "Assignment Operators" }, { "code": "let x = 0; x = x || 5; return x", "evalExpect": 5, "safeExpect": 5, "category": "Assignment Operators" }, { "code": "let x = 10; x = x && 5; return x", "evalExpect": 5, "safeExpect": 5, "category": "Assignment Operators" }, { "code": "let x = 10; x &&= 5; return x", "evalExpect": 5, "safeExpect": 5, "category": "Assignment Operators" }, { "code": "let x = 0; x &&= 5; return x", "evalExpect": 0, "safeExpect": 0, "category": "Assignment Operators" }, { "code": "let x = false; x &&= true; return x", "evalExpect": false, "safeExpect": false, "category": "Assignment Operators" }, { "code": "let x = 0; x ||= 5; return x", "evalExpect": 5, "safeExpect": 5, "category": "Assignment Operators" }, { "code": "let x = 10; x ||= 5; return x", "evalExpect": 10, "safeExpect": 10, "category": "Assignment Operators" }, { "code": "let x = false; x ||= \"default\"; return x", "evalExpect": "default", "safeExpect": "default", "category": "Assignment Operators" }, { "code": "let x = null; x ??= 5; return x", "evalExpect": 5, "safeExpect": 5, "category": "Assignment Operators" }, { "code": "let x = undefined; x ??= 10; return x", "evalExpect": 10, "safeExpect": 10, "category": "Assignment Operators" }, { "code": "let x = 0; x ??= 5; return x", "evalExpect": 0, "safeExpect": 0, "category": "Assignment Operators" }, { "code": "let x = \"\"; x ??= \"default\"; return x", "evalExpect": "", "safeExpect": "", "category": "Assignment Operators" }, { "code": "~test2", "evalExpect": -2, "safeExpect": -2, "category": "Bitwise Operators" }, { "code": "let test3 = 8; test3 >>>= 2; return test3", "evalExpect": 2, "safeExpect": 2, "category": "Bitwise Operators" }, { "code": "let test4 = -8; test4 >>>= 2; return test4", "evalExpect": 1073741822, "safeExpect": 1073741822, "category": "Bitwise Operators" }, { "code": "let test5 = 16; test5 >>>= 1; return test5", "evalExpect": 8, "safeExpect": 8, "category": "Bitwise Operators" }, { "code": "let test6 = -1; test6 >>>= 0; return test6", "evalExpect": 4294967295, "safeExpect": 4294967295, "category": "Bitwise Operators" }, { "code": "test2 ^= 1", "evalExpect": 0, "safeExpect": 0, "category": "Bitwise Operators" }, { "code": "test2 &= 3", "evalExpect": 1, "safeExpect": 1, "category": "Bitwise Operators" }, { "code": "test2 |= 2", "evalExpect": 3, "safeExpect": 3, "category": "Bitwise Operators" }, { "code": "test2 & 1", "evalExpect": 1, "safeExpect": 1, "category": "Bitwise Operators" }, { "code": "test2 | 4", "evalExpect": 5, "safeExpect": 5, "category": "Bitwise Operators" }, { "code": "8 >> 1 >> 1", "evalExpect": 2, "safeExpect": 2, "category": "Bitwise Operators" }, { "code": "1 << 2 << 1", "evalExpect": 8, "safeExpect": 8, "category": "Bitwise Operators" }, { "code": "16 >>> 2 >> 1", "evalExpect": 2, "safeExpect": 2, "category": "Bitwise Operators" }, { "code": "1 << 1 << 1", "evalExpect": 4, "safeExpect": 4, "category": "Bitwise Operators" }, { "code": "16 >> 1 >> 1", "evalExpect": 4, "safeExpect": 4, "category": "Bitwise Operators" }, { "code": "5 & 7 & 3", "evalExpect": 1, "safeExpect": 1, "category": "Bitwise Operators" }, { "code": "1 | 2 | 4", "evalExpect": 7, "safeExpect": 7, "category": "Bitwise Operators" }, { "code": "15 ^ 10 ^ 5", "evalExpect": 0, "safeExpect": 0, "category": "Bitwise Operators" }, { "code": "~5", "evalExpect": -6, "safeExpect": -6, "category": "Bitwise Operators" }, { "code": "/* 2 */ 1", "evalExpect": 1, "safeExpect": 1, "category": "Comments" }, { "code": "1 // 2", "evalExpect": 1, "safeExpect": 1, "category": "Comments" }, { "code": "/* 2 */ (() => /* 3 */ 1)() // 4", "evalExpect": 1, "safeExpect": 1, "category": "Comments" }, { "code": "/* never closed", "evalExpect": "error", "safeExpect": "/Unclosed comment '\\/\\*/", "category": "Comments" }, { "code": "test2 == '1'", "evalExpect": true, "safeExpect": true, "category": "Comparison Operators" }, { "code": "test2 === '1'", "evalExpect": false, "safeExpect": false, "category": "Comparison Operators" }, { "code": "test2 != '1'", "evalExpect": false, "safeExpect": false, "category": "Comparison Operators" }, { "code": "test2 !== '1'", "evalExpect": true, "safeExpect": true, "category": "Comparison Operators" }, { "code": "test2 < 1", "evalExpect": false, "safeExpect": false, "category": "Comparison Operators" }, { "code": "test2 > 1", "evalExpect": false, "safeExpect": false, "category": "Comparison Operators" }, { "code": "test2 >= 1", "evalExpect": true, "safeExpect": true, "category": "Comparison Operators" }, { "code": "test2 <= 1", "evalExpect": true, "safeExpect": true, "category": "Comparison Operators" }, { "code": "let a = null; let b = [a?.a]; return b[0] === undefined && b.length", "evalExpect": 1, "safeExpect": 1, "category": "Complex Expressions" }, { "code": "{a: 1, ...{b: 2, c: {d: test2,}}, e: 5}", "evalExpect": { "a": 1, "b": 2, "c": { "d": 1 }, "e": 5 }, "safeExpect": { "a": 1, "b": 2, "c": { "d": 1 }, "e": 5 }, "category": "Complex Expressions" }, { "code": "{a: 1,b: 2, /*,*/}", "evalExpect": { "a": 1, "b": 2 }, "safeExpect": { "a": 1, "b": 2 }, "category": "Complex Expressions" }, { "code": "test2 = 1,(() => 2)(),test2", "evalExpect": 1, "safeExpect": 1, "category": "Complex Expressions" }, { "code": "const a = () => {return 1}; const b = () => {return 2}; return (() => a() + b())()", "evalExpect": 3, "safeExpect": 3, "category": "Complex Expressions" }, { "code": "({}).a?.toString()", "category": "Complex Expressions" }, { "code": "({}).a?.toSring() + ({}).b?.toString()", "evalExpect": "NaN", "safeExpect": "NaN", "category": "Complex Expressions" }, { "code": "({})['b']?.toString() === undefined", "evalExpect": true, "safeExpect": true, "category": "Complex Expressions" }, { "code": "({}).c?.()() ? 1 : 2", "evalExpect": 2, "safeExpect": 2, "category": "Complex Expressions" }, { "code": "function plus(n,u){return n+u};function minus(n,u){return n-u};var added=plus(1,10);return minus(added,5);", "evalExpect": 6, "safeExpect": 6, "category": "Complex Expressions" }, { "code": "function LinkedListNode(e){this.value=e,this.next=null}function reverse(e){let n,t,r=e;for(;r;)t=r.next,r.next=n,n=r,r=t;return n}function reverse(e){if(!e||!e.next)return e;let n=reverse(e.next);return e.next.next=e,e.next=null,n} let l1 = new LinkedListNode(1); l1.next = new LinkedListNode(2); return reverse(l1);", "evalExpect": { "value": 2, "next": { "value": 1, "next": null } }, "safeExpect": { "value": 2, "next": { "value": 1, "next": null } }, "category": "Complex Expressions" }, { "code": "const f = function factorial(n) { return n <= 1 ? 1 : n * factorial(n - 1); }; return f(5)", "evalExpect": 120, "safeExpect": 120, "category": "Complex Expressions" }, { "code": "({a: 10}).a?.toString() + 5", "evalExpect": "105", "safeExpect": "105", "category": "Complex Expressions" }, { "code": "let arr = [1, 2, 3]; return arr?.[1]", "evalExpect": 2, "safeExpect": 2, "category": "Complex Expressions" }, { "code": "let obj = {fn: () => 42}; return obj?.fn?.()", "evalExpect": 42, "safeExpect": 42, "category": "Complex Expressions" }, { "code": "test[test2] ? true : false ? 'not ok' : 'ok'", "evalExpect": "ok", "safeExpect": "ok", "category": "Conditionals" }, { "code": "let ok = true; false ? ok = false : ok; return ok", "evalExpect": true, "safeExpect": true, "category": "Conditionals" }, { "code": "if (true) { return true; } else return false", "evalExpect": true, "safeExpect": true, "category": "Conditionals" }, { "code": "let a = false; if (a) { return false; } else return true", "evalExpect": true, "safeExpect": true, "category": "Conditionals" }, { "code": "let a = null; if (a?.a) { return false; } else return true", "evalExpect": true, "safeExpect": true, "category": "Conditionals" }, { "code": "if (false) return true; else if (false) {return true} else return false", "evalExpect": false, "safeExpect": false, "category": "Conditionals" }, { "code": "if (false) { return true; } else return false", "evalExpect": false, "safeExpect": false, "category": "Conditionals" }, { "code": "if (false) return true; return false", "evalExpect": false, "safeExpect": false, "category": "Conditionals" }, { "code": "if (true) {\n if (false)\n if (true)\n if (false)\n return 1\n else if (true)\n return 2\n else\n return 3\n else\n return 4\n else if (true)\n if (false)\n return 5\n else if (true)\n return 6\n else\n return 7\n else\n return 8\n} else if (true)\n return 9;", "evalExpect": 6, "safeExpect": 6, "category": "Conditionals" }, { "code": "throw new Error('test')", "evalExpect": "error", "safeExpect": "/test/", "category": "Error Handling" }, { "code": "throw undefined", "evalExpect": "error", "safeExpect": "error", "category": "Error Handling" }, { "code": "try {a.x.a} catch {return 1}; return 2", "evalExpect": 1, "safeExpect": 1, "category": "Error Handling" }, { "code": "try { throw new Error('test'); } catch { return 'caught'; }", "evalExpect": "caught", "safeExpect": "caught", "category": "Error Handling" }, { "code": "let x = 0; try { x = 1; } finally { x = 2; } return x;", "evalExpect": 2, "safeExpect": 2, "category": "Error Handling" }, { "code": "let x = 0; try { throw new Error(); } catch(e) { x = 1; } finally { x = 2; } return x;", "evalExpect": 2, "safeExpect": 2, "category": "Error Handling" }, { "code": "let x = 0; try { x = 1; } finally { x += 10; } return x;", "evalExpect": 11, "safeExpect": 11, "category": "Error Handling" }, { "code": "let x = 0; try { throw new Error(\"test\"); } catch(e) { x = 5; } finally { x *= 2; } return x;", "evalExpect": 10, "safeExpect": 10, "category": "Error Handling" }, { "code": "try { return 1; } finally { return 2; }", "evalExpect": 2, "safeExpect": 2, "category": "Error Handling" }, { "code": "let x = 0; try { return 10; } finally { x = 5; } return x;", "evalExpect": 10, "safeExpect": 10, "category": "Error Handling" }, { "code": "try { throw new Error(\"first\"); } finally { throw new Error(\"second\"); }", "evalExpect": "error", "safeExpect": "/second/", "category": "Error Handling" }, { "code": "try { throw new Error(\"test\"); } catch(e) { return e.message; } finally { let y = 1; }", "evalExpect": "test", "safeExpect": "test", "category": "Error Handling" }, { "code": "let result = \"\"; try { result += \"a\"; } catch(e) { result += \"b\"; } finally { result += \"c\"; } return result;", "evalExpect": "ac", "safeExpect": "ac", "category": "Error Handling" }, { "code": "let result = \"\"; try { throw new Error(); } catch(e) { result += \"b\"; } finally { result += \"c\"; } return result;", "evalExpect": "bc", "safeExpect": "bc", "category": "Error Handling" }, { "code": "function testFn1() { try { return \"try\"; } finally { } } return testFn1();", "evalExpect": "try", "safeExpect": "try", "category": "Error Handling" }, { "code": "function testFn2() { try { throw new Error(\"err\"); } catch(e) { return \"catch\"; } finally { } } return testFn2();", "evalExpect": "catch", "safeExpect": "catch", "category": "Error Handling" }, { "code": "let x = 0; try { } finally { x = 1; } return x;", "evalExpect": 1, "safeExpect": 1, "category": "Error Handling" }, { "code": "missing = 1;", "evalExpect": "error", "safeExpect": "/missing is not defined/", "category": "Error Handling" }, { "code": "Math = 1;", "evalExpect": "error", "safeExpect": "/Cannot assign property 'Math' of a global object/", "category": "Error Handling" }, { "code": "const answer = 1; answer = 2;", "evalExpect": "error", "safeExpect": "/Assignment to constant variable\\./", "category": "Error Handling" }, { "code": "1 2", "evalExpect": "error", "safeExpect": "/Unexpected token after .*: 2/", "category": "Error Handling" }, { "code": "const x = 0; ({ x } = { x: 1 });", "evalExpect": "error", "safeExpect": "/Assignment to constant variable/", "category": "Error Handling" }, { "code": "({ missing } = { missing: 1 });", "evalExpect": "error", "safeExpect": "/missing is not defined/", "category": "Error Handling" }, { "code": "{a: 1} = 1", "evalExpect": "error", "safeExpect": "/Unexpected token/", "category": "Error Handling" }, { "code": "missing: for (;;) { break nope; }", "evalExpect": "error", "safeExpect": "/Undefined label 'nope'/", "category": "Error Handling" }, { "code": "outer: { continue outer; }", "evalExpect": "error", "safeExpect": "/Illegal continue statement/", "category": "Error Handling" }, { "code": "outer: inner: { continue inner; }", "evalExpect": "error", "safeExpect": "/Illegal continue statement/", "category": "Error Handling" }, { "code": "outer: switch (1) { case 1: continue outer; }", "evalExpect": "error", "safeExpect": "/Illegal continue statement/", "category": "Error Handling" }, { "code": "outer: if (true) { continue outer; }", "evalExpect": "error", "safeExpect": "/Illegal continue statement/", "category": "Error Handling" }, { "code": "outer: try { continue outer; } finally {}", "evalExpect": "error", "safeExpect": "/Illegal continue statement/", "category": "Error Handling" }, { "code": "outer: { } break outer;", "evalExpect": "error", "safeExpect": "/Undefined label 'outer'/", "category": "Error Handling" }, { "code": "outer: for (;;) { try { break outer; } finally { throw new Error(\"stop\"); } }", "evalExpect": "error", "safeExpect": "/stop/", "category": "Error Handling" }, { "code": "nonExistentVar", "evalExpect": "error", "safeExpect": "/nonExistentVar is not defined/", "category": "Error Handling" }, { "code": "const list = []; const push = list.push; push(1); push(2); return list.join(',')", "evalExpect": "1,2", "safeExpect": "1,2", "category": "Function Replacements" }, { "code": "const list = []; const push = list.push; return push.name", "evalExpect": "push", "safeExpect": "push", "category": "Function Replacements" }, { "code": "const list = []; const other = []; const push = list.push; const rebound = push.bind(other); rebound(3); return [list.join(','), other.join(','), rebound.name].join('|')", "evalExpect": "|3|bound push", "safeExpect": "|3|bound push", "category": "Function Replacements" }, { "code": "const list = []; const other = []; const push = list.push; const rebound = push.bind(other, 3, 4); rebound(); return [list.join(','), other.join(','), rebound.name, rebound.length].join('|')", "evalExpect": "|3,4|bound push|0", "safeExpect": "|3,4|bound push|0", "category": "Function Replacements" }, { "code": "const list = []; const other = []; const third = []; const push = list.push; const rebound = push.bind(other, 3); const reboundAgain = rebound.bind(third, 4); reboundAgain(5); return [list.join(','), other.join(','), third.join(','), reboundAgain.name].join('|')", "evalExpect": "|3,4,5||bound bound push", "safeExpect": "|3,4,5||bound bound push", "category": "Function Replacements" }, { "code": "((a) => {return a + 1})(1)", "evalExpect": 2, "safeExpect": 2, "category": "Functions" }, { "code": "(() => '1' + (() => '22')())()", "evalExpect": "122", "safeExpect": "122", "category": "Functions" }, { "code": "(a => a + 1)(1)", "evalExpect": 2, "safeExpect": 2, "category": "Functions" }, { "code": "function f(a) { return a + 1 } return f(2);", "evalExpect": 3, "safeExpect": 3, "category": "Functions" }, { "code": "(function () { return 1 })()", "evalExpect": 1, "safeExpect": 1, "category": "Functions" }, { "code": "let list = [0, 1]; return list.sort((a, b) => (a < b) ? 1 : -1)", "evalExpect": [1, 0], "safeExpect": [1, 0], "category": "Functions" }, { "code": "let y = {a: 1, b(x) {return this.a + x}}; return y.b(2)", "evalExpect": 3, "safeExpect": 3, "category": "Functions" }, { "code": "let y = {a: '2', b() {return this.a = '1'}}; y.b(); return y.a", "evalExpect": "1", "safeExpect": "1", "category": "Functions" }, { "code": "[0,1].filter((...args) => args[1])", "evalExpect": [1], "safeExpect": [1], "category": "Functions" }, { "code": "Math.pow(...[2, 2])", "evalExpect": 4, "safeExpect": 4, "category": "Functions" }, { "code": "return f(); function f() { return 1; }", "evalExpect": 1, "safeExpect": 1, "category": "Functions" }, { "code": "return add(2, 3); function add(a, b) { return a + b; }", "evalExpect": 5, "safeExpect": 5, "category": "Functions" }, { "code": "let x = f(); function f() { return 42; } return x;", "evalExpect": 42, "safeExpect": 42, "category": "Functions" }, { "code": "const fn = ({ a: for }) => for; return fn({ a: 1 });", "evalExpect": "error", "safeExpect": "/Unexpected token 'for'/", "category": "Functions" }, { "code": "function fn(for) { return for; } return fn(1);", "evalExpect": "error", "safeExpect": "/Unexpected token 'for'/", "category": "Functions" }, { "code": "function fn(a, b,) { return a + b } return fn(1, 2);", "evalExpect": 3, "safeExpect": 3, "category": "Functions" }, { "code": "function fn(a, b) { return a + b } return fn(1, 2,);", "evalExpect": 3, "safeExpect": 3, "category": "Functions" }, { "code": "((a, b,) => a + b)(1, 2)", "evalExpect": 3, "safeExpect": 3, "category": "Functions" }, { "code": "!test2", "evalExpect": false, "safeExpect": false, "category": "Logical Operators" }, { "code": "!!test2", "evalExpect": true, "safeExpect": true, "category": "Logical Operators" }, { "code": "!({}).a?.a", "evalExpect": true, "safeExpect": true, "category": "Logical Operators" }, { "code": "!({a: {a: 1}}).a?.a", "evalExpect": false, "safeExpect": false, "category": "Logical Operators" }, { "code": "!({a: {a: 0}}).a?.a", "evalExpect": true, "safeExpect": true, "category": "Logical Operators" }, { "code": "!({}).a ? true : false", "evalExpect": true, "safeExpect": true, "category": "Logical Operators" }, { "code": "test2 && true", "evalExpect": true, "safeExpect": true, "category": "Logical Operators" }, { "code": "test2 || false", "evalExpect": 1, "safeExpect": 1, "category": "Logical Operators" }, { "code": "null ?? 'default'", "evalExpect": "default", "safeExpect": "default", "category": "Logical Operators" }, { "code": "undefined ?? 'default'", "evalExpect": "default", "safeExpect": "default", "category": "Logical Operators" }, { "code": "0 ?? 'default'", "evalExpect": 0, "safeExpect": 0, "category": "Logical Operators" }, { "code": "'' ?? 'default'", "evalExpect": "", "safeExpect": "", "category": "Logical Operators" }, { "code": "false ?? 'default'", "evalExpect": false, "safeExpect": false, "category": "Logical Operators" }, { "code": "NaN ?? 'default'", "evalExpect": "NaN", "safeExpect": "NaN", "category": "Logical Operators" }, { "code": "null ?? null ?? 'default'", "evalExpect": "default", "safeExpect": "default", "category": "Logical Operators" }, { "code": "1 ?? 2 ?? 3", "evalExpect": 1, "safeExpect": 1, "category": "Logical Operators" }, { "code": "null ?? 2 ?? 3", "evalExpect": 2, "safeExpect": 2, "category": "Logical Operators" }, { "code": "({a: 1}).a ?? 'default'", "evalExpect": 1, "safeExpect": 1, "category": "Logical Operators" }, { "code": "({}).a ?? 'default'", "evalExpect": "default", "safeExpect": "default", "category": "Logical Operators" }, { "code": "({}).a?.b ?? 'default'", "evalExpect": "default", "safeExpect": "default", "category": "Logical Operators" }, { "code": "null?.a ?? 'default'", "evalExpect": "default", "safeExpect": "default", "category": "Logical Operators" }, { "code": "({a: {b: 1}}).a?.b ?? 'default'", "evalExpect": 1, "safeExpect": 1, "category": "Logical Operators" }, { "code": "({a: null}).a?.b ?? 'default'", "evalExpect": "default", "safeExpect": "default", "category": "Logical Operators" }, { "code": "null ?? 1 || 2", "evalExpect": 1, "safeExpect": 1, "category": "Logical Operators" }, { "code": "0 || null ?? 1", "evalExpect": 1, "safeExpect": 1, "category": "Logical Operators" }, { "code": "(null ?? false) || 'fallback'", "evalExpect": "fallback", "safeExpect": "fallback", "category": "Logical Operators" }, { "code": "null ?? 0 || 'fallback'", "evalExpect": "error", "safeExpect": "fallback", "category": "Logical Operators" }, { "code": "true && null ?? 'default'", "evalExpect": "error", "safeExpect": "default", "category": "Logical Operators" }, { "code": "let x = 0; (1 ?? (x = 1)); return x", "evalExpect": 0, "safeExpect": 0, "category": "Logical Operators" }, { "code": "let x = 0; (null ?? (x = 1)); return x", "evalExpect": 1, "safeExpect": 1, "category": "Logical Operators" }, { "code": "let x = 0; (undefined ?? (x = 1)); return x", "evalExpect": 1, "safeExpect": 1, "category": "Logical Operators" }, { "code": "let x = 0; (0 ?? (x = 1)); return x", "evalExpect": 0, "safeExpect": 0, "category": "Logical Operators" }, { "code": "let x = 0; ('' ?? (x = 1)); return x", "evalExpect": 0, "safeExpect": 0, "category": "Logical Operators" }, { "code": "let x = 0; (false ?? (x = 1)); return x", "evalExpect": 0, "safeExpect": 0, "category": "Logical Operators" }, { "code": "({a: null ?? 'default'}).a", "evalExpect": "default", "safeExpect": "default", "category": "Logical Operators" }, { "code": "[null ?? 1, 2 ?? 3][0]", "evalExpect": 1, "safeExpect": 1, "category": "Logical Operators" }, { "code": "[null ?? 1, undefined ?? 3][1]", "evalExpect": 3, "safeExpect": 3, "category": "Logical Operators" }, { "code": "let outputs = {}; return (outputs['classify']?.intent ?? null)", "evalExpect": null, "safeExpect": null, "category": "Logical Operators" }, { "code": "let outputs = {classify: {intent: 'greeting'}}; return (outputs['classify']?.intent ?? null)", "evalExpect": "greeting", "safeExpect": "greeting", "category": "Logical Operators" }, { "code": "null ?? 5 ? \"yes\" : \"no\"", "evalExpect": "yes", "safeExpect": "yes", "category": "Logical Operators" }, { "code": "null ?? 0 ? \"yes\" : \"no\"", "evalExpect": "no", "safeExpect": "no", "category": "Logical Operators" }, { "code": "!~0", "evalExpect": false, "safeExpect": false, "category": "Logical Operators" }, { "code": "true && true && true", "evalExpect": true, "safeExpect": true, "category": "Logical Operators" }, { "code": "false || false || true", "evalExpect": true, "safeExpect": true, "category": "Logical Operators" }, { "code": "null ?? null ?? null ?? 'default'", "evalExpect": "default", "safeExpect": "default", "category": "Logical Operators" }, { "code": "1 || 2 && 3", "evalExpect": 1, "safeExpect": 1, "category": "Logical Operators" }, { "code": "0 && 1 || 2", "evalExpect": 2, "safeExpect": 2, "category": "Logical Operators" }, { "code": "true || false && false", "evalExpect": true, "safeExpect": true, "category": "Logical Operators" }, { "code": "!true", "evalExpect": false, "safeExpect": false, "category": "Logical Operators" }, { "code": "!false", "evalExpect": true, "safeExpect": true, "category": "Logical Operators" }, { "code": "let x; for(let i = 0; i < 2; i++){ x = i }; return x;", "evalExpect": 1, "safeExpect": 1, "category": "Loops" }, { "code": "let x; for(let i = 0; i < 2; i++){ x = i; break; }; return x;", "evalExpect": 0, "safeExpect": 0, "category": "Loops" }, { "code": "let x; for(let i = 0; i < 2; i++){ x = i; continue; x++ }; return x;", "evalExpect": 1, "safeExpect": 1, "category": "Loops" }, { "code": "break;", "evalExpect": "error", "safeExpect": "error", "category": "Loops" }, { "code": "continue;", "evalExpect": "error", "safeExpect": "error", "category": "Loops" }, { "code": "let sum = 0; for (let i = 0; i < 5; i++) { if (i === 2) continue; sum += i; }; return sum;", "evalExpect": 8, "safeExpect": 8, "category": "Loops" }, { "code": "let sum = 0; for (let i = 0; i < 10; i++) { if (i === 3) break; sum += i; }; return sum;", "evalExpect": 3, "safeExpect": 3, "category": "Loops" }, { "code": "let sum = 0; for (let i = 0; i < 5; i++) { if (i > 0) { if (i === 2) continue; } sum += i; }; return sum;", "evalExpect": 8, "safeExpect": 8, "category": "Loops" }, { "code": "let sum = 0; for (let i = 0; i < 5; i++) { if (i === 2) { continue; } sum += i; }; return sum;", "evalExpect": 8, "safeExpect": 8, "category": "Loops" }, { "code": "let x = 0; while (x < 5) { x++; if (x === 3) continue; if (x === 4) break; }; return x;", "evalExpect": 4, "safeExpect": 4, "category": "Loops" }, { "code": "let sum = 0; for (let i = 0; i < 5; i++) { sum += i === 2 ? continue : i; }; return sum;", "evalExpect": "error", "safeExpect": "error", "category": "Loops" }, { "code": "let x = 2; while(--x){ }; return x;", "evalExpect": 0, "safeExpect": 0, "category": "Loops" }, { "code": "let x = 1; do {x++} while(x < 1); return x;", "evalExpect": 2, "safeExpect": 2, "category": "Loops" }, { "code": "for(let i of [1,2]){ return i };", "evalExpect": 1, "safeExpect": 1, "category": "Loops" }, { "code": "let arr = [1,2]; for(let i of arr){ return i };", "evalExpect": 1, "safeExpect": 1, "category": "Loops" }, { "code": "for(let i in [1,2]){ return i };", "evalExpect": "0", "safeExpect": "0", "category": "Loops" }, { "code": "let i = 1; {let j = 1; i += j;}; return i", "evalExpect": 2, "safeExpect": 2, "category": "Loops" }, { "code": "let c = 0; for (let i = 0; i < 10; i++) {c++} return c", "evalExpect": 10, "safeExpect": 10, "category": "Loops" }, { "code": "outer: for(let i = 0; i < 3; i++) { for(let j = 0; j < 3; j++) { if(i === 1 && j === 1) break outer; } } return 'done'", "evalExpect": "done", "safeExpect": "done", "category": "Loops" }, { "code": "let hits = \"\"; outer: for (let i = 0; i < 3; i++) { for (let j = 0; j < 3; j++) { hits += `${i}${j}|`; continue outer; } } return hits;", "evalExpect": "00|10|20|", "safeExpect": "00|10|20|", "category": "Loops" }, { "code": "let out = \"\"; outer: { out += \"a\"; break outer; out += \"b\"; } return out;", "evalExpect": "a", "safeExpect": "a", "category": "Loops" }, { "code": "let out = \"\"; outer: switch (1) { case 1: out = \"ok\"; break outer; default: out = \"bad\"; } return out;", "evalExpect": "ok", "safeExpect": "ok", "category": "Loops" }, { "code": "let out = \"\"; outer: inner: for (let i = 0; i < 3; i++) { out += i; if (i < 2) continue outer; out += \"!\"; } return out;", "evalExpect": "012!", "safeExpect": "012!", "category": "Loops" }, { "code": "let out = \"\"; outer: inner: for (let i = 0; i < 3; i++) { out += i; if (i < 2) continue inner; out += \"!\"; } return out;", "evalExpect": "012!", "safeExpect": "012!", "category": "Loops" }, { "code": "let out = \"\"; first: second: { out += \"a\"; break first; out += \"b\"; } return out;", "evalExpect": "a", "safeExpect": "a", "category": "Loops" }, { "code": "let out = \"\"; first: second: switch (1) { case 1: out += \"a\"; break second; default: out += \"b\"; } return out;", "evalExpect": "a", "safeExpect": "a", "category": "Loops" }, { "code": "let out = \"\"; outer: if (true) { out += \"a\"; break outer; out += \"b\"; } return out;", "evalExpect": "a", "safeExpect": "a", "category": "Loops" }, { "code": "let out = \"\"; outer: try { out += \"a\"; break outer; } finally { out += \"f\"; } return out;", "evalExpect": "af", "safeExpect": "af", "category": "Loops" }, { "code": "let out = \"\"; outer: while (true) { out += \"a\"; break outer; } return out;", "evalExpect": "a", "safeExpect": "a", "category": "Loops" }, { "code": "let out = \"\"; outer: do { out += \"a\"; continue outer; out += \"b\"; } while (false); return out;", "evalExpect": "a", "safeExpect": "a", "category": "Loops" }, { "code": "let out = \"\"; outer: for (const value of [1, 2, 3]) { out += value; if (value === 2) break outer; } return out;", "evalExpect": "12", "safeExpect": "12", "category": "Loops" }, { "code": "let out = \"\"; outer: for (const key in { a: 1, b: 2, c: 3 }) { out += key; if (key === \"b\") break outer; } return out;", "evalExpect": "ab", "safeExpect": "ab", "category": "Loops" }, { "code": "let hits = \"\"; outer: for (let i = 0; i < 3; i++) { switch (i) { case 1: hits += \"s\"; continue outer; default: hits += i; } hits += \"|\"; } return hits;", "evalExpect": "0|s2|", "safeExpect": "0|s2|", "category": "Loops" }, { "code": "let hits = \"\"; outer: for (let i = 0; i < 3; i++) { inner: for (let j = 0; j < 3; j++) { if (j === 1) break inner; if (i === 2) break outer; hits += `${i}${j}|`; } } return hits;", "evalExpect": "00|10|", "safeExpect": "00|10|", "category": "Loops" }, { "code": "let log = \"\"; outer: { try { log += \"a\"; break outer; } finally { log += \"f\"; } log += \"x\"; } return log;", "evalExpect": "af", "safeExpect": "af", "category": "Loops" }, { "code": "let log = \"\"; outer: for (let i = 0; i < 3; i++) { for (let j = 0; j < 3; j++) { try { log += `${i}${j}|`; if (j === 1) break; } finally { if (j === 1) continue outer; } log += \"x\"; } log += \"y\"; } return log;", "evalExpect": "00|x01|10|x11|20|x21|", "safeExpect": "00|x01|10|x11|20|x21|", "category": "Loops" }, { "code": "let log = \"\"; outer: for (let i = 0; i < 3; i++) { try { switch (i) { case 2: log += \"b\"; break outer; default: log += i; } } finally { log += \"f\"; } log += \"x\"; } return log;", "evalExpect": "0fx1fxbf", "safeExpect": "0fx1fxbf", "category": "Loops" }, { "code": "for(let i = 0, j = 10; i < 3; i++, j--) { } return 'done'", "evalExpect": "done", "safeExpect": "done", "category": "Loops" }, { "code": "let i = 0; for (; i < 3;) { i++; } return i;", "evalExpect": 3, "safeExpect": 3, "category": "Loops" }, { "code": "let i = 0; for (;;) { i++; if (i === 3) break; } return i;", "evalExpect": 3, "safeExpect": 3, "category": "Loops" }, { "code": "let total = 0; for (; total < 3; total++) {} return total;", "evalExpect": 3, "safeExpect": 3, "category": "Loops" }, { "code": "let x = 0; do x++; while (x < 3); return x;", "evalExpect": 3, "safeExpect": 3, "category": "Loops" }, { "code": "a.b.c", "evalExpect": 2, "safeExpect": 2, "category": "Objects & Arrays" }, { "code": "[test2, 2]", "evalExpect": [1, 2], "safeExpect": [1, 2], "category": "Objects & Arrays" }, { "code": "{\"aa\": test[0](), b: test2 * 3}", "evalExpect": { "aa": 1, "b": 3 }, "safeExpect": { "aa": 1, "b": 3 }, "category": "Objects & Arrays" }, { "code": "{\"\\\\\":\"\\\\\"}", "evalExpect": { "\\": "\\" }, "safeExpect": { "\\": "\\" }, "category": "Objects & Arrays" }, { "code": "Object.keys({a:1})", "evalExpect": ["a"], "safeExpect": ["a"], "category": "Objects & Arrays" }, { "code": "[1, ...[2, [test2, 4]], 5]", "evalExpect": [1, 2, [1, 4], 5], "safeExpect": [1, 2, [1, 4], 5], "category": "Objects & Arrays" }, { "code": "const obj1 = {a: 1, b: 2}; const obj2 = {c: 3, ...obj1}; return obj2.a", "evalExpect": 1, "safeExpect": 1, "category": "Objects & Arrays" }, { "code": "const obj1 = {a: 1, b: 2}; const obj2 = {...obj1, b: 5}; return obj2.b", "evalExpect": 5, "safeExpect": 5, "category": "Objects & Arrays" }, { "code": "const obj1 = {x: 10}; const obj2 = {y: 20}; const obj3 = {...obj1, ...obj2}; return obj3.x + obj3.y", "evalExpect": 30, "safeExpect": 30, "category": "Objects & Arrays" }, { "code": "const arr1 = [1, 2, 3]; const arr2 = [...arr1, 4, 5]; return arr2.length", "evalExpect": 5, "safeExpect": 5, "category": "Objects & Arrays" }, { "code": "const arr1 = [1, 2]; const arr2 = [3, 4]; return [...arr1, ...arr2]", "evalExpect": [1, 2, 3, 4], "safeExpect": [1, 2, 3, 4], "category": "Objects & Arrays" }, { "code": "const a = [1]; const b = [2]; const c = [3]; return [...a, ...b, ...c]", "evalExpect": [1, 2, 3], "safeExpect": [1, 2, 3], "category": "Objects & Arrays" }, { "code": "const data = []; return {data}", "evalExpect": { "data": [] }, "safeExpect": { "data": [] }, "category": "Objects & Arrays" }, { "code": "const x = 1; const y = 2; return {x, y}", "evalExpect": { "x": 1, "y": 2 }, "safeExpect": { "x": 1, "y": 2 }, "category": "Objects & Arrays" }, { "code": "const name = \"Alice\"; const age = 30; return {name, age, city: \"NYC\"}", "evalExpect": { "name": "Alice", "age": 30, "city": "NYC" }, "safeExpect": { "name": "Alice", "age": 30, "city": "NYC" }, "category": "Objects & Arrays" }, { "code": "const a = 1; const obj = {a, b: a + 1}; return obj.a + obj.b", "evalExpect": 3, "safeExpect": 3, "category": "Objects & Arrays" }, { "code": "({[\"a\" + \"b\"]: 1})", "evalExpect": { "ab": 1 }, "safeExpect": { "ab": 1 }, "category": "Objects & Arrays" }, { "code": "let k = \"x\"; return ({[k]: 42})", "evalExpect": { "x": 42 }, "safeExpect": { "x": 42 }, "category": "Objects & Arrays" }, { "code": "({[1 + 2]: \"three\"})", "evalExpect": { "3": "three" }, "safeExpect": { "3": "three" }, "category": "Objects & Arrays" }, { "code": "let n = \"world\"; return ({[`hello_${n}`]: 1})", "evalExpect": { "hello_world": 1 }, "safeExpect": { "hello_world": 1 }, "category": "Objects & Arrays" }, { "code": "function k() { return \"key\" }; return ({[k()]: \"val\"})", "evalExpect": { "key": "val" }, "safeExpect": { "key": "val" }, "category": "Objects & Arrays" }, { "code": "let t = true; return ({[t ? \"a\" : \"b\"]: 1})", "evalExpect": { "a": 1 }, "safeExpect": { "a": 1 }, "category": "Objects & Arrays" }, { "code": "let a = \"x\", b = \"y\"; return ({[a]: 1, [b]: 2})", "evalExpect": { "x": 1, "y": 2 }, "safeExpect": { "x": 1, "y": 2 }, "category": "Objects & Arrays" }, { "code": "({a: 1, [\"b\"]: 2, c: 3})", "evalExpect": { "a": 1, "b": 2, "c": 3 }, "safeExpect": { "a": 1, "b": 2, "c": 3 }, "category": "Objects & Arrays" }, { "code": "({a: 1, [\"a\"]: 2})", "evalExpect": { "a": 2 }, "safeExpect": { "a": 2 }, "category": "Objects & Arrays" }, { "code": "let arr = [\"key\"]; return ({[arr[0]]: \"val\"})", "evalExpect": { "key": "val" }, "safeExpect": { "key": "val" }, "category": "Objects & Arrays" }, { "code": "let c = 0; function k() { c++; return \"a\" } let o = {[k()]: 1}; return c", "evalExpect": 1, "safeExpect": 1, "category": "Objects & Arrays" }, { "code": "({[\"\"]: 1})", "evalExpect": { "": 1 }, "safeExpect": { "": 1 }, "category": "Objects & Arrays" }, { "code": "({[0]: \"zero\"})", "evalExpect": { "0": "zero" }, "safeExpect": { "0": "zero" }, "category": "Objects & Arrays" }, { "code": "({[true]: 1})", "evalExpect": { "true": 1 }, "safeExpect": { "true": 1 }, "category": "Objects & Arrays" }, { "code": "({[null]: 1})", "evalExpect": { "null": 1 }, "safeExpect": { "null": 1 }, "category": "Objects & Arrays" }, { "code": "({[undefined]: 1})", "evalExpect": { "undefined": 1 }, "safeExpect": { "undefined": 1 }, "category": "Objects & Arrays" }, { "code": "let rest = {b: 2}; return ({[\"a\"]: 1, ...rest})", "evalExpect": { "a": 1, "b": 2 }, "safeExpect": { "a": 1, "b": 2 }, "category": "Objects & Arrays" }, { "code": "let rest = {a: 1}; return ({...rest, [\"b\"]: 2})", "evalExpect": { "a": 1, "b": 2 }, "safeExpect": { "a": 1, "b": 2 }, "category": "Objects & Arrays" }, { "code": "let rest = {a: 1}; return ({...rest, [\"a\"]: 2})", "evalExpect": { "a": 2 }, "safeExpect": { "a": 2 }, "category": "Objects & Arrays" }, { "code": "let o = {k: \"deep\"}; return ({[o.k]: 1})", "evalExpect": { "deep": 1 }, "safeExpect": { "deep": 1 }, "category": "Objects & Arrays" }, { "code": "({[(0, \"a\")]: 1})", "evalExpect": { "a": 1 }, "safeExpect": { "a": 1 }, "category": "Objects & Arrays" }, { "code": "let m = \"greet\"; let o = {[m]() { return \"hi\" }}; return o.greet()", "evalExpect": "hi", "safeExpect": "hi", "category": "Objects & Arrays" }, { "code": "let o = {[\"say\"]() { return \"hello\" }}; return o.say()", "evalExpect": "hello", "safeExpect": "hello", "category": "Objects & Arrays" }, { "code": "let o = {[\"a\" + \"b\"]() { return 1 }}; return o.ab()", "evalExpect": 1, "safeExpect": 1, "category": "Objects & Arrays" }, { "code": "let o = {x: 10, [\"getX\"]() { return this.x }}; return o.getX()", "evalExpect": 10, "safeExpect": 10, "category": "Objects & Arrays" }, { "code": "[1,2,3,].length", "evalExpect": 3, "safeExpect": 3, "category": "Objects & Arrays" }, { "code": "Object.keys({a:1,b:2,}).length", "evalExpect": 2, "safeExpect": 2, "category": "Objects & Arrays" }, { "code": "[,].length", "evalExpect": 1, "safeExpect": 1, "category": "Objects & Arrays" }, { "code": "0 in [,]", "evalExpect": false, "safeExpect": false, "category": "Objects & Arrays" }, { "code": "[,,].length", "evalExpect": 2, "safeExpect": 2, "category": "Objects & Arrays" }, { "code": "[1,,].length", "evalExpect": 2, "safeExpect": 2, "category": "Objects & Arrays" }, { "code": "1 in [1,,]", "evalExpect": false, "safeExpect": false, "category": "Objects & Arrays" }, { "code": "[1,,2].length", "evalExpect": 3, "safeExpect": 3, "category": "Objects & Arrays" }, { "code": "1 in [1,,2]", "evalExpect": false, "safeExpect": false, "category": "Objects & Arrays" }, { "code": "[1,,2][2]", "evalExpect": 2, "safeExpect": 2, "category": "Objects & Arrays" }, { "code": "test2 !== '1' && false", "evalExpect": false, "safeExpect": false, "category": "Operator Precedence" }, { "code": "true && true || false", "evalExpect": true, "safeExpect": true, "category": "Operator Precedence" }, { "code": "true || true && false", "evalExpect": true, "safeExpect": true, "category": "Operator Precedence" }, { "code": "1 && 2 == 1", "evalExpect": false, "safeExpect": false, "category": "Operator Precedence" }, { "code": "1 + 2 === 1 + 2 === 1 && 2", "evalExpect": false, "safeExpect": false, "category": "Operator Precedence" }, { "code": "true === 1 && 2", "evalExpect": false, "safeExpect": false, "category": "Operator Precedence" }, { "code": "-1 < 1 && 2 > 1", "evalExpect": true, "safeExpect": true, "category": "Operator Precedence" }, { "code": "!5 > 3", "evalExpect": false, "safeExpect": false, "category": "Operator Precedence" }, { "code": "!5 < 3", "evalExpect": true, "safeExpect": true, "category": "Operator Precedence" }, { "code": "!0 === true", "evalExpect": true, "safeExpect": true, "category": "Operator Precedence" }, { "code": "!false && true", "evalExpect": true, "safeExpect": true, "category": "Operator Precedence" }, { "code": "!true || true", "evalExpect": true, "safeExpect": true, "category": "Operator Precedence" }, { "code": "!false && false || true", "evalExpect": true, "safeExpect": true, "category": "Operator Precedence" }, { "code": "5 > 3 > 1", "evalExpect": false, "safeExpect": false, "category": "Operator Precedence" }, { "code": "1 < 2 < 3", "evalExpect": true, "safeExpect": true, "category": "Operator Precedence" }, { "code": "5 > 3 === true", "evalExpect": true, "safeExpect": true, "category": "Operator Precedence" }, { "code": "1 | 2 && 3", "evalExpect": 3, "safeExpect": 3, "category": "Operator Precedence" }, { "code": "true && 1 | 2", "evalExpect": 3, "safeExpect": 3, "category": "Operator Precedence" }, { "code": "4 & 5 || 0", "evalExpect": 4, "safeExpect": 4, "category": "Operator Precedence" }, { "code": "2 + 3 << 1", "evalExpect": 10, "safeExpect": 10, "category": "Operator Precedence" }, { "code": "8 >> 1 + 1", "evalExpect": 2, "safeExpect": 2, "category": "Operator Precedence" }, { "code": "1 << 2 * 2", "evalExpect": 16, "safeExpect": 16, "category": "Operator Precedence" }, { "code": "5 & 3 | 2", "evalExpect": 3, "safeExpect": 3, "category": "Operator Precedence" }, { "code": "8 | 4 & 2", "evalExpect": 8, "safeExpect": 8, "category": "Operator Precedence" }, { "code": "15 ^ 3 & 7", "evalExpect": 12, "safeExpect": 12, "category": "Operator Precedence" }, { "code": "2 * 3 ** 2", "evalExpect": 18, "safeExpect": 18, "category": "Operator Precedence" }, { "code": "10 / 2 ** 3", "evalExpect": 1.25, "safeExpect": 1.25, "category": "Operator Precedence" }, { "code": "2 + 3 ** 2", "evalExpect": 11, "safeExpect": 11, "category": "Operator Precedence" }, { "code": "2 * 3 * 2 ** 3", "evalExpect": 48, "safeExpect": 48, "category": "Operator Precedence" }, { "code": "-2 ** 2", "evalExpect": "error", "safeExpect": "error", "category": "Operator Precedence" }, { "code": "2 ** 3 ** 2", "evalExpect": 512, "safeExpect": 512, "category": "Operator Precedence" }, { "code": "2 ** 2 ** 3", "evalExpect": 256, "safeExpect": 256, "category": "Operator Precedence" }, { "code": "3 ** 2 ** 2", "evalExpect": 81, "safeExpect": 81, "category": "Operator Precedence" }, { "code": "4 / 2 ** 2 * 3", "evalExpect": 3, "safeExpect": 3, "category": "Operator Precedence" }, { "code": "100 - 10 ** 2", "evalExpect": 0, "safeExpect": 0, "category": "Operator Precedence" }, { "code": "typeof '1'", "evalExpect": "string", "safeExpect": "string", "category": "Other Operators" }, { "code": "typeof z === 'undefined'", "evalExpect": true, "safeExpect": true, "category": "Other Operators" }, { "code": "{} instanceof Object", "evalExpect": "error", "safeExpect": true, "category": "Other Operators" }, { "code": "{} instanceof undefined", "evalExpect": "error", "safeExpect": "error", "category": "Other Operators" }, { "code": "'a' in {a: 1}", "evalExpect": true, "safeExpect": true, "category": "Other Operators" }, { "code": "1,2", "evalExpect": 2, "safeExpect": 2, "category": "Other Operators" }, { "code": "void 2 == '2'", "evalExpect": false, "safeExpect": false, "category": "Other Operators" }, { "code": "void (2 == '2')", "category": "Other Operators" }, { "code": "new Date(0).toISOString()", "evalExpect": "1970-01-01T00:00:00.000Z", "safeExpect": "1970-01-01T00:00:00.000Z", "category": "Other Operators" }, { "code": "function E(a) { this.scope = a.context }; return new E(isNaN).scope?.Function?.name", "category": "Other Operators" }, { "code": "typeof 5 + \"2\"", "evalExpect": "number2", "safeExpect": "number2", "category": "Other Operators" }, { "code": "typeof (5 + 2)", "evalExpect": "number", "safeExpect": "number", "category": "Other Operators" }, { "code": "typeof 5 === \"number\"", "evalExpect": true, "safeExpect": true, "category": "Other Operators" }, { "code": "void 0 === undefined", "evalExpect": true, "safeExpect": true, "category": "Other Operators" }, { "code": "void 5 + 10", "evalExpect": "NaN", "safeExpect": "NaN", "category": "Other Operators" }, { "code": "typeof void 0", "evalExpect": "undefined", "safeExpect": "undefined", "category": "Other Operators" }, { "code": "null?.[0]", "category": "Other Operators" }, { "code": "null?.fn?.()", "category": "Other Operators" }, { "code": "let a = 1; let b = 2; switch(1) {case a: b = 1; case b: return b; default: return 0;}", "evalExpect": 1, "safeExpect": 1, "category": "Switch" }, { "code": "let b = 1; switch(1) {case 1: b = 2; break; case 2: b = 3; default: b = 4}; return b", "evalExpect": 2, "safeExpect": 2, "category": "Switch" }, { "code": "let b = 1; switch(3) {case 1: b = 2; break; case 2: b = 3; default: b = 4}; return b", "evalExpect": 4, "safeExpect": 4, "category": "Switch" }, { "code": "let b = 1; switch(1) {case 1:b = 2; case 2: b = 3; default: b = 4}; return b", "evalExpect": 4, "safeExpect": 4, "category": "Switch" }, { "code": "let a = 1; switch(a) {case 1: return 2}; return 1", "evalExpect": 2, "safeExpect": 2, "category": "Switch" }, { "code": "switch (1) { case 1: { return 42; } default: return 0; }", "evalExpect": 42, "safeExpect": 42, "category": "Switch" }, { "code": "switch (1) { case 1: case 2: return 7; default: return 0; }", "evalExpect": 7, "safeExpect": 7, "category": "Switch" }, { "code": "return eval(\"const value = 'oops\");", "evalExpect": "error", "safeExpect": "/Unclosed '\\''/", "category": "Syntax Errors" }, { "code": "return eval(\"const value = \\\"oops\");", "evalExpect": "error", "safeExpect": "/Unclosed '\"/", "category": "Syntax Errors" }, { "code": "return eval(\"const value = `oops\");", "evalExpect": "error", "safeExpect": "/Unclosed '`'/", "category": "Syntax Errors" }, { "code": "return eval(\"const value = 1; /* open comment\");", "evalExpect": "error", "safeExpect": "/Unclosed comment '\\/\\*'/", "category": "Syntax Errors" }, { "code": "return eval(\"const value =\");", "evalExpect": "error", "safeExpect": "/Unexpected end of expression/", "category": "Syntax Errors" }, { "code": "return eval(\"const value = [1 2];\");", "evalExpect": "error", "safeExpect": "/Unexpected token/", "category": "Syntax Errors" }, { "code": "return eval(\"const value = { a: 1 b: 2 };\");", "evalExpect": "error", "safeExpect": "/Unexpected token/", "category": "Syntax Errors" }, { "code": "return eval(\"fn(, value);\");", "evalExpect": "error", "safeExpect": "/Unexpected end of expression/", "category": "Syntax Errors" }, { "code": "return eval(\"const obj = { [key] };\");", "evalExpect": "error", "safeExpect": "/Unexpected token in computed property/", "category": "Syntax Errors" }, { "code": "return eval(\"return -2 ** 3;\");", "evalExpect": "error", "safeExpect": "/Unary operator used immediately before exponentiation expression/", "category": "Syntax Errors" }, { "code": "return eval(\"value.\");", "evalExpect": "error", "safeExpect": "/Unexpected token after prop: \\./", "category": "Syntax Errors" }, { "code": "return eval(\"const value = for;\");", "evalExpect": "error", "safeExpect": "/Unexpected token 'for'/", "category": "Syntax Errors" }, { "code": "return eval(\"return 1 + ;\");", "evalExpect": "error", "safeExpect": "/Unexpected end of expression/", "category": "Syntax Errors" }, { "code": "return eval(\"return 1,;\");", "evalExpect": "error", "safeExpect": "/Unexpected token/", "category": "Syntax Errors" }, { "code": "return eval(\"return value ? truthy :;\");", "evalExpect": "error", "safeExpect": "/Unexpected end of expression/", "category": "Syntax Errors" }, { "code": "return eval(\"({}).a?.;\");", "evalExpect": "error", "safeExpect": "/Unexpected token/", "category": "Syntax Errors" }, { "code": "return eval(\"({}).a?.[;\");", "evalExpect": "error", "safeExpect": "/Unclosed '\\['/", "category": "Syntax Errors" }, { "code": "return eval(\"a?.b = 1;\");", "evalExpect": "error", "safeExpect": "/Invalid left-hand side in assignment/", "category": "Syntax Errors" }, { "code": "return eval(\"new Map?.();\");", "evalExpect": "error", "safeExpect": "/optional chain|Unexpected/", "category": "Syntax Errors" }, { "code": "return eval(\"a[];\");", "evalExpect": "error", "safeExpect": "/Unexpected end of expression/", "category": "Syntax Errors" }, { "code": "return eval(\"a[;\");", "evalExpect": "error", "safeExpect": "/Unclosed '\\['/", "category": "Syntax Errors" }, { "code": "return eval(\"return (1 + 2;\");", "evalExpect": "error", "safeExpect": "/Unclosed '\\('/", "category": "Syntax Errors" }, { "code": "return eval(\"const value = [1, 2;\");", "evalExpect": "error", "safeExpect": "/Unclosed '\\['/", "category": "Syntax Errors" }, { "code": "return eval(\"const value = { a: 1;\");", "evalExpect": "error", "safeExpect": "/Unclosed '\\{'/", "category": "Syntax Errors" }, { "code": "return eval(\"fn(value;\");", "evalExpect": "error", "safeExpect": "/Unclosed '\\('/", "category": "Syntax Errors" }, { "code": "return eval(\"switch (value) case 1: break;\");", "evalExpect": "error", "safeExpect": "/Invalid switch/", "category": "Syntax Errors" }, { "code": "return eval(\"switch (value) { default: break; default: break; }\");", "evalExpect": "error", "safeExpect": "/Only one default switch case allowed/", "category": "Syntax Errors" }, { "code": "return eval(\"function fn(for) { return for; }\");", "evalExpect": "error", "safeExpect": "/Unexpected token 'for'/", "category": "Syntax Errors" }, { "code": "return eval(\"async function fn(await) {}\");", "evalExpect": "error", "safeExpect": "/Unexpected token 'await'/", "category": "Syntax Errors" }, { "code": "return eval(\"const [value];\");", "evalExpect": "error", "safeExpect": "/Destructuring declaration requires an initializer/", "category": "Syntax Errors" }, { "code": "return eval(\"const [value + 1] = arr;\");", "evalExpect": "error", "safeExpect": "/Invalid destructuring target/", "category": "Syntax Errors" }, { "code": "return eval(\"const { a: value + 1 } = obj;\");", "evalExpect": "error", "safeExpect": "/Invalid destructuring target/", "category": "Syntax Errors" }, { "code": "return eval(\"function fn(...rest, last) { return rest; }\");", "evalExpect": "error", "safeExpect": "/Rest parameter must be last formal parameter/", "category": "Syntax Errors" }, { "code": "return eval(\"(first, ...rest, last) => first\");", "evalExpect": "error", "safeExpect": "/Rest parameter must be last formal parameter/", "category": "Syntax Errors" }, { "code": "return eval(\"for (const [a, b] from pairs) {}\");", "evalExpect": "error", "safeExpect": "/Invalid for loop definition/", "category": "Syntax Errors" }, { "code": "return eval(\"for (let i = 0; i < 2) {}\");", "evalExpect": "error", "safeExpect": "/Invalid for loop definition/", "category": "Syntax Errors" }, { "code": "return eval(\"if (value) else return 1;\");", "evalExpect": "error", "safeExpect": "/Unexpected token/", "category": "Syntax Errors" }, { "code": "return eval(\"while () {}\");", "evalExpect": "error", "safeExpect": "/Unexpected end of expression/", "category": "Syntax Errors" }, { "code": "return eval(\"do { value++; }\");", "evalExpect": "error", "safeExpect": "/Unclosed '\\('/", "category": "Syntax Errors" }, { "code": "return eval(\"try {} catch () {}\");", "evalExpect": "error", "safeExpect": "/Unexpected token '\\)'/", "category": "Syntax Errors" }, { "code": "return eval(\"try { value++; }\");", "evalExpect": "error", "safeExpect": "/Missing catch or finally after try/", "category": "Syntax Errors" }, { "code": "return eval(\"try {} catch (for) {}\");", "evalExpect": "error", "safeExpect": "/Unexpected token 'for'/", "category": "Syntax Errors" }, { "code": "return eval(\"try {} finally\");", "evalExpect": "error", "safeExpect": "/Unexpected token/", "category": "Syntax Errors" }, { "code": "return eval(\"throw ;\");", "evalExpect": "error", "safeExpect": "/Unexpected end of expression/", "category": "Syntax Errors" }, { "code": "return eval(\"new ;\");", "evalExpect": "error", "safeExpect": "/Unexpected end of expression/", "category": "Syntax Errors" }, { "code": "return eval(\"typeof ;\");", "evalExpect": "error", "safeExpect": "/Unexpected end of expression/", "category": "Syntax Errors" }, { "code": "return eval(\"delete ;\");", "evalExpect": "error", "safeExpect": "/Unexpected end of expression/", "category": "Syntax Errors" }, { "code": "return eval(\"void ;\");", "evalExpect": "error", "safeExpect": "/Unexpected end of expression/", "category": "Syntax Errors" }, { "code": "return eval(\"\\\"a\\\" in ;\");", "evalExpect": "error", "safeExpect": "/Unexpected end of expression/", "category": "Syntax Errors" }, { "code": "return eval(\"value instanceof ;\");", "evalExpect": "error", "safeExpect": "/Unexpected end of expression/", "category": "Syntax Errors" }, { "code": "return eval(\"const value = [...];\");", "evalExpect": "error", "safeExpect": "/Unexpected end of expression/", "category": "Syntax Errors" }, { "code": "return eval(\"const value = {...};\");", "evalExpect": "error", "safeExpect": "/Unexpected end of expression/", "category": "Syntax Errors" }, { "code": "return eval(\"fn(...);\");", "evalExpect": "error", "safeExpect": "/Unexpected end of expression/", "category": "Syntax Errors" }, { "code": "return eval(\"while (true) { break 1; }\");", "evalExpect": "error", "safeExpect": "/Unexpected token '1'/", "category": "Syntax Errors" }, { "code": "return eval(\"while (true) { continue 1; }\");", "evalExpect": "error", "safeExpect": "/Unexpected token '1'/", "category": "Syntax Errors" }, { "code": "return eval(\"for (const { a } of) {}\");", "evalExpect": "error", "safeExpect": "/Unexpected end of expression/", "category": "Syntax Errors" }, { "code": "return eval(\"const { ...rest, value } = obj;\");", "evalExpect": "error", "safeExpect": "/Rest element must be last element/", "category": "Syntax Errors" }, { "code": "return eval(\"function fn(...[rest], last) { return last; }\");", "evalExpect": "error", "safeExpect": "/Rest parameter must be last formal parameter/", "category": "Syntax Errors" }, { "code": "return eval(\"yield 1;\");", "evalExpect": "error", "safeExpect": "/Unexpected token/", "category": "Syntax Errors" }, { "code": "return eval(\"function* gen(){ yield* ; }\");", "evalExpect": "error", "safeExpect": "/Unexpected end of expression/", "category": "Syntax Errors" }, { "code": "return eval(\"async function fn(){ await ; }\");", "evalExpect": "error", "safeExpect": "/Unexpected end of expression/", "category": "Syntax Errors" }, { "code": "return eval(\"async function* gen(){ yield* ; }\");", "evalExpect": "error", "safeExpect": "/Unexpected end of expression/", "category": "Syntax Errors" }, { "code": "return eval(\"switch (x) { case: break; }\");", "evalExpect": "error", "safeExpect": "/Unexpected end of expression/", "category": "Syntax Errors" }, { "code": "return eval(\"switch (x) { case 1 break; }\");", "evalExpect": "error", "safeExpect": "/switch|case|Unexpected/", "category": "Syntax Errors" }, { "code": "return eval(\"1++;\");", "evalExpect": "error", "safeExpect": "/Invalid left-hand side expression in postfix operation/", "category": "Syntax Errors" }, { "code": "return eval(\"--1;\");", "evalExpect": "error", "safeExpect": "/left-hand side|Unexpected/", "category": "Syntax Errors" }, { "code": "return eval(\"async function run() { for await (const key in obj) {} }\");", "evalExpect": "error", "safeExpect": "/Unexpected token 'in'/", "category": "Syntax Errors" }, { "code": "return eval(\"async function run(){ for await (const item of) {} }\");", "evalExpect": "error", "safeExpect": "/Unexpected end of expression/", "category": "Syntax Errors" }, { "code": "return eval(\"const re = /(/;\");", "evalExpect": "error", "safeExpect": "/Invalid regular expression/", "category": "Syntax Errors" }, { "code": "return eval(\"const re = /a/zz;\");", "evalExpect": "error", "safeExpect": "/Invalid flags/", "category": "Syntax Errors" }, { "code": "return eval(\"tag`${}`;\");", "evalExpect": "error", "safeExpect": "/Unexpected end of expression/", "category": "Syntax Errors" }, { "code": "return eval(\"tag`${value`;\");", "evalExpect": "error", "safeExpect": "/Unclosed/", "category": "Syntax Errors" }, { "code": "return eval(\"({a:1,,b:2})\");", "evalExpect": "error", "safeExpect": "/Unexpected token ,/", "category": "Syntax Errors" }, { "code": "const a = 1; const b = 2; return `${a} + ${b} = ${a+b}`", "evalExpect": "1 + 2 = 3", "safeExpect": "1 + 2 = 3", "category": "Template Literals" }, { "code": "const name = 'world'; return `hello ${name}`", "evalExpect": "hello world", "safeExpect": "hello world", "category": "Template Literals" }, { "code": "function tag(strings, ...values) { return strings[0] + values[0] + strings[1]; } return tag`hello ${\"world\"}`", "evalExpect": "hello world", "safeExpect": "hello world", "category": "Template Literals" }, { "code": "function tag(strings, ...values) { return values.reduce((acc, val, i) => acc + val + strings[i + 1], strings[0]); } return tag`a${1}b${2}c${3}d`", "evalExpect": "a1b2c3d", "safeExpect": "a1b2c3d", "category": "Template Literals" }, { "code": "const tagging = () => tag; function tag(strings, ...values) { return values.reduce((acc, val, i) => acc + val + strings[i + 1], strings[0]); } return tagging()`a${1}b${2}c${3}d`", "evalExpect": "a1b2c3d", "safeExpect": "a1b2c3d", "category": "Template Literals" }, { "code": "function tag(strings) { return strings[0]; } return tag`static template`", "evalExpect": "static template", "safeExpect": "static template", "category": "Template Literals" }, { "code": "const tag = (strings, ...values) => strings.length; return tag`test`", "evalExpect": 1, "safeExpect": 1, "category": "Template Literals" }, { "code": "const multiply = (strings, a, b) => a * b; return multiply`${2} * ${3}`", "evalExpect": 6, "safeExpect": 6, "category": "Template Literals" }, { "code": "const tag = (s, ...v) => v.length; return tag`${1}${2}${3}`", "evalExpect": 3, "safeExpect": 3, "category": "Template Literals" }, { "code": "const obj = { tag(s, v) { return v * 2; } }; return obj.tag`${5}`", "evalExpect": 10, "safeExpect": 10, "category": "Template Literals" }, { "code": "const tag = s => s.join(\"-\"); return tag`a${\"b\"}c`", "evalExpect": "a-c", "safeExpect": "a-c", "category": "Template Literals" }, { "code": "const inner = \"world\"; return `hello ${`${inner.toUpperCase()}!`}`", "evalExpect": "hello WORLD!", "safeExpect": "hello WORLD!", "category": "Template Literals" }, { "code": "const values = [1, 2]; return `${values.map(v => `${v * 2}`).join(\",\")}`", "evalExpect": "2,4", "safeExpect": "2,4", "category": "Template Literals" } ] ================================================ FILE: test/evalCompletionValue.spec.ts ================================================ import Sandbox from '../src/Sandbox.js'; describe('eval() Completion Value Tests', () => { let sandbox: Sandbox; beforeEach(() => { sandbox = new Sandbox(); }); describe('Declaration statements should return undefined', () => { it('should return undefined for var declaration', () => { const code = 'return eval("var x = 5;");'; const fn = sandbox.compile(code); const result = fn().run(); expect(result).toBeUndefined(); }); it('should return undefined for let declaration', () => { const code = 'return eval("let x = 5;");'; const fn = sandbox.compile(code); const result = fn().run(); expect(result).toBeUndefined(); }); it('should return undefined for const declaration', () => { const code = 'return eval("const x = 5;");'; const fn = sandbox.compile(code); const result = fn().run(); expect(result).toBeUndefined(); }); it('should return undefined for function declaration', () => { const code = 'return eval("function foo() { return 42; }");'; const fn = sandbox.compile(code); const result = fn().run(); expect(result).toBeUndefined(); }); // Note: Class declarations are not supported by the parser yet // it('should return undefined for class declaration', () => { // const code = 'return eval("class Foo {}");'; // const fn = sandbox.compile(code); // const result = fn().run(); // expect(result).toBeUndefined(); // }); }); describe('Expression statements should return the value', () => { it('should return value for numeric expression', () => { const code = 'return eval("5 + 3");'; const fn = sandbox.compile(code); const result = fn().run(); expect(result).toBe(8); }); it('should return value for string expression', () => { const code = 'return eval("\\"hello\\" + \\" world\\"");'; const fn = sandbox.compile(code); const result = fn().run(); expect(result).toBe('hello world'); }); it('should return value for assignment expression', () => { const code = 'return eval("var x; x = 10");'; const fn = sandbox.compile(code); const result = fn().run(); expect(result).toBe(10); }); it('should return value for function call', () => { const code = 'return eval("Math.max(1, 2, 3)");'; const fn = sandbox.compile(code); const scope = { Math }; const result = fn(scope).run(); expect(result).toBe(3); }); it('should return value for object literal', () => { const code = 'return eval("({ a: 1, b: 2 })");'; const fn = sandbox.compile(code); const result = fn().run(); expect(result).toEqual({ a: 1, b: 2 }); }); it('should return value for array literal', () => { const code = 'return eval("[1, 2, 3]");'; const fn = sandbox.compile(code); const result = fn().run(); expect(result).toEqual([1, 2, 3]); }); }); describe('Control flow statements should return undefined', () => { it('should return undefined for if statement', () => { const code = 'return eval("if (true) { 5; }");'; const fn = sandbox.compile(code); const result = fn().run(); expect(result).toBeUndefined(); }); it('should return undefined for for loop', () => { const code = 'return eval("for (let i = 0; i < 3; i++) {}");'; const fn = sandbox.compile(code); const result = fn().run(); expect(result).toBeUndefined(); }); it('should return undefined for while loop', () => { const code = 'return eval("var i = 0; while (i < 3) { i++; }");'; const fn = sandbox.compile(code); const result = fn().run(); expect(result).toBeUndefined(); }); it('should return undefined for switch statement', () => { const code = 'return eval("switch (1) { case 1: break; }");'; const fn = sandbox.compile(code); const result = fn().run(); expect(result).toBeUndefined(); }); it('should return undefined for try-catch', () => { const code = 'return eval("try { 5; } catch (e) {}");'; const fn = sandbox.compile(code); const result = fn().run(); expect(result).toBeUndefined(); }); }); describe('Empty statements should return undefined', () => { it('should return undefined for empty statement', () => { const code = 'return eval(";");'; const fn = sandbox.compile(code); const result = fn().run(); expect(result).toBeUndefined(); }); it('should return undefined for empty code', () => { const code = 'return eval("");'; const fn = sandbox.compile(code); const result = fn().run(); expect(result).toBeUndefined(); }); }); describe('Mixed statements - last statement determines completion value', () => { it('should return value when last is expression after declaration', () => { const code = 'return eval("let x = 5; x + 3");'; const fn = sandbox.compile(code); const result = fn().run(); expect(result).toBe(8); }); it('should return undefined when last is declaration after expression', () => { const code = 'return eval("5 + 3; let x = 10");'; const fn = sandbox.compile(code); const result = fn().run(); expect(result).toBeUndefined(); }); it('should return value from expression after control flow', () => { const code = 'return eval("if (true) {} 42");'; const fn = sandbox.compile(code); const result = fn().run(); expect(result).toBe(42); }); }); describe('Return and throw should work as-is', () => { it('should return value for explicit return', () => { const code = 'return eval("return 42");'; const fn = sandbox.compile(code); const result = fn().run(); expect(result).toBe(42); }); it('should throw for explicit throw', () => { const code = 'return eval("throw new Error(\\"test\\")");'; const fn = sandbox.compile(code); expect(() => fn().run()).toThrow('test'); }); it('should not double-wrap explicit return', () => { const code = 'return eval("return { value: 123 }");'; const fn = sandbox.compile(code); const result = fn().run(); expect(result).toEqual({ value: 123 }); }); }); describe('Should not leak internal Prop objects', () => { it('should not return Prop object for let declaration', () => { const code = 'return eval("let x = 5");'; const fn = sandbox.compile(code); const result = fn().run(); // Should be undefined, not a Prop object expect(result).toBeUndefined(); // Verify it's not an object with internal properties expect(typeof result).not.toBe('object'); }); it('should not return Prop object for const declaration', () => { const code = 'return eval("const x = 5");'; const fn = sandbox.compile(code); const result = fn().run(); expect(result).toBeUndefined(); expect(typeof result).not.toBe('object'); }); it('should not return Prop object for var declaration', () => { const code = 'return eval("var x = 5");'; const fn = sandbox.compile(code); const result = fn().run(); expect(result).toBeUndefined(); expect(typeof result).not.toBe('object'); }); }); describe('Matches native JavaScript eval behavior', () => { it('should match eval() for declarations', () => { const nativeResult = eval('let x = 5; undefined;'); // eval returns undefined for declarations const code = 'return eval("let x = 5");'; const fn = sandbox.compile(code); const sandboxResult = fn().run(); expect(sandboxResult).toBe(nativeResult); }); it('should match eval() for expressions', () => { const nativeResult = eval('5 + 3'); const code = 'return eval("5 + 3");'; const fn = sandbox.compile(code); const sandboxResult = fn().run(); expect(sandboxResult).toBe(nativeResult); }); it('should match eval() for assignment', () => { let x: number; const nativeResult = eval('x = 10'); const code = 'return eval("var x; x = 10");'; const fn = sandbox.compile(code); const sandboxResult = fn().run(); expect(sandboxResult).toBe(nativeResult); }); }); }); ================================================ FILE: test/expression.spec.ts ================================================ import Sandbox from '../src/Sandbox.js'; describe('Expression Compilation Tests', () => { describe('compileExpression - only first statement/expression', () => { it('should only compile first statement, ignoring rest', () => { const sandbox = new Sandbox(); let result = 0; const code = 'result = 1; result = 2'; const fn = sandbox.compileExpression(code); const scope = { result }; fn(scope).run(); // Only first statement executes expect(scope.result).toBe(1); }); it('should only compile first expression with semicolons', () => { const sandbox = new Sandbox(); let a = 0; let b = 0; const code = 'a = 5; b = 10'; const fn = sandbox.compileExpression(code); const scope = { a, b }; fn(scope).run(); expect(scope.a).toBe(5); expect(scope.b).toBe(0); // Second statement not executed }); it('should compile single expression', () => { const sandbox = new Sandbox(); let x = 0; const code = 'x = 42'; const fn = sandbox.compileExpression(code); const scope = { x }; fn(scope).run(); expect(scope.x).toBe(42); }); it('should handle ternary operator', () => { const sandbox = new Sandbox(); let result = ''; const code = 'result = (value > 5 ? "big" : "small")'; const fn = sandbox.compileExpression(code); const scope1 = { result, value: 10 }; const scope2 = { result: '', value: 3 }; fn(scope1).run(); fn(scope2).run(); expect(scope1.result).toBe('big'); expect(scope2.result).toBe('small'); }); it('should execute function call in expression', () => { const sandbox = new Sandbox(); let result = 0; const fn = sandbox.compileExpression('result = callback()'); const scope = { result, callback: () => 99, }; fn(scope).run(); expect(scope.result).toBe(99); }); }); describe('compileExpression - allows functions with statements', () => { it('should allow IIFE with multiple statements', () => { const sandbox = new Sandbox(); let result = 0; const code = 'result = (() => { let a = 1; let b = 2; return a + b; })()'; const fn = sandbox.compileExpression(code); const scope = { result }; fn(scope).run(); expect(scope.result).toBe(3); }); it('should allow arrow function with multiple statements', () => { const sandbox = new Sandbox(); let result = 0; const code = 'result = ((x) => { const y = x * 2; const z = y + 3; return z; })(5)'; const fn = sandbox.compileExpression(code); const scope = { result }; fn(scope).run(); expect(scope.result).toBe(13); }); it('should allow function expression with statements', () => { const sandbox = new Sandbox(); let result = 0; const code = 'result = (function() { let x = 10; let y = 20; return x + y; })()'; const fn = sandbox.compileExpression(code); const scope = { result }; fn(scope).run(); expect(scope.result).toBe(30); }); it('should allow nested functions with loops', () => { const sandbox = new Sandbox(); let result = 0; const code = ` result = ((a) => { const helper = (b) => { let sum = 0; for (let i = 0; i < b; i++) { sum += i; } return sum; }; return helper(a); })(5) `; const fn = sandbox.compileExpression(code); const scope = { result }; fn(scope).run(); expect(scope.result).toBe(10); // 0+1+2+3+4 }); it('should allow functions with if statements', () => { const sandbox = new Sandbox(); let result = ''; const code = ` result = ((x) => { if (x > 10) { return 'greater'; } else { return 'less'; } })(15) `; const fn = sandbox.compileExpression(code); const scope = { result }; fn(scope).run(); expect(scope.result).toBe('greater'); }); it('should allow functions with while loops', () => { const sandbox = new Sandbox(); let result = 0; const code = ` result = (() => { let i = 0; let sum = 0; while (i < 5) { sum += i; i++; } return sum; })() `; const fn = sandbox.compileExpression(code); const scope = { result }; fn(scope).run(); expect(scope.result).toBe(10); // 0+1+2+3+4 }); it('should allow functions with try-catch', () => { const sandbox = new Sandbox(); let result = 0; const code = ` result = (() => { try { let x = 10; return x * 2; } catch (e) { return 0; } })() `; const fn = sandbox.compileExpression(code); const scope = { result }; fn(scope).run(); expect(scope.result).toBe(20); }); it('should allow functions with switch statements', () => { const sandbox = new Sandbox(); let result = ''; const code = ` result = ((x) => { let res; switch (x) { case 1: res = 'one'; break; case 2: res = 'two'; break; default: res = 'other'; } return res; })(2) `; const fn = sandbox.compileExpression(code); const scope = { result }; fn(scope).run(); expect(scope.result).toBe('two'); }); it('should allow functions with variable declarations', () => { const sandbox = new Sandbox(); let result = 0; const code = ` result = (() => { let x = 5; const y = 10; var z = 15; return x + y + z; })() `; const fn = sandbox.compileExpression(code); const scope = { result }; fn(scope).run(); expect(scope.result).toBe(30); }); it('should allow functions with for loops', () => { const sandbox = new Sandbox(); let result = 0; const code = ` result = (() => { let total = 0; for (let i = 1; i <= 5; i++) { total += i; } return total; })() `; const fn = sandbox.compileExpression(code); const scope = { result }; fn(scope).run(); expect(scope.result).toBe(15); // 1+2+3+4+5 }); }); describe('compileExpressionAsync - only first statement/expression', () => { it('should only compile first async statement', async () => { const sandbox = new Sandbox(); let result = 0; const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); const code = 'result = await delay(10).then(() => 1); result = 2'; const fn = sandbox.compileExpressionAsync(code); const scope = { result, delay }; await fn(scope).run(); // Only first statement executes expect(scope.result).toBe(1); }); it('should compile single await expression', async () => { const sandbox = new Sandbox(); let result = 0; const code = 'result = await Promise.resolve(42)'; const fn = sandbox.compileExpressionAsync(code); const scope = { result }; await fn(scope).run(); expect(scope.result).toBe(42); }); }); describe('compileExpressionAsync - allows async functions with statements', () => { it('should allow async IIFE with multiple statements', async () => { const sandbox = new Sandbox(); let result = 0; const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); const code = ` result = await (async () => { let a = 1; await delay(10); let b = 2; return a + b; })() `; const fn = sandbox.compileExpressionAsync(code); const scope = { result, delay }; await fn(scope).run(); expect(scope.result).toBe(3); }); it('should allow async function with for-of loop', async () => { const sandbox = new Sandbox(); let result = 0; const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); const code = ` result = await (async (arr) => { let sum = 0; for (const item of arr) { await delay(5); sum += item; } return sum; })([1, 2, 3, 4, 5]) `; const fn = sandbox.compileExpressionAsync(code); const scope = { result, delay }; await fn(scope).run(); expect(scope.result).toBe(15); }); it('should allow async function with try-catch', async () => { const sandbox = new Sandbox(); let result = 0; const code = ` result = await (async () => { try { const x = await Promise.resolve(10); return x * 2; } catch (e) { return 0; } })() `; const fn = sandbox.compileExpressionAsync(code); const scope = { result }; await fn(scope).run(); expect(scope.result).toBe(20); }); it('should allow Promise.all', async () => { const sandbox = new Sandbox(); let result: number[] = []; const delay = (ms: number, val: number) => new Promise((resolve) => setTimeout(() => resolve(val), ms)); const code = 'result = await Promise.all([delay(10, 1), delay(10, 2), delay(10, 3)])'; const fn = sandbox.compileExpressionAsync(code); const scope = { result, delay }; await fn(scope).run(); expect(scope.result).toEqual([1, 2, 3]); }); it('should allow async arrow function with conditionals', async () => { const sandbox = new Sandbox(); let result = ''; const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); const code = ` result = await (async (x) => { await delay(5); if (x > 10) { return 'big'; } else if (x > 5) { return 'medium'; } else { return 'small'; } })(7) `; const fn = sandbox.compileExpressionAsync(code); const scope = { result, delay }; await fn(scope).run(); expect(scope.result).toBe('medium'); }); }); }); ================================================ FILE: test/parse.spec.ts ================================================ import { ParseError } from '../src/parser'; import Sandbox from '../src/Sandbox'; describe('Sandbox.parse() tests', () => { it('Sandbox.parse()', () => { const code = 'return 1 + 2;'; const ast = Sandbox.parse(code); expect(ast).toBeDefined(); }); it('Sandbox.parse() with syntax error', () => { const code = 'return 1 + ;'; expect(() => { Sandbox.parse(code); }).toThrow(ParseError); }); }); ================================================ FILE: test/performance.mjs ================================================ import { Sandbox } from '../dist/esm/Sandbox.js' import { Bench } from 'tinybench'; import Table from 'cli-table3'; import chalk from 'chalk'; const bench = new Bench({ name: 'simple benchmark', time: 2000 }); const sandbox = new Sandbox(); const code = ` let a = [1]; let count = 1000; while(count--) { a.push(0); } return a; `; const codeComplex = ` function LinkedListNode(value) { this.value = value; this.next = null; } function reverse(head) { let node = head, previous, tmp; while (node) { // save next before we overwrite node.next! tmp = node.next; // reverse pointer node.next = previous; // step forward in the list previous = node; node = tmp; } return previous; } function reverse(head) { if (!head || !head.next) { return head; } let tmp = reverse(head.next); head.next.next = head; head.next = undefined; return tmp; } const root = new LinkedListNode(0); let current = root; for (let i = 1; i < 100; i++) { const node = new LinkedListNode(i); current.next = node; current = node; } reverse(root); `; const ciMultiplier = process.env.CI ? 1.3 : 1; const tests = [ { name: '1k loop', code, max: Math.round(2.950 * ciMultiplier * 1000)/1000 }, { name: 'reverse linked list', code: codeComplex, max: Math.round(1.750 * ciMultiplier * 1000)/1000 } ]; // const exec = sandbox.compile(codeComplex); // exec({}).run(); const passes = (bool) => bool ? chalk.green('PASS') : chalk.red('FAIL'); const tpColor = (max, tp) => { const ratio = max / tp; return ratio < 1 ? chalk.red(tp) : ratio < 1.1 ? chalk.yellow(tp) : ratio < 1.3 ? chalk.green(tp) : chalk.blue(tp); } (async () => { for (const test of tests) { const exec = sandbox.compile(test.code); bench.add(test.name, () => exec({}).run()); // const func = new Function(test.code); // bench.add(test.name, ()=>func()) } const start = performance.now(); await bench.run(); const duration = Math.round((performance.now() - start)/100)/10; const output = bench.results.map((res, i) => { const test = tests[i]; if (res.state !== 'completed') return [bench.tasks[i].name, '-', '-', '-', '-']; const tp = Math.round(res.latency.p50*10000)/10000; const exec = sandbox.compile(test.code)({}); exec.context.ctx.ticks.ticks = 0n; exec.run(); return Object.values({ name: bench.tasks[i].name, latency: tpColor(test.max, tp), maximum: tests[i].max.toString(), ticks: exec.context.ctx.ticks.ticks, passes: passes(test.max > tp) }) }) const table = new Table({ head: ['name', 'latency (ms)', 'fail at (ms)', 'ticks', 'result'], style: { head: [] } }) // console.log(bench.results) table.push(...output); // console.table(bench.table()); console.log(table.toString()); console.log(`ran for ${duration}s`); let fail = false; for (const i in bench.results) { const res = bench.results[i]; const test = tests[i]; if (res.state === 'errored') { console.error(chalk.red(`ERROR: ${test.name} - ${res.error}`)); fail = true; } if (res.state !== 'completed') continue; if (res.latency.p50 > test.max / .99) { console.error(chalk.red(`FAILED: ${test.name} (${(res.latency.p50 / test.max * 100).toFixed(2)}%)`)); fail = true; } } if (fail) process.exit(1); })(); ================================================ FILE: test/sandboxErrorCatch.spec.ts ================================================ import Sandbox from '../src/Sandbox.js'; describe('SandboxError catch behavior', () => { describe('SandboxError is not catchable by sandboxed code', () => { it('sandboxed try/catch cannot catch a SandboxError — it propagates out', () => { const sandbox = new Sandbox({ forbidFunctionCalls: true }); const scope: Record = { catchRan: false }; const fn = sandbox.compile(` try { Math.abs(-1); } catch (e) { catchRan = true; } `); expect(() => fn(scope).run()).toThrow('Function invocations are not allowed'); expect(scope['catchRan']).toBe(false); }); it('sandboxed try/catch cannot catch a function-creation SandboxError', () => { const sandbox = new Sandbox({ forbidFunctionCreation: true }); const scope: Record = { catchRan: false }; const fn = sandbox.compile(` try { (() => 1)(); } catch (e) { catchRan = true; } `); expect(() => fn(scope).run()).toThrow('Function creation is forbidden'); expect(scope['catchRan']).toBe(false); }); it('sandboxed try/catch cannot catch a regex SandboxError', () => { const sandbox = new Sandbox({ globals: {} }); const scope: Record = { catchRan: false }; const fn = sandbox.compile(` try { /test/; } catch (e) { catchRan = true; } `); expect(() => fn(scope).run()).toThrow('Regex not permitted'); expect(scope['catchRan']).toBe(false); }); it('finally does not run when a SandboxError occurs', () => { const sandbox = new Sandbox({ forbidFunctionCreation: true }); const scope: Record = { finallyRan: false }; const fn = sandbox.compile(` try { (() => 1)(); } catch (e) { // should not run } finally { finallyRan = true; } `); expect(() => fn(scope).run()).toThrow('Function creation is forbidden'); expect(scope['finallyRan']).toBe(false); }); it('finally does not run without a catch block when SandboxError occurs', () => { const sandbox = new Sandbox({ forbidFunctionCreation: true }); const scope: Record = { finallyRan: false }; const fn = sandbox.compile(` try { (() => 1)(); } finally { finallyRan = true; } `); expect(() => fn(scope).run()).toThrow('Function creation is forbidden'); expect(scope['finallyRan']).toBe(false); }); it('nested try/catch: inner catch does not intercept SandboxError, outer also cannot', () => { const sandbox = new Sandbox({ forbidFunctionCalls: true }); const scope: Record = { innerCaught: false, outerCaught: false }; const fn = sandbox.compile(` try { try { Math.abs(-1); } catch (inner) { innerCaught = true; } } catch (outer) { outerCaught = true; } `); expect(() => fn(scope).run()).toThrow('Function invocations are not allowed'); expect(scope['innerCaught']).toBe(false); expect(scope['outerCaught']).toBe(false); }); it('non-SandboxError can still be caught normally', () => { const sandbox = new Sandbox(); const scope: Record = { caught: false }; const fn = sandbox.compile(` try { throw new Error('regular error'); } catch (e) { caught = true; } `); fn(scope).run(); expect(scope['caught']).toBe(true); }); it('execution after a caught non-SandboxError continues normally', () => { const sandbox = new Sandbox(); const fn = sandbox.compile(` let x = 0; try { throw new Error('oops'); } catch (e) { x = 1; } return x + 10; `); expect(fn({}).run()).toBe(11); }); }); describe('when haltOnSandboxError is true', () => { it('halt fires when SandboxError escapes (no try/catch)', () => { const sandbox = new Sandbox({ forbidFunctionCalls: true, haltOnSandboxError: true }); let haltFired = false; let haltError: Error | undefined; sandbox.subscribeHalt((args) => { haltFired = true; haltError = args?.error; }); sandbox.compile('Math.abs(-1)')({}).run(); expect(haltFired).toBe(true); expect(haltError?.message).toBe('Function invocations are not allowed'); }); it('halt fires when SandboxError escapes a try/catch (catch does not run)', () => { const sandbox = new Sandbox({ forbidFunctionCalls: true, haltOnSandboxError: true }); let haltFired = false; const scope: Record = { catchRan: false }; sandbox.subscribeHalt(() => { haltFired = true; }); const fn = sandbox.compile(` try { Math.abs(-1); } catch (e) { catchRan = true; } `); fn(scope).run(); expect(haltFired).toBe(true); expect(scope['catchRan']).toBe(false); }); it('halt fires when SandboxError occurs inside a try/finally', () => { const sandbox = new Sandbox({ forbidFunctionCreation: true, haltOnSandboxError: true }); let haltFired = false; sandbox.subscribeHalt(() => { haltFired = true; }); sandbox .compile( ` try { (() => 1)(); } finally { // finally does not prevent halt } `, )({}) .run(); expect(haltFired).toBe(true); }); it('non-SandboxError can still be caught — halt does not fire', () => { const sandbox = new Sandbox({ haltOnSandboxError: true }); let haltFired = false; const scope: Record = { caught: false }; sandbox.subscribeHalt(() => { haltFired = true; }); sandbox .compile( ` try { throw new Error('regular error'); } catch (e) { caught = true; } `, )(scope) .run(); expect(haltFired).toBe(false); expect(scope['caught']).toBe(true); }); }); }); ================================================ FILE: test/sandboxRestrictions.spec.ts ================================================ import Sandbox from '../src/Sandbox.js'; describe('Executor Edge Cases', () => { describe('Function creation restrictions', () => { it('should throw when function creation is forbidden', () => { const sandbox = new Sandbox({ forbidFunctionCreation: true }); const fn = sandbox.compile('(function() { return 1; })()'); expect(() => { fn({}).run(); }).toThrow('Function creation is forbidden'); }); it('should throw when arrow function creation is forbidden', () => { const sandbox = new Sandbox({ forbidFunctionCreation: true }); const fn = sandbox.compile('(() => 1)()'); expect(() => { fn({}).run(); }).toThrow('Function creation is forbidden'); }); it('should throw when async function creation is forbidden', () => { const sandbox = new Sandbox({ forbidFunctionCreation: true }); const fn = sandbox.compile('(async function() { return 1; })'); expect(() => { fn({}).run(); }).toThrow('Function creation is forbidden'); }); it('should throw when async arrow function creation is forbidden', () => { const sandbox = new Sandbox({ forbidFunctionCreation: true }); const fn = sandbox.compile('(async () => 1)'); expect(() => { fn({}).run(); }).toThrow('Function creation is forbidden'); }); it('should throw when generator creation is forbidden', () => { const sandbox = new Sandbox({ forbidFunctionCreation: true }); const fn = sandbox.compile('(function* () { yield 1; })().next().value'); expect(() => { fn({}).run(); }).toThrow('Function creation is forbidden'); }); it('should throw when async generator creation is forbidden', () => { const sandbox = new Sandbox({ forbidFunctionCreation: true }); const fn = sandbox.compile('(async function* () { yield 1; })()'); expect(() => { fn({}).run(); }).toThrow('Function creation is forbidden'); }); it('should throw when async generators are not permitted', () => { const sandbox = new Sandbox({ prototypeWhitelist: new Map() }); const fn = sandbox.compile('(async function* () { yield 1; })()'); expect(() => { fn({}).run(); }).toThrow('Async/await not permitted'); }); }); describe('Function call restrictions', () => { it('should throw when function calls are forbidden', () => { const sandbox = new Sandbox({ forbidFunctionCalls: true }); const fn = sandbox.compile('Math.abs(-1)'); expect(() => { fn({}).run(); }).toThrow('Function invocations are not allowed'); }); }); describe('Regex restrictions', () => { it('should throw when regex is not permitted', () => { const sandbox = new Sandbox({ globals: {} }); const fn = sandbox.compile('/test/'); expect(() => { fn({}).run(); }).toThrow('Regex not permitted'); }); }); describe('haltOnSandboxError option', () => { it('should halt execution when SandboxError is thrown and haltOnSandboxError is true', () => { const sandbox = new Sandbox({ forbidFunctionCalls: true, haltOnSandboxError: true, }); let haltCalled = false; let haltError: Error | undefined; sandbox.subscribeHalt((args) => { haltCalled = true; haltError = args?.error; }); const fn = sandbox.compile('Math.abs(-1)'); const { run } = fn({}); // Should not throw immediately, but halt instead run(); expect(haltCalled).toBe(true); expect(haltError).toBeDefined(); expect(haltError?.message).toBe('Function invocations are not allowed'); }); it('should throw immediately when SandboxError is thrown and haltOnSandboxError is false', () => { const sandbox = new Sandbox({ forbidFunctionCalls: true, haltOnSandboxError: false, }); let haltCalled = false; sandbox.subscribeHalt(() => { haltCalled = true; }); const fn = sandbox.compile('Math.abs(-1)'); expect(() => { fn({}).run(); }).toThrow('Function invocations are not allowed'); expect(haltCalled).toBe(false); }); it('should throw immediately when SandboxError is thrown and haltOnSandboxError is undefined', () => { const sandbox = new Sandbox({ forbidFunctionCalls: true }); const fn = sandbox.compile('Math.abs(-1)'); expect(() => { fn({}).run(); }).toThrow('Function invocations are not allowed'); }); it('should resume and propagate error after halt', (done) => { const sandbox = new Sandbox({ forbidFunctionCalls: true, haltOnSandboxError: true, }); let haltCalled = false; let errorReceived: Error | undefined; sandbox.subscribeHalt((args) => { haltCalled = true; errorReceived = args?.error; // Resume after a short delay setTimeout(() => { sandbox.resumeExecution(); }, 50); }); const fn = sandbox.compile('Math.abs(-1)'); const { run } = fn({}); // Run - execution will halt run(); // Wait for resume to happen setTimeout(() => { expect(haltCalled).toBe(true); expect(errorReceived).toBeDefined(); expect(errorReceived?.message).toBe('Function invocations are not allowed'); done(); }, 100); }); it('should halt on function creation error when haltOnSandboxError is true', () => { const sandbox = new Sandbox({ forbidFunctionCreation: true, haltOnSandboxError: true, }); let haltCalled = false; let haltError: Error | undefined; sandbox.subscribeHalt((args) => { haltCalled = true; haltError = args?.error; }); const fn = sandbox.compile('(() => 1)()'); const { run } = fn({}); run(); expect(haltCalled).toBe(true); expect(haltError).toBeDefined(); expect(haltError?.message).toBe('Function creation is forbidden'); }); it('should halt on regex error when haltOnSandboxError is true', () => { const sandbox = new Sandbox({ globals: {}, haltOnSandboxError: true, }); let haltCalled = false; let haltError: Error | undefined; sandbox.subscribeHalt((args) => { haltCalled = true; haltError = args?.error; }); const fn = sandbox.compile('/test/'); const { run } = fn({}); run(); expect(haltCalled).toBe(true); expect(haltError).toBeDefined(); expect(haltError?.message).toBe('Regex not permitted'); }); it('should not halt on non-SandboxError exceptions', () => { const sandbox = new Sandbox({ haltOnSandboxError: true }); let haltCalled = false; sandbox.subscribeHalt(() => { haltCalled = true; }); const fn = sandbox.compile('throw new Error("Regular error")'); expect(() => { fn({}).run(); }).toThrow('Regular error'); expect(haltCalled).toBe(false); }); it('should provide context information when halting', () => { const sandbox = new Sandbox({ forbidFunctionCalls: true, haltOnSandboxError: true, }); let haltContext: any; sandbox.subscribeHalt((args) => { haltContext = args; }); const fn = sandbox.compile('Math.abs(-1)'); const { run } = fn({}); run(); expect(haltContext).toBeDefined(); expect(haltContext.error).toBeDefined(); expect(haltContext.ticks).toBeDefined(); expect(haltContext.scope).toBeDefined(); expect(haltContext.context).toBeDefined(); }); }); describe('maxParserRecursionDepth option', () => { it('should not throw with default maxParserRecursionDepth and 256 nesting', () => { const sandbox = new Sandbox(); const deeplyNested = 'return' + '('.repeat(256) + '1' + ')'.repeat(256); const fn = sandbox.compile(deeplyNested); expect(fn({}).run()).toBe(1); }); it('should throw when expression nesting exceeds maxParserRecursionDepth', () => { const sandbox = new Sandbox({ maxParserRecursionDepth: 5 }); const deeplyNested = '('.repeat(10) + '1' + ')'.repeat(10); expect(() => { sandbox.compile(deeplyNested); }).toThrow('Maximum expression depth exceeded'); }); it('should not throw when expression nesting is within maxParserRecursionDepth', () => { const sandbox = new Sandbox({ maxParserRecursionDepth: 20 }); const nested = 'return (((1 + 2)))'; const fn = sandbox.compile(nested); expect(fn({}).run()).toBe(3); }); it('should throw on deeply nested template literals when depth is low', () => { const sandbox = new Sandbox({ maxParserRecursionDepth: 2 }); expect(() => { sandbox.compile('`${ `${ `${ 1 }` }` }`'); }).toThrow('Maximum expression depth exceeded'); }); it('should parse deeply nested template literals within depth limit', () => { const sandbox = new Sandbox({ maxParserRecursionDepth: 4 }); const fn = sandbox.compile('return `${ `${ 1 }` }`'); expect(fn({}).run()).toBe('1'); }); it('should apply depth limit when using compileExpression', () => { const sandbox = new Sandbox({ maxParserRecursionDepth: 5 }); const deeplyNested = '('.repeat(10) + '1' + ')'.repeat(10); expect(() => { sandbox.compileExpression(deeplyNested); }).toThrow('Maximum expression depth exceeded'); }); it('should apply depth limit when using compileExpressionAsync', () => { const sandbox = new Sandbox({ maxParserRecursionDepth: 5 }); const deeplyNested = '('.repeat(10) + '1' + ')'.repeat(10); expect(() => { sandbox.compileExpressionAsync(deeplyNested); }).toThrow('Maximum expression depth exceeded'); }); it('should apply depth limit when using compileAsync', () => { const sandbox = new Sandbox({ maxParserRecursionDepth: 5 }); const deeplyNested = '('.repeat(10) + '1' + ')'.repeat(10); expect(() => { sandbox.compileAsync(deeplyNested); }).toThrow('Maximum expression depth exceeded'); }); it('should apply depth limit when calling Function constructor', () => { const sandbox = new Sandbox({ maxParserRecursionDepth: 5 }); expect(() => { sandbox .compile('new Function("return " + "(".repeat(10) + "1" + ")".repeat(10))()')({}) .run(); }).toThrow('Maximum expression depth exceeded'); }); it('should throw on deeply nested ternary expressions', () => { const sandbox = new Sandbox({ maxParserRecursionDepth: 5 }); const deepTernary = 'true ? '.repeat(10) + '1' + ' : 0'.repeat(10); expect(() => { sandbox.compile(deepTernary); }).toThrow('Maximum expression depth exceeded'); }); it('should not throw on ternary within depth limit', () => { const sandbox = new Sandbox(); const fn = sandbox.compile('return true ? true ? 1 : 2 : 3'); expect(fn({}).run()).toBe(1); }); it('should throw on deeply chained right-associative assignments', () => { const sandbox = new Sandbox({ maxParserRecursionDepth: 5 }); const deepAssign = 'let x; ' + 'x = '.repeat(10) + '1'; expect(() => { sandbox.compile(deepAssign); }).toThrow('Maximum expression depth exceeded'); }); it('should not throw on assignment chain within depth limit', () => { const sandbox = new Sandbox(); const fn = sandbox.compile('let a, b, c; a = b = c = 5; return a'); expect(fn({}).run()).toBe(5); }); it('should throw on deeply chained else-if', () => { const sandbox = new Sandbox({ maxParserRecursionDepth: 5 }); let code = 'if (false) { 1 }'; for (let i = 0; i < 10; i++) code += ' else if (false) { ' + i + ' }'; code += ' else { 42 }'; expect(() => { sandbox.compile(code); }).toThrow('Maximum expression depth exceeded'); }); it('should not throw on else-if chain within depth limit', () => { const sandbox = new Sandbox(); const fn = sandbox.compile( 'if (false) { return 1 } else if (false) { return 2 } else { return 3 }', ); expect(fn({}).run()).toBe(3); }); }); }); ================================================ FILE: test/semicolonInsertion.spec.ts ================================================ import Sandbox from '../src/Sandbox.js'; describe('Semicolon Insertion Tests', () => { beforeEach(() => {}); it('should handle one-line if statement without braces', () => { const logs: any[] = []; const mockConsole = { log: (...args: any[]) => logs.push(...args) }; const sandbox = new Sandbox({ globals: { console: mockConsole, }, }); const script = ` console.log(1) if (false) throw new Error('test') console.log(2) console.log(3) `; const jsExecutor = sandbox.compile(script); jsExecutor({}).run(); expect(logs).toEqual([1, 2, 3]); }); it('should handle do-while loop correctly', () => { const logs: any[] = []; const mockConsole = { log: (...args: any[]) => logs.push(...args) }; const sandbox = new Sandbox({ globals: { console: mockConsole, }, }); const script = ` console.log(1) do { console.log(2) break } while (true) console.log(3) console.log(4) `; const jsExecutor = sandbox.compile(script); jsExecutor({}).run(); expect(logs).toEqual([1, 2, 3, 4]); }); it('should handle one-line while statement without braces', () => { const script = ` console.log(1) let x = 0 while (x < 1) x++ console.log(2) console.log(3) `; const logs: any[] = []; const mockConsole = { log: (...args: any[]) => logs.push(...args) }; const sandbox = new Sandbox({ globals: { console: mockConsole, }, }); const jsExecutor = sandbox.compile(script); jsExecutor({}).run(); expect(logs).toEqual([1, 2, 3]); }); it('should handle one-line for statement without braces', () => { const script = ` console.log(1) for (let i = 0; i < 1; i++) console.log('loop') console.log(2) console.log(3) `; const logs: any[] = []; const mockConsole = { log: (...args: any[]) => logs.push(...args) }; const sandbox = new Sandbox({ globals: { console: mockConsole, }, }); const jsExecutor = sandbox.compile(script); jsExecutor({}).run(); expect(logs).toEqual([1, 'loop', 2, 3]); }); it('should handle nested if statements without braces', () => { const logs: any[] = []; const mockConsole = { log: (...args: any[]) => logs.push(...args) }; const sandbox = new Sandbox({ globals: { console: mockConsole, }, }); const script = ` console.log(1) if (true) if (true) console.log(2) console.log(3) if (false) console.log('skip') console.log(4) `; const jsExecutor = sandbox.compile(script); jsExecutor({}).run(); expect(logs).toEqual([1, 2, 3, 4]); }); it('should handle functions with control flow statements', () => { const logs: any[] = []; const mockConsole = { log: (...args: any[]) => logs.push(...args) }; const sandbox = new Sandbox({ globals: { console: mockConsole, }, }); const script = ` console.log(1) function test() { if (true) console.log(2) console.log(3) for (let i = 0; i < 2; i++) console.log(4) console.log(5) } test() console.log(6) `; const jsExecutor = sandbox.compile(script); jsExecutor({}).run(); expect(logs).toEqual([1, 2, 3, 4, 4, 5, 6]); }); it('should handle switch with control flow in cases', () => { const logs: any[] = []; const mockConsole = { log: (...args: any[]) => logs.push(...args) }; const sandbox = new Sandbox({ globals: { console: mockConsole, }, }); const script = ` console.log(1) let x = 2 switch (x) { case 1: if (true) console.log('a') console.log('b') break case 2: console.log(2) if (true) console.log(3) console.log(4) break } console.log(5) `; const jsExecutor = sandbox.compile(script); jsExecutor({}).run(); expect(logs).toEqual([1, 2, 3, 4, 5]); }); it('should handle multiple loops with single-line bodies', () => { const logs: any[] = []; const mockConsole = { log: (...args: any[]) => logs.push(...args) }; const sandbox = new Sandbox({ globals: { console: mockConsole, }, }); const script = ` console.log(1) for (let i = 0; i < 2; i++) console.log(2) console.log(3) let j = 0 while (j < 2) j++ console.log(4) do { console.log(5) break } while (true) console.log(6) `; const jsExecutor = sandbox.compile(script); jsExecutor({}).run(); expect(logs).toEqual([1, 2, 2, 3, 4, 5, 6]); }); it('should handle complex nested structures', () => { const logs: any[] = []; const mockConsole = { log: (...args: any[]) => logs.push(...args) }; const sandbox = new Sandbox({ globals: { console: mockConsole, }, }); const script = ` console.log(1) if (true) { console.log(2) for (let i = 0; i < 2; i++) { if (i === 1) console.log(3) } console.log(4) } console.log(5) function nested() { if (false) return console.log(6) do { console.log(7) break } while (true) console.log(8) } nested() console.log(9) `; const jsExecutor = sandbox.compile(script); jsExecutor({}).run(); expect(logs).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]); }); it('should handle if-else chains without braces', () => { const logs: any[] = []; const mockConsole = { log: (...args: any[]) => logs.push(...args) }; const sandbox = new Sandbox({ globals: { console: mockConsole, }, }); const script = ` console.log(1) let x = 2 if (x === 1) console.log('a') else if (x === 2) console.log(2) else console.log('c') console.log(3) if (x === 3) console.log('d') else if (x === 4) console.log('e') console.log(4) `; const jsExecutor = sandbox.compile(script); jsExecutor({}).run(); expect(logs).toEqual([1, 2, 3, 4]); }); it('should handle try-catch with control flow', () => { const logs: any[] = []; const mockConsole = { log: (...args: any[]) => logs.push(...args) }; const sandbox = new Sandbox({ globals: { console: mockConsole, }, }); const script = ` console.log(1) try { console.log(2) if (false) throw new Error('test') console.log(3) } catch (e) { console.log('error') } console.log(4) try { if (true) throw new Error('test2') console.log('skip') } catch (e) { console.log(5) } console.log(6) `; const jsExecutor = sandbox.compile(script); jsExecutor({}).run(); expect(logs).toEqual([1, 2, 3, 4, 5, 6]); }); it('should handle arrow functions with control flow', () => { const logs: any[] = []; const mockConsole = { log: (...args: any[]) => logs.push(...args) }; const sandbox = new Sandbox({ globals: { console: mockConsole, }, }); const script = ` console.log(1) const fn = () => { if (true) console.log(2) console.log(3) } fn() console.log(4) const arr = [1, 2] arr.forEach(x => { if (x === 2) console.log(5) }) console.log(6) `; const jsExecutor = sandbox.compile(script); jsExecutor({}).run(); expect(logs).toEqual([1, 2, 3, 4, 5, 6]); }); it('should handle deeply nested control structures', () => { const logs: any[] = []; const mockConsole = { log: (...args: any[]) => logs.push(...args) }; const sandbox = new Sandbox({ globals: { console: mockConsole, }, }); const script = ` console.log(1) for (let i = 0; i < 2; i++) { if (i === 0) { console.log(2) while (false) console.log('skip') console.log(3) } else { for (let j = 0; j < 2; j++) { if (j === 1) console.log(4) } console.log(5) } } console.log(6) do { if (true) { console.log(7) if (false) console.log('skip') console.log(8) } break } while (true) console.log(9) `; const jsExecutor = sandbox.compile(script); jsExecutor({}).run(); expect(logs).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]); }); test('should handle multiple do-while loops in sequence', () => { const code = ` let x = 0 do { x++ } while (x < 3) do { x++ } while (x < 6) return x `; const sandbox = new Sandbox(); const result = sandbox.compile(code); expect(result({}).run()).toBe(6); }); test('should handle nested do-while loops', () => { const code = ` let x = 0, y = 0 do { y = 0; do { y++ } while (y < 2) x++ } while (x < 3) return x + y `; const sandbox = new Sandbox(); const result = sandbox.compile(code); expect(result({}).run()).toBe(5); }); test('should handle do-while with complex preceding code', () => { const code = ` function test() { let x = 0 if (true) { x = 5 } return x } let y = 0; do { y++ } while (y < 2) return test() + y `; const sandbox = new Sandbox(); const result = sandbox.compile(code); expect(result({}).run()).toBe(7); }); test('should handle do-while inside if without braces', () => { const code = ` let count = 0 if (true) do { count++ } while (count < 3) return count `; const sandbox = new Sandbox(); const result = sandbox.compile(code); expect(result({}).run()).toBe(3); }); test('should handle multiple control structures with mixed brace styles', () => { const code = ` let result = 0 if (true) { result = 1 } if (result > 0) result = 2 else result = 0 while (result < 5) result++ do { result++ } while (result < 10) return result `; const sandbox = new Sandbox(); const result = sandbox.compile(code); expect(result({}).run()).toBe(10); }); test('should handle large script with functions and callbacks', () => { const code = ` // Utility functions at the top function add(a, b) { return a + b } function multiply(x, y) { let result = 0 let count = 0 do { result += x count++ } while (count < y) return result } // Main processing function with nested structures function processData(data) { let total = 0 let index = 0 // Nested callback const callback = function(item) { if (item > 0) return item * 2 else return 0 } // Do-while inside function do { if (data[index]) { total += callback(data[index]) } index++ } while (index < data.length) return total } // Another function with complex control flow function calculate(n) { let sum = 0 let i = 0 while (i < n) { if (i % 2 === 0) sum += i else sum += i * 2 i++ } // Do-while at the end do { sum++ } while (sum < 20) return sum } // Nested closures function outer(x) { return function inner(y) { let result = x + y if (result > 10) result = 10 else if (result > 5) result = 5 else result = 0 return result } } // Execute everything const data = [1, 2, 3, 4, 5] let finalResult = 0 finalResult += processData(data) finalResult += calculate(5) finalResult += outer(3)(4) finalResult += multiply(3, 4) return finalResult `; const sandbox = new Sandbox(); const result = sandbox.compile(code); // processData: (1*2 + 2*2 + 3*2 + 4*2 + 5*2) = 30 // calculate: (0 + 2 + 4 + 2*1 + 2*3) = 14, then incremented to 20 // outer(3)(4): 7 > 5, so returns 5 // multiply(3, 4): 12 // Total: 30 + 20 + 5 + 12 = 67 expect(result({}).run()).toBe(67); }); test('should handle deeply nested callbacks with do-while', () => { const code = ` function outer() { let x = 0 const middle = function() { let y = 0 const inner = function() { let z = 0 do { z++ } while (z < 2) return z } do { y += inner() } while (y < 5) return y } do { x += middle() } while (x < 10) return x } return outer() `; const sandbox = new Sandbox(); const result = sandbox.compile(code); expect(result({}).run()).toBe(12); }); test('should handle mixed control structures across function boundaries', () => { const code = ` let globalCount = 0 // Top-level if-else if (globalCount < 10) globalCount = 10 // Function declaration after control structure function looper() { return 3 } return globalCount + looper() `; const sandbox = new Sandbox(); const result = sandbox.compile(code); expect(result({}).run()).toBe(13); }); }); ================================================ FILE: test/subscriptions.spec.ts ================================================ import Sandbox from '../src/Sandbox.js'; describe('Subscription Tests', () => { describe('subscribeGet', () => { it('should track property access', () => { const sandbox = new Sandbox(); const accesses: Array<{ obj: any; name: string }> = []; const code = ` const value = obj.property; return value; `; const fn = sandbox.compile(code); const scope = { obj: { property: 42 } }; const { context, run } = fn(scope); sandbox.subscribeGet((obj, name) => { accesses.push({ obj, name }); }, context); const result = run(); expect(result).toBe(42); expect(accesses.length).toBeGreaterThan(0); expect(accesses.some((a) => a.name === 'property')).toBe(true); }); it('should track multiple property accesses', () => { const sandbox = new Sandbox(); const accesses: string[] = []; const code = ` const a = obj.x; const b = obj.y; const c = obj.z; return a + b + c; `; const fn = sandbox.compile(code); const scope = { obj: { x: 1, y: 2, z: 3 } }; const { context, run } = fn(scope); sandbox.subscribeGet((obj, name) => { if (typeof name === 'string') { accesses.push(name); } }, context); const result = run(); expect(result).toBe(6); expect(accesses.filter((a) => a === 'x').length).toBeGreaterThan(0); expect(accesses.filter((a) => a === 'y').length).toBeGreaterThan(0); expect(accesses.filter((a) => a === 'z').length).toBeGreaterThan(0); }); it('should allow unsubscribing from get events', () => { const sandbox = new Sandbox(); let accessCount = 0; const code = ` return obj.value; `; const fn = sandbox.compile(code); const scope = { obj: { value: 10 } }; const { context, run } = fn(scope); const subscription = sandbox.subscribeGet((obj, name) => { if (name === 'value') accessCount++; }, context); run(); const countAfterFirstRun = accessCount; subscription.unsubscribe(); // Run again in same context run(); expect(countAfterFirstRun).toBeGreaterThan(0); expect(accessCount).toBe(countAfterFirstRun); // Should not increase after unsubscribe }); it('should track nested property access', () => { const sandbox = new Sandbox(); const accesses: string[] = []; const code = ` return obj.nested.deep.value; `; const fn = sandbox.compile(code); const scope = { obj: { nested: { deep: { value: 99 } } } }; const { context, run } = fn(scope); sandbox.subscribeGet((obj, name) => { if (typeof name === 'string') { accesses.push(name); } }, context); const result = run(); expect(result).toBe(99); expect(accesses).toContain('nested'); expect(accesses).toContain('deep'); expect(accesses).toContain('value'); }); it('should track nested property access performed by JSON.stringify', () => { const sandbox = new Sandbox(); const accesses: string[] = []; const scope = { payload: { top: 1, nested: { inner: 2, }, }, }; const fn = sandbox.compile('return JSON.stringify(payload)'); const { context, run } = fn(scope); sandbox.subscribeGet((obj, name) => { if (typeof name === 'string') { accesses.push(name); } }, context); const result = run(); expect(result).toBe('{"top":1,"nested":{"inner":2}}'); expect(accesses).toContain('top'); expect(accesses).toContain('nested'); expect(accesses).toContain('inner'); }); }); describe('subscribeSet', () => { it('should track property modification', () => { const sandbox = new Sandbox(); const modifications: string[] = []; const code = ` obj.property = 100; return obj.property; `; const fn = sandbox.compile(code); const scope = { obj: { property: 42 } }; const { context, run } = fn(scope); sandbox.subscribeSet( scope.obj, 'property', (change) => { modifications.push(change.type); }, context, ); const result = run(); expect(result).toBe(100); expect(scope.obj.property).toBe(100); expect(modifications.length).toBe(1); expect(modifications[0]).toBe('replace'); }); it('should track multiple modifications to same property', () => { const sandbox = new Sandbox(); const modifications: string[] = []; const code = ` obj.value = 1; obj.value = 2; obj.value = 3; return obj.value; `; const fn = sandbox.compile(code); const scope = { obj: { value: 0 } }; const { context, run } = fn(scope); sandbox.subscribeSet( scope.obj, 'value', (change) => { modifications.push(change.type); }, context, ); run(); expect(modifications).toEqual(['replace', 'replace', 'replace']); }); it('should only track specified property', () => { const sandbox = new Sandbox(); let xModifications = 0; let yModifications = 0; const code = ` obj.x = 10; obj.y = 20; obj.x = 30; `; const fn = sandbox.compile(code); const scope = { obj: { x: 0, y: 0 } }; const { context, run } = fn(scope); sandbox.subscribeSet( scope.obj, 'x', () => { xModifications++; }, context, ); sandbox.subscribeSet( scope.obj, 'y', () => { yModifications++; }, context, ); run(); expect(xModifications).toBe(2); expect(yModifications).toBe(1); }); it('should allow unsubscribing from set events', () => { const sandbox = new Sandbox(); let modificationCount = 0; const code = ` obj.value = 100; `; const fn = sandbox.compile(code); const scope = { obj: { value: 0 } }; const { context, run } = fn(scope); const subscription = sandbox.subscribeSet( scope.obj, 'value', () => { modificationCount++; }, context, ); run(); expect(modificationCount).toBe(1); subscription.unsubscribe(); const { context: context2, run: run2 } = fn(scope); run2(); expect(modificationCount).toBe(1); // Should not increase after unsubscribe }); it('should track changes to nested objects', () => { const sandbox = new Sandbox(); let modificationCount = 0; const code = ` obj.nested.value = 42; return obj.nested.value; `; const fn = sandbox.compile(code); const scope = { obj: { nested: { value: 0 } } }; const { context, run } = fn(scope); sandbox.subscribeSet( scope.obj.nested, 'value', (change) => { modificationCount++; }, context, ); run(); expect(modificationCount).toBe(1); expect(scope.obj.nested.value).toBe(42); }); it('should handle increment operations', () => { const sandbox = new Sandbox(); let modificationCount = 0; const code = ` obj.counter++; obj.counter++; obj.counter++; return obj.counter; `; const fn = sandbox.compile(code); const scope = { obj: { counter: 0 } }; const { context, run } = fn(scope); sandbox.subscribeSet( scope.obj, 'counter', (change) => { modificationCount++; }, context, ); const result = run(); expect(result).toBe(3); expect(modificationCount).toBe(3); }); it('should handle compound assignments', () => { const sandbox = new Sandbox(); let modificationCount = 0; const code = ` obj.value += 10; obj.value *= 2; obj.value -= 5; return obj.value; `; const fn = sandbox.compile(code); const scope = { obj: { value: 5 } }; const { context, run } = fn(scope); sandbox.subscribeSet( scope.obj, 'value', (change) => { modificationCount++; }, context, ); run(); expect(scope.obj.value).toBe(25); expect(modificationCount).toBe(3); }); }); describe('subscribeSetGlobal', () => { it('should track global property modifications', () => { const sandbox = new Sandbox(); let modificationCount = 0; const code = ` obj.value = 999; `; const fn = sandbox.compile(code); const scope = { obj: { value: 0 } }; sandbox.subscribeSetGlobal(scope.obj, 'value', (change) => { modificationCount++; }); fn(scope).run(); expect(modificationCount).toBe(1); expect(scope.obj.value).toBe(999); }); it('should persist across multiple executions', () => { const sandbox = new Sandbox(); let modificationCount = 0; const code = ` obj.value++; `; const fn = sandbox.compile(code); const scope = { obj: { value: 0 } }; sandbox.subscribeSetGlobal(scope.obj, 'value', () => { modificationCount++; }); fn(scope).run(); fn(scope).run(); fn(scope).run(); expect(modificationCount).toBe(3); }); it('should allow unsubscribing from global set events', () => { const sandbox = new Sandbox(); let modificationCount = 0; const code = ` obj.value = 5; `; const fn = sandbox.compile(code); const scope = { obj: { value: 0 } }; const subscription = sandbox.subscribeSetGlobal(scope.obj, 'value', () => { modificationCount++; }); fn(scope).run(); expect(modificationCount).toBe(1); subscription.unsubscribe(); fn(scope).run(); expect(modificationCount).toBe(1); // Should not increase }); it('should track modifications to different objects', () => { const sandbox = new Sandbox(); let obj1ModCount = 0; let obj2ModCount = 0; const code = ` obj1.value = 100; obj2.value = 200; `; const fn = sandbox.compile(code); const scope = { obj1: { value: 0 }, obj2: { value: 0 }, }; sandbox.subscribeSetGlobal(scope.obj1, 'value', (change) => { obj1ModCount++; }); sandbox.subscribeSetGlobal(scope.obj2, 'value', (change) => { obj2ModCount++; }); fn(scope).run(); expect(obj1ModCount).toBe(1); expect(obj2ModCount).toBe(1); expect(scope.obj1.value).toBe(100); expect(scope.obj2.value).toBe(200); }); }); describe('Multiple subscribers', () => { it('should notify multiple subscribers for same property', () => { const sandbox = new Sandbox(); let subscriber1Called = 0; let subscriber2Called = 0; let subscriber3Called = 0; const code = ` obj.value = 42; `; const fn = sandbox.compile(code); const scope = { obj: { value: 0 } }; const { context, run } = fn(scope); sandbox.subscribeSet( scope.obj, 'value', () => { subscriber1Called++; }, context, ); sandbox.subscribeSet( scope.obj, 'value', () => { subscriber2Called++; }, context, ); sandbox.subscribeSet( scope.obj, 'value', () => { subscriber3Called++; }, context, ); run(); expect(subscriber1Called).toBe(1); expect(subscriber2Called).toBe(1); expect(subscriber3Called).toBe(1); }); it('should independently unsubscribe multiple subscribers', () => { const sandbox = new Sandbox(); let subscriber1Called = 0; let subscriber2Called = 0; const code = ` obj.value = 10; `; const fn = sandbox.compile(code); const scope = { obj: { value: 0 } }; const { context, run } = fn(scope); const sub1 = sandbox.subscribeSet( scope.obj, 'value', () => { subscriber1Called++; }, context, ); const sub2 = sandbox.subscribeSet( scope.obj, 'value', () => { subscriber2Called++; }, context, ); run(); expect(subscriber1Called).toBe(1); expect(subscriber2Called).toBe(1); sub1.unsubscribe(); // Set a different value to trigger subscription again scope.obj.value = 0; // Reset run(); expect(subscriber1Called).toBe(1); // Should not increase expect(subscriber2Called).toBe(2); // Should increase }); }); describe('Change notification with object replacement', () => { it('should track when property value is an object that gets replaced', () => { const sandbox = new Sandbox(); let modificationCount = 0; const code = ` obj.nested = { x: 1, y: 2 }; obj.nested = { x: 3, y: 4 }; `; const fn = sandbox.compile(code); const scope = { obj: { nested: { x: 0, y: 0 } } }; const { context, run } = fn(scope); sandbox.subscribeSet( scope.obj, 'nested', (change) => { modificationCount++; }, context, ); run(); expect(modificationCount).toBe(2); expect(scope.obj.nested).toEqual({ x: 3, y: 4 }); }); it('should track array modifications', () => { const sandbox = new Sandbox(); let modificationCount = 0; const code = ` obj.items = [1, 2, 3]; obj.items = [4, 5, 6]; `; const fn = sandbox.compile(code); const scope = { obj: { items: [] as number[] } }; const { context, run } = fn(scope); sandbox.subscribeSet( scope.obj, 'items', (change) => { modificationCount++; }, context, ); run(); expect(modificationCount).toBe(2); expect(scope.obj.items).toEqual([4, 5, 6]); }); }); describe('Array mutation subscriptions', () => { it('should track array push operations', () => { const sandbox = new Sandbox(); const changes: any[] = []; const code = ` arr.push(4); arr.push(5, 6); `; const fn = sandbox.compile(code); const scope = { arr: [1, 2, 3] }; const { context, run } = fn(scope); sandbox.subscribeSet( scope, 'arr', (change) => { changes.push(change); }, context, ); run(); expect(changes.length).toBe(2); expect(changes[0].type).toBe('push'); expect(changes[0].added).toEqual([4]); expect(changes[1].type).toBe('push'); expect(changes[1].added).toEqual([5, 6]); expect(scope.arr).toEqual([1, 2, 3, 4, 5, 6]); }); it('should track array pop operations', () => { const sandbox = new Sandbox(); const changes: any[] = []; const code = ` arr.pop(); arr.pop(); `; const fn = sandbox.compile(code); const scope = { arr: [1, 2, 3, 4, 5] }; const { context, run } = fn(scope); sandbox.subscribeSet( scope, 'arr', (change) => { changes.push(change); }, context, ); run(); expect(changes.length).toBe(2); expect(changes[0].type).toBe('pop'); expect(changes[0].removed).toEqual([5]); expect(changes[1].type).toBe('pop'); expect(changes[1].removed).toEqual([4]); expect(scope.arr).toEqual([1, 2, 3]); }); it('should track array shift operations', () => { const sandbox = new Sandbox(); const changes: any[] = []; const code = ` arr.shift(); `; const fn = sandbox.compile(code); const scope = { arr: [1, 2, 3] }; const { context, run } = fn(scope); sandbox.subscribeSet( scope, 'arr', (change) => { changes.push(change); }, context, ); run(); expect(changes.length).toBe(1); expect(changes[0].type).toBe('shift'); expect(changes[0].removed).toEqual([1]); expect(scope.arr).toEqual([2, 3]); }); it('should track array unshift operations', () => { const sandbox = new Sandbox(); const changes: any[] = []; const code = ` arr.unshift(0); arr.unshift(-2, -1); `; const fn = sandbox.compile(code); const scope = { arr: [1, 2, 3] }; const { context, run } = fn(scope); sandbox.subscribeSet( scope, 'arr', (change) => { changes.push(change); }, context, ); run(); expect(changes.length).toBe(2); expect(changes[0].type).toBe('unshift'); expect(changes[0].added).toEqual([0]); expect(changes[1].type).toBe('unshift'); expect(changes[1].added).toEqual([-2, -1]); expect(scope.arr).toEqual([-2, -1, 0, 1, 2, 3]); }); it('should track array splice operations', () => { const sandbox = new Sandbox(); const changes: any[] = []; const code = ` arr.splice(1, 2, 'a', 'b'); `; const fn = sandbox.compile(code); const scope = { arr: [1, 2, 3, 4, 5] }; const { context, run } = fn(scope); sandbox.subscribeSet( scope, 'arr', (change) => { changes.push(change); }, context, ); run(); expect(changes.length).toBe(1); expect(changes[0].type).toBe('splice'); expect(changes[0].startIndex).toBe(1); expect(changes[0].deleteCount).toBe(2); expect(changes[0].added).toEqual(['a', 'b']); expect(changes[0].removed).toEqual([2, 3]); expect(scope.arr).toEqual([1, 'a', 'b', 4, 5]); }); it('should track array reverse operations', () => { const sandbox = new Sandbox(); const changes: any[] = []; const code = ` arr.reverse(); `; const fn = sandbox.compile(code); const scope = { arr: [1, 2, 3, 4, 5] }; const { context, run } = fn(scope); sandbox.subscribeSet( scope, 'arr', (change) => { changes.push(change); }, context, ); run(); expect(changes.length).toBe(1); expect(changes[0].type).toBe('reverse'); expect(scope.arr).toEqual([5, 4, 3, 2, 1]); }); it('should track array sort operations', () => { const sandbox = new Sandbox(); const changes: any[] = []; const code = ` arr.sort((a, b) => b - a); `; const fn = sandbox.compile(code); const scope = { arr: [3, 1, 4, 1, 5, 9] }; const { context, run } = fn(scope); sandbox.subscribeSet( scope, 'arr', (change) => { changes.push(change); }, context, ); run(); expect(changes.length).toBe(1); expect(changes[0].type).toBe('sort'); expect(scope.arr).toEqual([9, 5, 4, 3, 1, 1]); }); it('should track array copyWithin operations', () => { const sandbox = new Sandbox(); const changes: any[] = []; const code = ` arr.copyWithin(0, 3, 5); `; const fn = sandbox.compile(code); const scope = { arr: [1, 2, 3, 4, 5] }; const { context, run } = fn(scope); sandbox.subscribeSet( scope, 'arr', (change) => { changes.push(change); }, context, ); run(); expect(changes.length).toBe(1); expect(changes[0].type).toBe('copyWithin'); expect(changes[0].startIndex).toBe(0); expect(changes[0].endIndex).toBe(2); expect(changes[0].added).toEqual([4, 5]); expect(changes[0].removed).toEqual([1, 2]); expect(scope.arr).toEqual([4, 5, 3, 4, 5]); }); it('should not trigger change when push adds no elements', () => { const sandbox = new Sandbox(); const changes: any[] = []; const code = ` arr.push(); `; const fn = sandbox.compile(code); const scope = { arr: [1, 2, 3] }; const { context, run } = fn(scope); sandbox.subscribeSet( scope, 'arr', (change) => { changes.push(change); }, context, ); run(); expect(changes.length).toBe(0); expect(scope.arr).toEqual([1, 2, 3]); }); it('should not trigger change when pop on empty array', () => { const sandbox = new Sandbox(); const changes: any[] = []; const code = ` arr.pop(); `; const fn = sandbox.compile(code); const scope = { arr: [] as number[] }; const { context, run } = fn(scope); sandbox.subscribeSet( scope, 'arr', (change) => { changes.push(change); }, context, ); run(); expect(changes.length).toBe(0); expect(scope.arr).toEqual([]); }); it('should not trigger change when reverse on empty array', () => { const sandbox = new Sandbox(); const changes: any[] = []; const code = ` arr.reverse(); `; const fn = sandbox.compile(code); const scope = { arr: [] as number[] }; const { context, run } = fn(scope); sandbox.subscribeSet( scope, 'arr', (change) => { changes.push(change); }, context, ); run(); expect(changes.length).toBe(0); expect(scope.arr).toEqual([]); }); }); }); ================================================ FILE: test/symbol.spec.ts ================================================ 'use strict'; import Sandbox, { LocalScope } from '../src/Sandbox.js'; function createScope(vars: Record = {}) { Object.setPrototypeOf(vars, LocalScope.prototype); return vars; } describe('SandboxSymbol', () => { it('keeps Symbol.for state local to the sandbox', () => { const sandbox = new Sandbox(); const hostShared = Symbol.for('shared'); const result = sandbox .compile( ` const local = Symbol('secret'); const shared = Symbol.for('shared'); return { localType: typeof local, shared, sameWithinSandbox: shared === Symbol.for('shared'), sandboxKey: Symbol.keyFor(shared), hostKeyInsideSandbox: Symbol.keyFor(hostShared), equalsHostShared: shared === hostShared, }; `, true, )(createScope({ hostShared })) .run() as { localType: string; shared: symbol; sameWithinSandbox: boolean; sandboxKey: string; hostKeyInsideSandbox: string | undefined; equalsHostShared: boolean; }; expect(result.localType).toBe('symbol'); expect(result.sameWithinSandbox).toBe(true); expect(result.sandboxKey).toBe('shared'); expect(result.hostKeyInsideSandbox).toBeUndefined(); expect(result.equalsHostShared).toBe(false); expect(Symbol.keyFor(result.shared)).toBeUndefined(); expect(Symbol.keyFor(hostShared)).toBe('shared'); }); it('keeps well-known symbols native for iterator support', () => { const sandbox = new Sandbox(); const result = sandbox .compile( ` const iterable = { [Symbol.iterator]: function* () { yield 7; }, }; return [Symbol.iterator === hostIterator, [...iterable][0]]; `, true, )(createScope({ hostIterator: Symbol.iterator })) .run(); expect(result).toEqual([true, 7]); }); it('exposes the default safe symbol allowlist', () => { const sandbox = new Sandbox(); const result = sandbox .compile( ` return [ Symbol.iterator === hostIterator, Symbol.asyncIterator === hostAsyncIterator, Symbol.custom, ]; `, true, )(createScope({ hostIterator: Symbol.iterator, hostAsyncIterator: Symbol.asyncIterator })) .run(); expect(result).toEqual([true, true, undefined]); }); it('allows custom whitelisted symbol statics', () => { const custom = Symbol.for('custom'); const sandbox = new Sandbox({ symbolWhitelist: { ...Sandbox.SAFE_SYMBOLS, custom, }, }); const result = sandbox .compile( ` const sym = Symbol.for('custom'); return [ sym === hostCustom, sym === Symbol.for('custom'), typeof sym ]; `, true, )(createScope({ hostCustom: custom })) .run(); expect(result).toEqual([false, true, 'symbol']); }); it('preserves sandbox symbol keys across spread and fromEntries', () => { const sandbox = new Sandbox(); const result = sandbox .compile( ` const local = Symbol('local'); const viaSpread = { ...{ [local]: 3, a: 1 } }; const shared = Symbol.for('shared'); const viaEntries = Object.fromEntries([[shared, 5]]); return [ viaSpread[local], viaSpread.hasOwnProperty(local), Object.keys(viaSpread), viaEntries[shared], viaEntries.hasOwnProperty(shared), Symbol.keyFor(shared), ]; `, true, )() .run(); expect(result).toEqual([3, true, ['a'], 5, true, 'shared']); }); }); ================================================ FILE: test/taggedTemplateEscaping.spec.ts ================================================ import Sandbox from '../src/Sandbox.js'; describe('Tagged Template Escaping Tests', () => { let sandbox: Sandbox; beforeEach(() => { sandbox = new Sandbox(); }); it('should handle escaped dollar signs in tagged templates', () => { const code = ` function tag(strings, ...values) { let result = strings[0]; for (let i = 0; i < values.length; i++) { result += values[i] + strings[i + 1]; } return result; } const x = 5; return tag\`value: \\\${x}\`; `; const fn = sandbox.compile(code); const result = fn().run(); // The escaped \${ should be treated as literal text "${x}", not interpolation expect(result).toBe('value: ${x}'); }); it('should handle normal interpolations in tagged templates', () => { const code = ` function tag(strings, ...values) { return strings[0] + values[0] + strings[1]; } const x = 42; return tag\`value: \${x}!\`; `; const fn = sandbox.compile(code); const result = fn().run(); expect(result).toBe('value: 42!'); }); it('should handle multiple escaped sequences', () => { const code = ` function tag(strings, ...values) { return strings.join('|'); } return tag\`start\\\${foo}\\\${bar}end\`; `; const fn = sandbox.compile(code); const result = fn().run(); expect(result).toBe('start${foo}${bar}end'); }); it('should handle mixed escaped and real interpolations', () => { const code = ` function tag(strings, ...values) { let result = ''; for (let i = 0; i < strings.length; i++) { result += strings[i]; if (i < values.length) { result += values[i]; } } return result; } const x = 10; return tag\`value: \${x}, escaped: \\\${y}\`; `; const fn = sandbox.compile(code); const result = fn().run(); expect(result).toBe('value: 10, escaped: ${y}'); }); it('should handle malformed placeholders without hanging', () => { const code = ` function tag(strings) { return strings[0]; } return tag\`text with \\\${ no closing brace\`; `; const fn = sandbox.compile(code); const result = fn().run(); // Should treat the escaped ${ as literal text since it's not a valid placeholder expect(result).toContain('${'); }); it('should handle non-numeric placeholder content', () => { const code = ` function tag(strings) { return strings[0]; } // The escaped sequence should be treated as literal text return tag\`start\`; `; const fn = sandbox.compile(code); const result = fn().run(); expect(result).toBe('start'); }); it('should correctly parse adjacent interpolations', () => { const code = ` function tag(strings, ...values) { return values.join(','); } const a = 1, b = 2; return tag\`\${a}\${b}\`; `; const fn = sandbox.compile(code); const result = fn().run(); expect(result).toBe('1,2'); }); it('should handle escaped backslash before interpolation', () => { const code = ` function tag(strings, ...values) { return strings[0] + values[0] + strings[1]; } const x = 5; return tag\`\\\\\${x}\`; `; const fn = sandbox.compile(code); const result = fn().run(); // \\ becomes \ and ${x} is interpolated expect(result).toBe('\\5'); }); }); ================================================ FILE: test/ticks/sandboxArrayTicks.spec.ts ================================================ import Sandbox from '../../src/Sandbox.js'; describe('array ticks', () => { function haltsWithQuota( code: string, quota: bigint, scope: Record = {}, ): boolean { const sandbox = new Sandbox({ executionQuota: quota, haltOnSandboxError: true }); let halted = false; sandbox.subscribeHalt(() => { halted = true; }); sandbox.compile(code)(scope).run(); return halted; } describe('flat', () => { it('does not halt on a small flat array within quota', () => { expect(haltsWithQuota('return arr.flat()', 100n, { arr: [1, 2, 3] })).toBe(false); }); it('halts when flat on large array exceeds quota', () => { const arr = Array.from({ length: 200 }, (_, i) => i); expect(haltsWithQuota('return arr.flat()', 10n, { arr })).toBe(true); }); it('halts on deeply nested array with depth=2 but not with depth=1', () => { const arr = [Array.from({ length: 100 }, (_, i) => i)]; const nested = [arr]; expect(haltsWithQuota('return arr.flat(1)', 10n, { arr: nested })).toBe(false); expect(haltsWithQuota('return arr.flat(2)', 10n, { arr: nested })).toBe(true); }); it('respects explicit depth argument — depth=0 does not recurse', () => { const arr = [Array.from({ length: 100 }, (_, i) => i)]; expect(haltsWithQuota('return arr.flat(0)', 10n, { arr })).toBe(false); }); }); describe('flatMap', () => { it('does not halt on a small array within quota', () => { expect(haltsWithQuota('return arr.flatMap(x => x)', 100n, { arr: [[1], [2], [3]] })).toBe( false, ); }); it('halts when flatMap on large array exceeds quota', () => { const arr = Array.from({ length: 200 }, () => [1, 2, 3]); expect(haltsWithQuota('return arr.flatMap(x => x)', 10n, { arr })).toBe(true); }); it('only counts one level deep (does not recurse into deeply nested arrays)', () => { const arr = [Array.from({ length: 100 }, (_, i) => i)]; const nested = [arr]; expect(haltsWithQuota('return arr.flatMap(x => x)', 10n, { arr: nested })).toBe(false); }); }); describe('concat', () => { it('does not halt when concatenating small arrays within quota', () => { expect(haltsWithQuota('return a.concat(b)', 100n, { a: [1, 2], b: [3, 4, 5] })).toBe(false); }); it('halts when concat on large arrays exceeds quota', () => { const a = Array(100).fill(0); const b = Array(100).fill(0); expect(haltsWithQuota('return a.concat(b)', 10n, { a, b })).toBe(true); }); }); describe('O(1) — pop, at', () => { it('pop does not halt on a large array (always 1 tick)', () => { const arr = Array(200).fill(0); expect(haltsWithQuota('arr.pop()', 50n, { arr })).toBe(false); }); it('at does not halt on a large array (always 1 tick)', () => { const arr = Array(200).fill(0); expect(haltsWithQuota('return arr.at(0)', 50n, { arr })).toBe(false); }); it('O(1) does not halt at quota that halts O(n) map on the same array', () => { const arr = Array(200).fill(0); expect(haltsWithQuota('return arr.map(x => x)', 50n, { arr })).toBe(true); expect(haltsWithQuota('arr.at(1)', 50n, { arr })).toBe(false); }); }); describe('O(n) — map, filter, forEach, reduce, find, indexOf, etc.', () => { it('does not halt on a small array within quota', () => { expect(haltsWithQuota('return arr.map(x => x)', 100n, { arr: [1, 2, 3] })).toBe(false); }); it('halts on a large array when quota < array length', () => { const arr = Array(200).fill(1); expect(haltsWithQuota('return arr.map(x => x)', 10n, { arr })).toBe(true); }); it('filter halts on a large array', () => { const arr = Array(200).fill(1); expect(haltsWithQuota('return arr.filter(x => x)', 10n, { arr })).toBe(true); }); it('forEach halts on a large array', () => { const arr = Array(200).fill(1); expect(haltsWithQuota('arr.forEach(x => x)', 10n, { arr })).toBe(true); }); it('reduce halts on a large array', () => { const arr = Array(200).fill(1); expect(haltsWithQuota('return arr.reduce((a, x) => a + x, 0)', 10n, { arr })).toBe(true); }); it('find halts on a large array', () => { const arr = Array(200).fill(1); expect(haltsWithQuota('return arr.find(x => x === 2)', 10n, { arr })).toBe(true); }); it('indexOf halts on a large array', () => { const arr = Array(200).fill(1); expect(haltsWithQuota('return arr.indexOf(2)', 10n, { arr })).toBe(true); }); it('includes halts on a large array', () => { const arr = Array(200).fill(1); expect(haltsWithQuota('return arr.includes(2)', 10n, { arr })).toBe(true); }); it('slice halts on a large array', () => { const arr = Array(200).fill(1); expect(haltsWithQuota('return arr.slice()', 10n, { arr })).toBe(true); }); it('join halts on a large array', () => { const arr = Array(200).fill('a'); expect(haltsWithQuota('return arr.join(",")', 10n, { arr })).toBe(true); }); }); describe('O(n log n) — sort, toSorted', () => { it('sort halts at a lower quota than an equivalent O(n) op on the same array', () => { const arr = Array.from({ length: 200 }, (_, i) => 200 - i); expect(haltsWithQuota('return arr.indexOf(-1)', 500n, { arr })).toBe(false); expect(haltsWithQuota('return arr.sort()', 500n, { arr: [...arr] })).toBe(true); }); it('does not halt on a small array within quota', () => { expect(haltsWithQuota('return arr.sort()', 100n, { arr: [3, 1, 2] })).toBe(false); }); it('toSorted halts on a large array', () => { const arr = Array.from({ length: 200 }, (_, i) => 200 - i); expect(haltsWithQuota('return arr.toSorted()', 500n, { arr })).toBe(true); }); }); describe('Array.from', () => { it('does not halt on a small array-like within quota', () => { expect(haltsWithQuota('return Array.from(src)', 100n, { src: [1, 2, 3] })).toBe(false); }); it('halts when source length exceeds quota', () => { const src = Array(200).fill(1); expect(haltsWithQuota('return Array.from(src)', 10n, { src })).toBe(true); }); it('halts when using Array.from with a mapFn on a large source', () => { const src = Array(200).fill(1); expect(haltsWithQuota('return Array.from(src, x => x * 2)', 10n, { src })).toBe(true); }); it('works with array-like objects with a length property', () => { const src = { length: 200, 0: 'a', 1: 'b' }; expect(haltsWithQuota('return Array.from(src)', 10n, { src })).toBe(true); }); it('does not halt when source has no length (e.g. Set)', () => { expect(haltsWithQuota('return Array.from(src)', 10n, { src: new Set([1, 2, 3]) })).toBe( false, ); }); }); describe('arr.length =', () => { it('does not halt when shrinking within quota', () => { const arr = Array(5).fill(0); expect(haltsWithQuota('arr.length = 2', 100n, { arr })).toBe(false); }); it('halts when expanding length far exceeds quota', () => { const arr: number[] = []; expect(haltsWithQuota('arr.length = 200', 10n, { arr })).toBe(true); }); it('halts when shrinking by a large delta that exceeds quota', () => { const arr = Array(200).fill(0); expect(haltsWithQuota('arr.length = 0', 10n, { arr })).toBe(true); }); it('does not charge ticks when length is unchanged', () => { const arr = Array(5).fill(0); expect(haltsWithQuota('arr.length = 5', 5n, { arr })).toBe(false); }); }); }); ================================================ FILE: test/ticks/sandboxCollectionTicks.spec.ts ================================================ import Sandbox from '../../src/Sandbox.js'; describe('Map ticks', () => { function haltsWithQuota( code: string, quota: bigint, scope: Record = {}, ): boolean { const sandbox = new Sandbox({ executionQuota: quota, haltOnSandboxError: true }); let halted = false; sandbox.subscribeHalt(() => { halted = true; }); sandbox.compile(code)(scope).run(); return halted; } describe('O(1) — get, set, has, delete', () => { it('get does not halt on a large Map (always 1 tick)', () => { const m = new Map(Array.from({ length: 200 }, (_, i) => [i, i])); expect(haltsWithQuota('return m.get(0)', 50n, { m })).toBe(false); }); it('set does not halt on a large Map (always 1 tick)', () => { const m = new Map(Array.from({ length: 200 }, (_, i) => [i, i])); expect(haltsWithQuota('m.set(999, 999)', 50n, { m })).toBe(false); }); it('has does not halt on a large Map (always 1 tick)', () => { const m = new Map(Array.from({ length: 200 }, (_, i) => [i, i])); expect(haltsWithQuota('return m.has(0)', 50n, { m })).toBe(false); }); it('delete does not halt on a large Map (always 1 tick)', () => { const m = new Map(Array.from({ length: 200 }, (_, i) => [i, i])); expect(haltsWithQuota('m.delete(0)', 50n, { m })).toBe(false); }); it('O(1) does not halt at quota that halts O(n) forEach on same Map', () => { const m = new Map(Array.from({ length: 200 }, (_, i) => [i, i])); expect(haltsWithQuota('m.forEach(() => {})', 50n, { m })).toBe(true); expect(haltsWithQuota('return m.get(0)', 50n, { m })).toBe(false); }); }); describe('O(n) — keys, values, entries, forEach, clear', () => { it('does not halt on a small Map within quota', () => { const m = new Map([ ['a', 1], ['b', 2], ]); expect(haltsWithQuota('return [...m.keys()]', 100n, { m })).toBe(false); }); it('forEach halts on a large Map', () => { const m = new Map(Array.from({ length: 200 }, (_, i) => [i, i])); expect(haltsWithQuota('m.forEach(() => {})', 10n, { m })).toBe(true); }); it('keys halts on a large Map', () => { const m = new Map(Array.from({ length: 200 }, (_, i) => [i, i])); expect(haltsWithQuota('return [...m.keys()]', 10n, { m })).toBe(true); }); it('values halts on a large Map', () => { const m = new Map(Array.from({ length: 200 }, (_, i) => [i, i])); expect(haltsWithQuota('return [...m.values()]', 10n, { m })).toBe(true); }); it('entries halts on a large Map', () => { const m = new Map(Array.from({ length: 200 }, (_, i) => [i, i])); expect(haltsWithQuota('return [...m.entries()]', 10n, { m })).toBe(true); }); it('clear halts on a large Map', () => { const m = new Map(Array.from({ length: 200 }, (_, i) => [i, i])); expect(haltsWithQuota('m.clear()', 10n, { m })).toBe(true); }); }); describe('null edge cases', () => { it('Map.get on null map throws TypeError', () => { expect(() => new Sandbox().compile('return m.get(0)')({ m: null }).run()).toThrow(TypeError); }); }); }); describe('Set ticks', () => { function haltsWithQuota( code: string, quota: bigint, scope: Record = {}, ): boolean { const sandbox = new Sandbox({ executionQuota: quota, haltOnSandboxError: true }); let halted = false; sandbox.subscribeHalt(() => { halted = true; }); sandbox.compile(code)(scope).run(); return halted; } describe('O(1) — add, has, delete', () => { it('add does not halt on a large Set (always 1 tick)', () => { const s = new Set(Array.from({ length: 200 }, (_, i) => i)); expect(haltsWithQuota('s.add(999)', 50n, { s })).toBe(false); }); it('has does not halt on a large Set (always 1 tick)', () => { const s = new Set(Array.from({ length: 200 }, (_, i) => i)); expect(haltsWithQuota('return s.has(0)', 50n, { s })).toBe(false); }); it('delete does not halt on a large Set (always 1 tick)', () => { const s = new Set(Array.from({ length: 200 }, (_, i) => i)); expect(haltsWithQuota('s.delete(0)', 50n, { s })).toBe(false); }); it('O(1) does not halt at quota that halts O(n) forEach on same Set', () => { const s = new Set(Array.from({ length: 200 }, (_, i) => i)); expect(haltsWithQuota('s.forEach(() => {})', 50n, { s })).toBe(true); expect(haltsWithQuota('s.has(0)', 50n, { s })).toBe(false); }); }); describe('O(n) — values, keys, entries, forEach, clear', () => { it('does not halt on a small Set within quota', () => { const s = new Set([1, 2, 3]); expect(haltsWithQuota('return [...s.values()]', 100n, { s })).toBe(false); }); it('forEach halts on a large Set', () => { const s = new Set(Array.from({ length: 200 }, (_, i) => i)); expect(haltsWithQuota('s.forEach(() => {})', 10n, { s })).toBe(true); }); it('values halts on a large Set', () => { const s = new Set(Array.from({ length: 200 }, (_, i) => i)); expect(haltsWithQuota('return [...s.values()]', 10n, { s })).toBe(true); }); it('keys halts on a large Set', () => { const s = new Set(Array.from({ length: 200 }, (_, i) => i)); expect(haltsWithQuota('return [...s.keys()]', 10n, { s })).toBe(true); }); it('entries halts on a large Set', () => { const s = new Set(Array.from({ length: 200 }, (_, i) => i)); expect(haltsWithQuota('return [...s.entries()]', 10n, { s })).toBe(true); }); it('clear halts on a large Set', () => { const s = new Set(Array.from({ length: 200 }, (_, i) => i)); expect(haltsWithQuota('s.clear()', 10n, { s })).toBe(true); }); }); describe('null edge cases', () => { it('Set.has on null set throws TypeError', () => { expect(() => new Sandbox().compile('return s.has(0)')({ s: null }).run()).toThrow(TypeError); }); }); }); describe('TypedArray ticks', () => { // TypedArrays need explicit whitelisting. // The shared %TypedArray%.prototype(s) must be registered via fake constructors // because prototypeWhitelist keys are constructors (mapped to .prototype internally). // In some environments (e.g., JSDOM) Uint8Array has a patched prototype chain, // so collect all unique %TypedArray% protos across the typed array types. const typedArrayCtors = [ Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array, ]; const uniqueTypedArrayProtos = new Set( typedArrayCtors.map((T) => Object.getPrototypeOf(T.prototype)), ); const prototypeWhitelist = new Map(Sandbox.SAFE_PROTOTYPES); for (const proto of uniqueTypedArrayProtos) { function FakeTypedArrayCtor() {} FakeTypedArrayCtor.prototype = proto; prototypeWhitelist.set(FakeTypedArrayCtor as any, new Set()); } for (const T of typedArrayCtors) { prototypeWhitelist.set(T, new Set()); } function haltsWithQuota( code: string, quota: bigint, scope: Record = {}, ): boolean { const sandbox = new Sandbox({ executionQuota: quota, haltOnSandboxError: true, prototypeWhitelist, }); let halted = false; sandbox.subscribeHalt(() => { halted = true; }); sandbox.compile(code)(scope).run(); return halted; } it('map on large TypedArray halts', () => { const ta = new Int32Array(200); expect(haltsWithQuota('return ta.map(x => x)', 10n, { ta })).toBe(true); }); it('indexOf on large TypedArray halts', () => { const ta = new Float64Array(200); expect(haltsWithQuota('return ta.indexOf(-1)', 10n, { ta })).toBe(true); }); it('sort on large TypedArray halts at lower quota than indexOf (O(n log n))', () => { const ta = new Int32Array(Array.from({ length: 200 }, (_, i) => 200 - i)); expect(haltsWithQuota('return ta.indexOf(-1)', 500n, { ta })).toBe(false); expect(haltsWithQuota('return ta.sort()', 500n, { ta: ta.slice() })).toBe(true); }); it('at on large TypedArray does not halt (O(1))', () => { const ta = new Uint8Array(200); expect(haltsWithQuota('return ta.at(0)', 50n, { ta })).toBe(false); }); it('does not halt on small TypedArray within quota', () => { const ta = new Int32Array([1, 2, 3]); expect(haltsWithQuota('return ta.map(x => x)', 100n, { ta })).toBe(false); }); }); ================================================ FILE: test/ticks/sandboxNativeTicks.spec.ts ================================================ import Sandbox from '../../src/Sandbox.js'; describe('Math ticks', () => { function haltsWithQuota( code: string, quota: bigint, scope: Record = {}, ): boolean { const sandbox = new Sandbox({ executionQuota: quota, haltOnSandboxError: true }); let halted = false; sandbox.subscribeHalt(() => { halted = true; }); sandbox.compile(code)(scope).run(); return halted; } it('Math.max halts when called with many arguments', () => { const args = Array.from({ length: 200 }, (_, i) => i).join(','); expect(haltsWithQuota(`return Math.max(${args})`, 10n)).toBe(true); }); it('Math.min halts when called with many arguments', () => { const args = Array.from({ length: 200 }, (_, i) => i).join(','); expect(haltsWithQuota(`return Math.min(${args})`, 10n)).toBe(true); }); it('Math.max does not halt with few arguments', () => { expect(haltsWithQuota('return Math.max(1, 2, 3)', 100n)).toBe(false); }); it('Math.hypot halts when called with many arguments', () => { const args = Array.from({ length: 200 }, (_, i) => i).join(','); expect(haltsWithQuota(`return Math.hypot(${args})`, 10n)).toBe(true); }); }); describe('JSON ticks', () => { function haltsWithQuota( code: string, quota: bigint, scope: Record = {}, ): boolean { const sandbox = new Sandbox({ executionQuota: quota, haltOnSandboxError: true }); let halted = false; sandbox.subscribeHalt(() => { halted = true; }); sandbox.compile(code)(scope).run(); return halted; } it('JSON.parse halts on a long string', () => { const json = JSON.stringify(Array(200).fill(1)); expect(haltsWithQuota('return JSON.parse(json)', 10n, { json })).toBe(true); }); it('JSON.parse does not halt on a short string', () => { expect(haltsWithQuota('return JSON.parse(json)', 100n, { json: '[1,2,3]' })).toBe(false); }); it('JSON.stringify halts on a large object', () => { const obj = Object.fromEntries(Array.from({ length: 200 }, (_, i) => [`k${i}`, i])); expect(haltsWithQuota('return JSON.stringify(obj)', 10n, { obj })).toBe(true); }); }); describe('RegExp ticks', () => { function haltsWithQuota( code: string, quota: bigint, scope: Record = {}, ): boolean { const sandbox = new Sandbox({ executionQuota: quota, haltOnSandboxError: true }); let halted = false; sandbox.subscribeHalt(() => { halted = true; }); sandbox.compile(code)(scope).run(); return halted; } it('exec halts on a long input string', () => { const input = 'a'.repeat(200); expect(haltsWithQuota('return re.exec(input)', 10n, { re: /z/, input })).toBe(true); }); it('test halts on a long input string', () => { const input = 'a'.repeat(200); expect(haltsWithQuota('return re.test(input)', 10n, { re: /z/, input })).toBe(true); }); it('does not halt on a short input string', () => { expect(haltsWithQuota('return re.test(input)', 100n, { re: /z/, input: 'hello' })).toBe(false); }); }); describe('Promise ticks', () => { function haltsWithQuota( code: string, quota: bigint, scope: Record = {}, ): boolean { const sandbox = new Sandbox({ executionQuota: quota, haltOnSandboxError: true }); let halted = false; sandbox.subscribeHalt(() => { halted = true; }); sandbox.compile(code)(scope).run(); return halted; } it('Promise.all halts on a large array of promises', () => { const promises = Array.from({ length: 200 }, () => Promise.resolve(1)); expect(haltsWithQuota('return Promise.all(promises)', 10n, { promises })).toBe(true); }); it('Promise.allSettled halts on a large array', () => { const promises = Array.from({ length: 200 }, () => Promise.resolve(1)); expect(haltsWithQuota('return Promise.allSettled(promises)', 10n, { promises })).toBe(true); }); it('Promise.race halts on a large array', () => { const promises = Array.from({ length: 200 }, () => Promise.resolve(1)); expect(haltsWithQuota('return Promise.race(promises)', 10n, { promises })).toBe(true); }); it('does not halt on a small array of promises', () => { const promises = [Promise.resolve(1), Promise.resolve(2)]; expect(haltsWithQuota('return Promise.all(promises)', 100n, { promises })).toBe(false); }); }); describe('new constructor ticks', () => { function haltsWithQuota( code: string, quota: bigint, scope: Record = {}, ): boolean { const sandbox = new Sandbox({ executionQuota: quota, haltOnSandboxError: true }); let halted = false; sandbox.subscribeHalt(() => { halted = true; }); sandbox.compile(code)(scope).run(); return halted; } describe('new Array', () => { it('new Array(n) halts when n is large', () => { expect(haltsWithQuota('return new Array(200)', 10n)).toBe(true); }); it('new Array(n) does not halt when n is small', () => { expect(haltsWithQuota('return new Array(3)', 20n)).toBe(false); }); it('new Array(...items) charges args.length ticks', () => { expect(haltsWithQuota('return new Array("a","b","c")', 10n)).toBe(false); const items = Array.from({ length: 200 }, (_, i) => i).join(','); expect(haltsWithQuota(`return new Array(${items})`, 10n)).toBe(true); }); }); describe('new Map / new Set', () => { it('new Map(iterable) halts when iterable is large', () => { const entries = Array.from({ length: 200 }, (_, i): [number, number] => [i, i]); expect(haltsWithQuota('return new Map(entries)', 10n, { entries })).toBe(true); }); it('new Map(iterable) does not halt when iterable is small', () => { expect(haltsWithQuota('return new Map(entries)', 20n, { entries: [[1, 1]] })).toBe(false); }); it('new Set(iterable) halts when iterable is large', () => { const items = Array.from({ length: 200 }, (_, i) => i); expect(haltsWithQuota('return new Set(items)', 10n, { items })).toBe(true); }); it('new Set(iterable) does not halt when iterable is small', () => { expect(haltsWithQuota('return new Set(items)', 20n, { items: [1, 2, 3] })).toBe(false); }); }); describe('new TypedArray', () => { it('new Int32Array(n) halts when n is large', () => { expect(haltsWithQuota('return new Int32Array(200)', 10n)).toBe(true); }); it('new Int32Array(n) does not halt when n is small', () => { expect(haltsWithQuota('return new Int32Array(3)', 20n)).toBe(false); }); it('new Float64Array(n) halts when n is large', () => { expect(haltsWithQuota('return new Float64Array(200)', 10n)).toBe(true); }); it('new TypedArray(array) charges source length ticks', () => { const src = Array.from({ length: 200 }, (_, i) => i); expect(haltsWithQuota('return new Int32Array(src)', 10n, { src })).toBe(true); expect(haltsWithQuota('return new Int32Array(src)', 210n, { src })).toBe(false); }); }); }); ================================================ FILE: test/ticks/sandboxObjectTicks.spec.ts ================================================ import Sandbox from '../../src/Sandbox.js'; describe('object ticks', () => { function haltsWithQuota( code: string, quota: bigint, scope: Record = {}, ): boolean { const sandbox = new Sandbox({ executionQuota: quota, haltOnSandboxError: true }); let halted = false; sandbox.subscribeHalt(() => { halted = true; }); sandbox.compile(code)(scope).run(); return halted; } describe('O(1) — Object.is, hasOwnProperty, isPrototypeOf', () => { it('Object.is does not halt on a large object (always 1 tick)', () => { const obj = Object.fromEntries(Array.from({ length: 200 }, (_, i) => [`k${i}`, i])); expect(haltsWithQuota('return Object.is(obj, obj)', 50n, { obj })).toBe(false); }); it('hasOwnProperty does not halt on an object with many keys (always 1 tick)', () => { const obj = Object.fromEntries(Array.from({ length: 200 }, (_, i) => [`k${i}`, i])); expect(haltsWithQuota('return obj.hasOwnProperty("k0")', 50n, { obj })).toBe(false); }); it('O(1) does not halt at quota that halts O(n) on same object', () => { const obj = Object.fromEntries(Array.from({ length: 200 }, (_, i) => [`k${i}`, i])); expect(haltsWithQuota('return Object.keys(obj)', 50n, { obj })).toBe(true); expect(haltsWithQuota('return obj.hasOwnProperty("k0")', 50n, { obj })).toBe(false); }); }); describe('O(n) static — Object.keys, Object.values, Object.entries, etc.', () => { it('does not halt on a small object within quota', () => { expect(haltsWithQuota('return Object.keys(obj)', 100n, { obj: { a: 1, b: 2 } })).toBe(false); }); it('Object.keys halts on an object with many keys', () => { const obj = Object.fromEntries(Array.from({ length: 200 }, (_, i) => [`k${i}`, i])); expect(haltsWithQuota('return Object.keys(obj)', 10n, { obj })).toBe(true); }); it('Object.values halts on an object with many keys', () => { const obj = Object.fromEntries(Array.from({ length: 200 }, (_, i) => [`k${i}`, i])); expect(haltsWithQuota('return Object.values(obj)', 10n, { obj })).toBe(true); }); it('Object.entries halts on an object with many keys', () => { const obj = Object.fromEntries(Array.from({ length: 200 }, (_, i) => [`k${i}`, i])); expect(haltsWithQuota('return Object.entries(obj)', 10n, { obj })).toBe(true); }); it('Object.getOwnPropertyNames halts on an object with many keys', () => { const obj = Object.fromEntries(Array.from({ length: 200 }, (_, i) => [`k${i}`, i])); expect(haltsWithQuota('return Object.getOwnPropertyNames(obj)', 10n, { obj })).toBe(true); }); }); }); describe('JSON.stringify ticks', () => { function haltsWithQuota( code: string, quota: bigint, scope: Record = {}, ): boolean { const sandbox = new Sandbox({ executionQuota: quota, haltOnSandboxError: true }); let halted = false; sandbox.subscribeHalt(() => { halted = true; }); const fn = sandbox.compile(code); const { context, run } = fn(scope); sandbox.subscribeGet(() => {}, context); run(); return halted; } it('does not halt on a small object within quota', () => { expect(haltsWithQuota('return JSON.stringify(obj)', 100n, { obj: { a: 1 } })).toBe(false); }); it('halts when stringifying an object with many keys', () => { const obj = Object.fromEntries(Array.from({ length: 200 }, (_, i) => [`k${i}`, i])); expect(haltsWithQuota('return JSON.stringify(obj)', 10n, { obj })).toBe(true); }); it('halts on deeply nested objects (counts keys at all levels)', () => { let obj: Record = {}; let cur = obj; for (let i = 0; i < 50; i++) { cur.next = {}; cur = cur.next as any; } expect(haltsWithQuota('return JSON.stringify(obj)', 10n, { obj })).toBe(true); }); it('does not halt for a small object at generous quota', () => { const obj = { a: 1, b: 2, c: 3 }; expect(haltsWithQuota('return JSON.stringify(obj)', 100n, { obj })).toBe(false); }); it('fires tick check even without subscribeGet (getTicks handles JSON directly)', () => { const sandbox = new Sandbox({ executionQuota: 10n, haltOnSandboxError: true }); let halted = false; sandbox.subscribeHalt(() => { halted = true; }); const obj = Object.fromEntries(Array.from({ length: 200 }, (_, i) => [`k${i}`, i])); sandbox.compile('return JSON.stringify(obj)')({ obj }).run(); expect(halted).toBe(true); }); }); describe('null obj edge cases', () => { function run(code: string, scope: Record = {}) { return new Sandbox().compile(code)(scope).run(); } it('calling a method on null throws TypeError', () => { expect(() => run('return null.toString()')).toThrow(TypeError); }); it('JSON.stringify(null) returns "null" without crashing', () => { expect(run('return JSON.stringify(null)')).toBe('null'); }); it('JSON.stringify(null) does not halt even with subscribeGet active', () => { const sandbox = new Sandbox({ executionQuota: 100n, haltOnSandboxError: true }); let halted = false; sandbox.subscribeHalt(() => { halted = true; }); const fn = sandbox.compile('return JSON.stringify(null)'); const { context, run: r } = fn({}); sandbox.subscribeGet(() => {}, context); const result = r(); expect(halted).toBe(false); expect(result).toBe('null'); }); it('Array.from(null) throws TypeError', () => { expect(() => run('return Array.from(null)')).toThrow(TypeError); }); it('Array.from(null) does not crash the tick calculation', () => { const sandbox = new Sandbox({ executionQuota: 1000n, haltOnSandboxError: true }); let halted = false; sandbox.subscribeHalt(() => { halted = true; }); expect(() => sandbox.compile('return Array.from(null)')({}).run()).toThrow(TypeError); expect(halted).toBe(false); }); it('Object.keys(null) throws TypeError', () => { expect(() => run('return Object.keys(null)')).toThrow(TypeError); }); it('arr.length = null does not charge ticks (non-number assignment)', () => { const sandbox = new Sandbox({ executionQuota: 5n, haltOnSandboxError: true }); let halted = false; sandbox.subscribeHalt(() => { halted = true; }); const arr: number[] = [1, 2, 3]; sandbox.compile('arr.length = null')({ arr }).run(); expect(halted).toBe(false); }); }); ================================================ FILE: test/ticks/sandboxSpreadTicks.spec.ts ================================================ import Sandbox from '../../src/Sandbox.js'; describe('spread ticks', () => { function haltsWithQuota( code: string, quota: bigint, scope: Record = {}, ): boolean { const sandbox = new Sandbox({ executionQuota: quota, haltOnSandboxError: true }); let halted = false; sandbox.subscribeHalt(() => { halted = true; }); sandbox.compile(code)(scope).run(); return halted; } it('spread into array literal halts on large array', () => { const arr = Array.from({ length: 200 }, (_, i) => i); expect(haltsWithQuota('return [...arr]', 10n, { arr })).toBe(true); }); it('spread into array literal does not halt on small array', () => { const arr = [1, 2, 3]; expect(haltsWithQuota('return [...arr]', 50n, { arr })).toBe(false); }); it('spread into function call halts on large array', () => { const arr = Array.from({ length: 200 }, (_, i) => i); expect(haltsWithQuota('return Math.max(...arr)', 10n, { arr })).toBe(true); }); it('spread into function call does not halt on small array', () => { const arr = [1, 2, 3]; expect(haltsWithQuota('return Math.max(...arr)', 50n, { arr })).toBe(false); }); it('object spread halts on large object', () => { const obj = Object.fromEntries(Array.from({ length: 200 }, (_, i) => [`k${i}`, i])); expect(haltsWithQuota('return {...obj}', 10n, { obj })).toBe(true); }); it('object spread does not halt on small object', () => { const obj = { a: 1, b: 2, c: 3 }; expect(haltsWithQuota('return {...obj}', 50n, { obj })).toBe(false); }); it('spread of generator halts on large generator', () => { expect( haltsWithQuota( 'function* gen() { for (let i = 0; i < 200; i++) yield i; } return [...gen()]', 10n, ), ).toBe(true); }); it('spread of generator does not halt on small generator', () => { expect( haltsWithQuota('function* gen() { yield 1; yield 2; yield 3; } return [...gen()]', 50n), ).toBe(false); }); it('multiple array spreads accumulate ticks', () => { const a = [1, 2, 3]; const b = [4, 5, 6]; expect(haltsWithQuota('return [...a]', 20n, { a })).toBe(false); expect(haltsWithQuota('return [...a, ...b]', 10n, { a, b })).toBe(true); }); it('spread into user-defined function halts on large array', () => { const big = Array.from({ length: 200 }, (_, i) => i); expect( haltsWithQuota('function f(...args) { return args.length } return f(...big)', 10n, { big }), ).toBe(true); }); it('spread into user-defined function does not halt on small array', () => { const arr = [1, 2, 3]; expect( haltsWithQuota('function f(...args) { return args.length } return f(...arr)', 20n, { arr }), ).toBe(false); }); it('multiple object spreads accumulate ticks', () => { const o1 = { a: 1, b: 2 }; const o2 = { c: 3, d: 4 }; expect(haltsWithQuota('return {...o1}', 20n, { o1 })).toBe(false); expect(haltsWithQuota('return {...o1, ...o2}', 10n, { o1, o2 })).toBe(true); }); it('spread combined with non-spread args does not double-count', () => { const arr = [1, 2, 3]; expect(haltsWithQuota('return [0, ...arr, 4]', 20n, { arr })).toBe(false); }); }); ================================================ FILE: test/ticks/sandboxStringTicks.spec.ts ================================================ import Sandbox from '../../src/Sandbox.js'; describe('string method ticks', () => { function haltsWithQuota( code: string, quota: bigint, scope: Record = {}, ): boolean { const sandbox = new Sandbox({ executionQuota: quota, haltOnSandboxError: true }); let halted = false; sandbox.subscribeHalt(() => { halted = true; }); sandbox.compile(code)(scope).run(); return halted; } describe('O(1) — charAt, charCodeAt, codePointAt, at', () => { it('charAt does not halt on a long string (always 1 tick)', () => { const s = 'a'.repeat(200); expect(haltsWithQuota('return s.charAt(0)', 50n, { s })).toBe(false); }); it('charCodeAt does not halt on a long string (always 1 tick)', () => { const s = 'a'.repeat(200); expect(haltsWithQuota('return s.charCodeAt(0)', 50n, { s })).toBe(false); }); it('at does not halt on a long string (always 1 tick)', () => { const s = 'a'.repeat(200); expect(haltsWithQuota('return s.at(0)', 50n, { s })).toBe(false); }); it('O(1) does not halt at quota that halts O(n) on same string', () => { const s = 'a'.repeat(200); expect(haltsWithQuota('return s.indexOf("z")', 50n, { s })).toBe(true); expect(haltsWithQuota('return s.charAt(0)', 50n, { s })).toBe(false); }); }); describe('O(n) — indexOf, includes, slice, split, replace, trim, etc.', () => { it('does not halt on a short string within quota', () => { expect(haltsWithQuota('return s.indexOf("z")', 100n, { s: 'hello' })).toBe(false); }); it('indexOf halts on a long string exceeding quota', () => { const s = 'a'.repeat(200); expect(haltsWithQuota('return s.indexOf("z")', 10n, { s })).toBe(true); }); it('includes halts on a long string', () => { const s = 'a'.repeat(200); expect(haltsWithQuota('return s.includes("z")', 10n, { s })).toBe(true); }); it('slice halts on a long string', () => { const s = 'a'.repeat(200); expect(haltsWithQuota('return s.slice(0)', 10n, { s })).toBe(true); }); it('split halts on a long string', () => { const s = 'a'.repeat(200); expect(haltsWithQuota('return s.split("")', 10n, { s })).toBe(true); }); it('replace halts on a long string', () => { const s = 'a'.repeat(200); expect(haltsWithQuota('return s.replace("b", "c")', 10n, { s })).toBe(true); }); it('toLowerCase halts on a long string', () => { const s = 'A'.repeat(200); expect(haltsWithQuota('return s.toLowerCase()', 10n, { s })).toBe(true); }); it('trim halts on a long string', () => { const s = ' '.repeat(200); expect(haltsWithQuota('return s.trim()', 10n, { s })).toBe(true); }); }); }); describe('string concatenation ticks (+ operator)', () => { function haltsWithQuota( code: string, quota: bigint, scope: Record = {}, ): boolean { const sandbox = new Sandbox({ executionQuota: quota, haltOnSandboxError: true }); let halted = false; sandbox.subscribeHalt(() => { halted = true; }); sandbox.compile(code)(scope).run(); return halted; } it('string + string halts when result is long', () => { const a = 'x'.repeat(100); const b = 'y'.repeat(100); expect(haltsWithQuota('return a + b', 10n, { a, b })).toBe(true); }); it('string + string does not halt when result is short', () => { expect(haltsWithQuota('return a + b', 100n, { a: 'hi', b: ' there' })).toBe(false); }); it('string + number halts when result is long', () => { const a = 'x'.repeat(200); expect(haltsWithQuota('return a + 42', 10n, { a })).toBe(true); }); it('number + string halts when result is long', () => { const b = 'x'.repeat(200); expect(haltsWithQuota('return 42 + b', 10n, { b })).toBe(true); }); it('number + number does not charge string ticks', () => { expect(haltsWithQuota('return 1 + 2', 5n)).toBe(false); }); it('chained concatenation accumulates ticks', () => { const s = 'x'.repeat(50); expect(haltsWithQuota('return s + s + s + s', 10n, { s })).toBe(true); }); it('empty string + empty string does not halt', () => { expect(haltsWithQuota('return "" + ""', 50n)).toBe(false); }); it('string literal + string literal halts when combined result is long', () => { const a = 'a'.repeat(100); const b = 'b'.repeat(100); expect(haltsWithQuota('return a + b', 10n, { a, b })).toBe(true); }); }); describe('string += ticks (AddEquals operator)', () => { function haltsWithQuota( code: string, quota: bigint, scope: Record = {}, ): boolean { const sandbox = new Sandbox({ executionQuota: quota, haltOnSandboxError: true }); let halted = false; sandbox.subscribeHalt(() => { halted = true; }); sandbox.compile(code)(scope).run(); return halted; } it('+= on string halts when result is long', () => { const s = 'x'.repeat(100); expect(haltsWithQuota('let r = s; r += s; return r', 10n, { s })).toBe(true); }); it('+= on string does not halt when result is short', () => { expect(haltsWithQuota('let r = "hi"; r += " there"; return r', 100n)).toBe(false); }); it('+= on number does not charge string ticks', () => { expect(haltsWithQuota('let n = 1; n += 2; return n', 50n)).toBe(false); }); it('+= string appended multiple times accumulates ticks', () => { const s = 'x'.repeat(40); // first += produces 80-char string, second produces 120-char — should halt early expect(haltsWithQuota('let r = s; r += s; r += s; return r', 10n, { s })).toBe(true); }); it('+= number to string halts when result is long', () => { const s = 'x'.repeat(200); expect(haltsWithQuota('let r = s; r += 1; return r', 10n, { s })).toBe(true); }); it('+= string to number coerces and halts when result is long', () => { const s = 'x'.repeat(200); expect(haltsWithQuota('let r = 1; r += s; return r', 10n, { s })).toBe(true); }); }); describe('template literal ticks', () => { function haltsWithQuota( code: string, quota: bigint, scope: Record = {}, ): boolean { const sandbox = new Sandbox({ executionQuota: quota, haltOnSandboxError: true }); let halted = false; sandbox.subscribeHalt(() => { halted = true; }); sandbox.compile(code)(scope).run(); return halted; } it('template literal halts when result is long', () => { const s = 'x'.repeat(200); expect(haltsWithQuota('return `prefix_${s}`', 10n, { s })).toBe(true); }); it('template literal does not halt when result is short', () => { expect(haltsWithQuota('return `hello ${"world"}`', 100n)).toBe(false); }); it('template literal with large interpolated value halts', () => { const val = 'a'.repeat(200); expect(haltsWithQuota('return `${val}`', 10n, { val })).toBe(true); }); it('template literal with no interpolation does not halt for short string', () => { expect(haltsWithQuota('return `hello world`', 100n)).toBe(false); }); it('template literal with multiple interpolations halts when total is long', () => { const a = 'x'.repeat(100); const b = 'y'.repeat(100); expect(haltsWithQuota('return `${a}${b}`', 10n, { a, b })).toBe(true); }); it('template literal with multiple interpolations does not halt when total is short', () => { expect(haltsWithQuota('return `${"hi"} ${"there"}`', 100n)).toBe(false); }); it('template literal with number interpolation does not halt when short', () => { expect(haltsWithQuota('return `value: ${42}`', 100n)).toBe(false); }); }); describe('String() cast ticks', () => { function haltsWithQuota( code: string, quota: bigint, scope: Record = {}, ): boolean { const sandbox = new Sandbox({ executionQuota: quota, haltOnSandboxError: true }); let halted = false; sandbox.subscribeHalt(() => { halted = true; }); sandbox.compile(code)(scope).run(); return halted; } it('String(s) halts when s is a long string', () => { const s = 'x'.repeat(200); expect(haltsWithQuota('return String(s)', 10n, { s })).toBe(true); }); it('String(s) does not halt when s is a short string', () => { expect(haltsWithQuota('return String(s)', 100n, { s: 'hello' })).toBe(false); }); it('String(n) does not halt for small number', () => { expect(haltsWithQuota('return String(42)', 50n)).toBe(false); }); it('String(true) does not halt (result is 4 chars)', () => { expect(haltsWithQuota('return String(true)', 50n)).toBe(false); }); it('String(null) does not halt (result is 4 chars)', () => { expect(haltsWithQuota('return String(null)', 50n)).toBe(false); }); it('String(undefined) does not halt (result is 9 chars)', () => { expect(haltsWithQuota('return String(undefined)', 50n)).toBe(false); }); it('String(obj) with object whose toString returns long string halts', () => { const obj = { toString: () => 'x'.repeat(200) }; expect(haltsWithQuota('return String(obj)', 10n, { obj })).toBe(true); }); }); ================================================ FILE: test/ticksQuotaHalt.spec.ts ================================================ import Sandbox from '../src/Sandbox'; import { SandboxExecutionQuotaExceededError } from '../src/utils'; describe('ticks quota halt', () => { it('should trigger halt subscription when tick quota is exceeded', async () => { const sandbox = new Sandbox({ haltOnSandboxError: true, executionQuota: BigInt(100), }); let haltCalled = false; sandbox.subscribeHalt((params) => { if (params.type === 'error' && params.error instanceof SandboxExecutionQuotaExceededError) { haltCalled = true; } }); const code = ` let str = ''; for (let i = 0; i < 1000; i++) { str += 'a'; } `; const fn = sandbox.compile(code); const { run } = fn(); run(); expect(haltCalled).toBe(true); }); it('should allow resuming after tick quota halt', async () => { const sandbox = new Sandbox({ haltOnSandboxError: true, executionQuota: BigInt(100), }); let haltCalled = false; sandbox.subscribeHalt((params) => { if (params.type === 'error' && params.error instanceof SandboxExecutionQuotaExceededError) { haltCalled = true; params.ticks.tickLimit = params.ticks.ticks + BigInt(10000); // Increase tick limit to allow resuming sandbox.resumeExecution(); } }); const code = ` let str = ''; for (let i = 0; i < 100; i++) { str += 'a'; } return str.length; `; const fn = sandbox.compileAsync(code); const { run } = fn(); const len = await run(); expect(haltCalled).toBe(true); expect(len).toBe(100); }); it('should halt sandbox A when execution quota is exceeded', (done) => { const globals = { ...Sandbox.SAFE_GLOBALS, setTimeout, clearTimeout }; const prototypeWhitelist = Sandbox.SAFE_PROTOTYPES; const sandboxA = new Sandbox({ globals, prototypeWhitelist, executionQuota: 50n, haltOnSandboxError: true, }); let haltedA = false; sandboxA.subscribeHalt(() => { haltedA = true; }); const sandboxB = new Sandbox({ globals, prototypeWhitelist }); // Sandbox A schedules a heavy string handler sandboxA .compile( 'setTimeout("let x=0; for (let i=0;i<200;i++){ x += i } globalThis.doneA = true;", 0);', )() .run(); // Run sandbox B before A's timer fires sandboxB.compile('1+1')().run(); setTimeout(() => { expect(haltedA).toBe(true); expect(sandboxA.context.sandboxGlobal.doneA).toBeUndefined(); done(); }, 50); }); it('should allow resuming after a quota halt in the middle of sync generator iteration', async () => { const sandbox = new Sandbox({ haltOnSandboxError: true, }); const progress: number[] = []; let haltCalled = false; const fn = sandbox.compileAsync(` function* gen(limit) { for (let i = 0; i < limit; i++) { let spin = 0; for (let j = 0; j < 40; j++) { spin += j; } yield i + spin * 0; } } for (const value of gen(5)) { progress.push(value); if (progress.length === 2) { armQuota(); } } return progress.slice(); `); const { context, run } = fn({ progress, armQuota: () => { context.ctx.ticks.tickLimit = context.ctx.ticks.ticks + 30n; }, }); sandbox.subscribeHalt((params) => { if (params.type === 'error' && params.error instanceof SandboxExecutionQuotaExceededError) { haltCalled = true; expect(progress).toEqual([0, 1]); params.ticks.tickLimit = params.ticks.ticks + 10000n; sandbox.resumeExecution(); } }); const values = await run(); expect(haltCalled).toBe(true); expect(values).toEqual([0, 1, 2, 3, 4]); expect(progress).toEqual([0, 1, 2, 3, 4]); }); it('should allow resuming async generator next() after a quota halt', async () => { const sandbox = new Sandbox({ haltOnSandboxError: true, }); let haltCalled = false; const fn = sandbox.compile(` async function* gen(limit) { for (let i = 0; i < limit; i++) { let spin = 0; for (let j = 0; j < 40; j++) { spin += j; } yield i + spin * 0; } } return gen(3); `); const { context, run } = fn(); const gen = run() as AsyncGenerator; expect(await gen.next()).toEqual({ value: 0, done: false }); sandbox.subscribeHalt((params) => { if (params.type === 'error' && params.error instanceof SandboxExecutionQuotaExceededError) { haltCalled = true; params.ticks.tickLimit = params.ticks.ticks + 10000n; sandbox.resumeExecution(); } }); context.ctx.ticks.tickLimit = context.ctx.ticks.ticks + 30n; expect(await gen.next()).toEqual({ value: 1, done: false }); expect(await gen.next()).toEqual({ value: 2, done: false }); expect(await gen.next()).toEqual({ value: undefined, done: true }); expect(haltCalled).toBe(true); }); it('should automatically halt and resume in nonBlocking mode', async () => { const sandbox = new Sandbox({ nonBlocking: true, }); let haltCount = 0; let resumeCount = 0; sandbox.subscribeHalt(() => { haltCount++; }); sandbox.subscribeResume(() => { resumeCount++; }); const fn = sandbox.compileAsync(` for (let i = 0; i < 20000; i++) { total += 1; } return total; `); const scope = { total: 0 }; const { run } = fn(scope); const res = run(); expect(scope.total).toBeLessThan(20000); expect(scope.total).toBeGreaterThan(0); const result = await res; expect(result).toBe(20000); expect(haltCount).toBeGreaterThan(0); expect(resumeCount).toBeGreaterThan(0); expect(resumeCount).toBe(haltCount); }); it('should automatically halt and resume in the middle of generator iteration in nonBlocking mode', async () => { const sandbox = new Sandbox({ nonBlocking: true, }); const progress: number[] = []; let haltCount = 0; let resumeCount = 0; sandbox.subscribeHalt(() => { haltCount++; }); sandbox.subscribeResume(() => { resumeCount++; }); const fn = sandbox.compileAsync(` function* gen(limit) { for (let i = 0; i < limit; i++) { let spin = 0; for (let j = 0; j < 60; j++) { spin += j; } yield i + spin * 0; } } for (const value of gen(300)) { progress.push(value); } return progress.slice(); `); const { run } = fn({ progress }); const values = await run(); expect(values).toHaveLength(300); expect(values[0]).toBe(0); expect(values[299]).toBe(299); expect(progress).toEqual(values); expect(haltCount).toBeGreaterThan(0); expect(resumeCount).toBeGreaterThan(0); expect(resumeCount).toBe(haltCount); }); it('should throw when executionQuota is exceeded without haltOnSandboxError', () => { const sandbox = new Sandbox({ executionQuota: BigInt(100), }); const code = ` let str = ''; for (let i = 0; i < 1000; i++) { str += 'a'; } `; const fn = sandbox.compile(code); const { run } = fn(); expect(() => run()).toThrow(SandboxExecutionQuotaExceededError); }); }); ================================================ FILE: test/timers.spec.ts ================================================ import Sandbox from '../src/Sandbox.js'; describe('Timer Tests', () => { describe('setTimeout', () => { it('should execute setTimeout callback', (done) => { const sandbox = new Sandbox({ globals: { setTimeout, }, }); let result = 0; const code = ` setTimeout(() => { result = 42; }, 10); `; const fn = sandbox.compile(code); const scope = { result }; const { context, run } = fn(scope); run(); setTimeout(() => { expect(scope.result).toBe(42); done(); }, 50); }); it('should clear setTimeout with clearTimeout', (done) => { const sandbox = new Sandbox({ globals: { setTimeout, clearTimeout, }, }); let result = 0; const code = ` const timeoutId = setTimeout(() => { result = 42; }, 10); clearTimeout(timeoutId); `; const fn = sandbox.compile(code); const scope = { result }; const { context, run } = fn(scope); run(); setTimeout(() => { expect(scope.result).toBe(0); done(); }, 50); }); }); describe('setInterval', () => { it('should execute setInterval callback multiple times', (done) => { const sandbox = new Sandbox({ globals: { setInterval, clearInterval, }, }); let count = 0; const code = ` const intervalId = setInterval(() => { count++; if (count >= 3) { clearInterval(intervalId); } }, 10); `; const fn = sandbox.compile(code); const scope = { count }; const { context, run } = fn(scope); run(); setTimeout(() => { expect(scope.count).toBeGreaterThanOrEqual(3); done(); }, 100); }); it('should clear setInterval with clearInterval', (done) => { const sandbox = new Sandbox({ globals: { setInterval, clearInterval, }, }); let count = 0; const code = ` const intervalId = setInterval(() => { count++; }, 10); clearInterval(intervalId); `; const fn = sandbox.compile(code); const scope = { count }; const { context, run } = fn(scope); run(); setTimeout(() => { expect(scope.count).toBe(0); done(); }, 50); }); }); describe('setTimeout/setInterval with string arguments', () => { it('should execute setTimeout with string code argument', (done) => { const sandbox = new Sandbox({ globals: { setTimeout, clearTimeout, }, }); let completed = false; const code = ` // String code executes in isolated scope setTimeout('1 + 1', 10); setTimeout(() => { completed = true; }, 30); `; const fn = sandbox.compile(code); const scope = { completed }; const { context, run } = fn(scope); run(); setTimeout(() => { expect(scope.completed).toBe(true); done(); }, 50); }); it('should execute setInterval with string code argument', (done) => { const sandbox = new Sandbox({ globals: { globalThis, setTimeout, setInterval, clearInterval, }, }); let completed = false; const code = ` globalThis.completed = completed; const intervalId = setInterval('this.completed = true', 10); setTimeout(() => { clearInterval(intervalId); completed = globalThis.completed; }, 55); `; const fn = sandbox.compile(code); const scope = { completed }; const { context, run } = fn(scope); run(); setTimeout(() => { expect(scope.completed).toBe(true); done(); }, 100); }); it('should clear setTimeout with string code before execution', (done) => { const sandbox = new Sandbox({ globals: { globalThis, setTimeout, clearTimeout, }, }); let completed = false; const code = ` const timeoutId = setTimeout('this.completed = 1 + 1', 10); clearTimeout(timeoutId); completed = globalThis.completed || false; `; const fn = sandbox.compile(code); const scope = { completed }; const { context, run } = fn(scope); run(); setTimeout(() => { expect(scope.completed).toBe(false); done(); }, 50); }); it('should execute string code with multiple statements', (done) => { const sandbox = new Sandbox({ globals: { globalThis, setTimeout, clearTimeout, }, }); let completed = false; const code = ` globalThis.completed = completed; setTimeout('let x = 5; let y = 10; this.completed = x + y', 10); setTimeout(() => { completed = globalThis.completed; }, 30); `; const fn = sandbox.compile(code); const scope = { completed }; const { context, run } = fn(scope); run(); setTimeout(() => { expect(scope.completed).toEqual(15); done(); }, 50); }); it('should execute string code with expressions', (done) => { const sandbox = new Sandbox({ globals: { setTimeout, clearTimeout, }, }); let completed = false; const code = ` setTimeout('2 + 3', 10); setTimeout(() => { completed = true; }, 30); `; const fn = sandbox.compile(code); const scope = { completed }; const { context, run } = fn(scope); run(); setTimeout(() => { expect(scope.completed).toBe(true); done(); }, 50); }); it('should handle empty string code', (done) => { const sandbox = new Sandbox({ globals: { setTimeout, clearTimeout, }, }); let completed = false; const code = ` setTimeout('', 10); setTimeout(() => { completed = true; }, 30); `; const fn = sandbox.compile(code); const scope = { completed }; const { context, run } = fn(scope); run(); setTimeout(() => { expect(scope.completed).toBe(true); done(); }, 50); }); }); }); ================================================ FILE: test/timersAsync.spec.ts ================================================ import Sandbox from '../src/Sandbox.js'; describe('Async Timer Tests', () => { describe('setTimeout with async execution', () => { it('should execute async setTimeout callback', async () => { const sandbox = new Sandbox(); let result = 0; // Provide a delay function to the sandbox const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); const code = ` await delay(50); result = 42; `; const fn = sandbox.compileAsync(code); const scope = { result, delay }; const { context, run } = fn(scope); await run(); expect(scope.result).toBe(42); }); it('should handle multiple async delays', async () => { const sandbox = new Sandbox(); let results: number[] = []; const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); const code = ` results.push(1); await delay(30); results.push(2); await delay(30); results.push(3); `; const fn = sandbox.compileAsync(code); const scope = { results, delay }; const { context, run } = fn(scope); await run(); expect(scope.results).toEqual([1, 2, 3]); }); it('should handle async setTimeout in sandbox', async () => { const sandbox = new Sandbox({ globals: { setTimeout }, }); let result = 0; const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); const code = ` setTimeout(async () => { await delay(30); result = 42; }, 20); `; const fn = sandbox.compileAsync(code); const scope = { result, delay }; const { context, run } = fn(scope); await run(); // Wait for the async callback to complete await new Promise((resolve) => setTimeout(resolve, 100)); expect(scope.result).toBe(42); }); }); }); ================================================ FILE: test/timersAsyncHalt.spec.ts ================================================ import Sandbox from '../src/Sandbox.js'; describe('Async Halt and Resume Tests', () => { describe('async execution with halt/resume', () => { it('should halt async execution and resume', async () => { const sandbox = new Sandbox(); let result = 0; let executionCompleted = false; const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); const code = ` result = 1; await delay(50); result = 2; await delay(50); result = 3; `; const fn = sandbox.compileAsync(code); const scope = { result, delay }; const { context, run } = fn(scope); // Start execution but don't wait const promise = run().then(() => { executionCompleted = true; }); // Wait a bit for first assignment await new Promise((resolve) => setTimeout(resolve, 30)); expect(scope.result).toBe(1); // Halt execution sandbox.haltExecution(); // Wait and verify execution is halted await new Promise((resolve) => setTimeout(resolve, 100)); expect(scope.result).toBe(1); // Should still be 1 expect(executionCompleted).toBe(false); // Resume execution sandbox.resumeExecution(); // Wait for completion await promise; expect(scope.result).toBe(3); expect(executionCompleted).toBe(true); }, 10000); it('should handle halt subscription during async execution', async () => { const sandbox = new Sandbox(); let haltCalled = false; sandbox.subscribeHalt(() => { haltCalled = true; }); const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); const code = ` await delay(50); `; const fn = sandbox.compileAsync(code); const scope = { delay }; const { context, run } = fn(scope); // Start execution const promise = run(); // Wait a bit then halt await new Promise((resolve) => setTimeout(resolve, 30)); sandbox.haltExecution(); expect(haltCalled).toBe(true); // Resume to allow cleanup sandbox.resumeExecution(); await promise; }); it('should handle resume subscription during async execution', async () => { const sandbox = new Sandbox(); let resumeCalled = false; sandbox.subscribeResume(() => { resumeCalled = true; }); const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); const code = ` await delay(50); `; const fn = sandbox.compileAsync(code); const scope = { delay }; const { context, run } = fn(scope); // Start execution const promise = run(); // Wait a bit then halt await new Promise((resolve) => setTimeout(resolve, 30)); sandbox.haltExecution(); // Resume await new Promise((resolve) => setTimeout(resolve, 20)); sandbox.resumeExecution(); expect(resumeCalled).toBe(true); // Wait for completion await promise; }); it('should pause execution on tick limit and resume', async () => { const sandbox = new Sandbox({ haltOnSandboxError: true }); let result = 0; const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); const code = ` for (let i = 0; i < 100; i++) { result = i; } await delay(10); result = 999; `; const fn = sandbox.compileAsync(code); const scope = { result, delay }; const { context, run } = fn(scope); // Set a tick limit context.ctx.ticks.tickLimit = 100n; let haltCalled = 0; sandbox.subscribeHalt(() => { haltCalled++; }); // Start execution - should halt due to tick limit const promise = run(); // Wait for halt await new Promise((resolve) => setTimeout(resolve, 100)); expect(haltCalled).toBe(1); // Resume execution context.ctx.ticks.tickLimit = undefined; sandbox.resumeExecution(); // Wait for completion await promise; expect(scope.result).toBe(999); }); }); }); ================================================ FILE: test/timersHalt.spec.ts ================================================ import Sandbox from '../src/Sandbox.js'; describe('Timer Halt and Resume Tests', () => { describe('setTimeout with halt/resume', () => { it('should pause setTimeout when halted and resume when continued', (done) => { const sandbox = new Sandbox({ globals: { setTimeout, }, }); let result = 0; const code = ` setTimeout(() => { result = 42; }, 50); `; const fn = sandbox.compile(code); const scope = { result }; const { context, run } = fn(scope); run(); // Halt immediately sandbox.haltExecution(); // Check that callback didn't execute during halt setTimeout(() => { expect(scope.result).toBe(0); // Resume execution sandbox.resumeExecution(); // Check that callback executes after resume setTimeout(() => { expect(scope.result).toBe(42); done(); }, 100); }, 100); }); it('should trigger halt subscription when halted', (done) => { const sandbox = new Sandbox({ globals: { setTimeout, }, }); let haltCalled = false; sandbox.subscribeHalt(() => { haltCalled = true; }); const code = ` setTimeout(() => {}, 100); `; const fn = sandbox.compile(code); const { context, run } = fn(); run(); sandbox.haltExecution(); setTimeout(() => { expect(haltCalled).toBe(true); done(); }, 50); }); it('should trigger resume subscription when resumed', (done) => { const sandbox = new Sandbox({ globals: { setTimeout, }, }); let resumeCalled = false; sandbox.subscribeResume(() => { resumeCalled = true; }); const code = ` setTimeout(() => {}, 100); `; const fn = sandbox.compile(code); const { context, run } = fn(); run(); sandbox.haltExecution(); setTimeout(() => { sandbox.resumeExecution(); expect(resumeCalled).toBe(true); done(); }, 50); }); }); describe('setInterval with halt/resume', () => { it('should pause setInterval when halted and resume when continued', (done) => { const sandbox = new Sandbox({ globals: { setInterval, clearInterval, }, }); let count = 0; const code = ` const intervalId = setInterval(() => { count++; if (count >= 5) { clearInterval(intervalId); } }, 20); `; const fn = sandbox.compile(code); const scope = { count }; const { context, run } = fn(scope); run(); // Let it run a bit setTimeout(() => { const countBeforeHalt = scope.count; sandbox.haltExecution(); // Wait during halt setTimeout(() => { // Count should not increase during halt expect(scope.count).toBe(countBeforeHalt); // Resume execution sandbox.resumeExecution(); // Wait for interval to complete setTimeout(() => { expect(scope.count).toBeGreaterThanOrEqual(5); done(); }, 200); }, 100); }, 50); }); }); }); ================================================ FILE: test/tryFinallyControlFlow.spec.ts ================================================ import Sandbox from '../src/Sandbox.js'; describe('Try/Finally Control Flow Tests', () => { let sandbox: Sandbox; beforeEach(() => { sandbox = new Sandbox(); }); describe('Finally with return overrides try/catch', () => { it('should return from finally instead of try', () => { const code = ` function test() { try { return 1; } finally { return 2; } } return test(); `; const result = sandbox.compile(code)().run(); expect(result).toBe(2); }); it('should return from finally instead of catch', () => { const code = ` function test() { try { throw new Error('test'); } catch (e) { return 1; } finally { return 2; } } return test(); `; const result = sandbox.compile(code)().run(); expect(result).toBe(2); }); }); describe('Finally with break overrides try/catch', () => { it('should break from finally instead of continuing try loop', () => { const code = ` let result = ''; for (let i = 0; i < 5; i++) { try { result += i; if (i === 2) continue; } finally { if (i === 2) break; } } return result; `; const result = sandbox.compile(code)().run(); // Should be '012' - breaks at i=2 in finally, ignoring try's continue expect(result).toBe('012'); }); it('should break from finally overriding try return in loop', () => { const code = ` function test() { let result = ''; for (let i = 0; i < 5; i++) { try { result += i; if (i === 2) return 'early'; } finally { if (i === 2) break; } } return result; } return test(); `; const result = sandbox.compile(code)().run(); // Finally's break overrides try's return, so function returns '012' not 'early' expect(result).toBe('012'); }); it('should break from finally overriding catch', () => { const code = ` let result = ''; for (let i = 0; i < 5; i++) { try { if (i === 2) throw new Error('test'); result += i; } catch (e) { result += 'E'; } finally { if (i === 2) break; } } return result; `; const result = sandbox.compile(code)().run(); // Should be '01E' - breaks at i=2 in finally after catch executes expect(result).toBe('01E'); }); }); describe('Finally with continue overrides try/catch', () => { it('should continue from finally instead of breaking from try', () => { const code = ` let result = ''; for (let i = 0; i < 5; i++) { try { result += i; if (i === 2) break; } finally { if (i === 2) continue; } } return result; `; const result = sandbox.compile(code)().run(); // Should be '01234' - finally's continue overrides try's break expect(result).toBe('01234'); }); it('should continue from finally overriding try return in loop', () => { const code = ` function test() { let result = ''; for (let i = 0; i < 5; i++) { try { result += i; if (i === 2) return 'early'; } finally { if (i === 2) continue; } } return result; } return test(); `; const result = sandbox.compile(code)().run(); // Finally's continue overrides try's return, continues loop expect(result).toBe('01234'); }); it('should continue from finally overriding catch', () => { const code = ` let result = ''; for (let i = 0; i < 5; i++) { try { if (i === 2) throw new Error('test'); result += i; } catch (e) { result += 'E'; break; } finally { if (i === 2) continue; } } return result; `; const result = sandbox.compile(code)().run(); // Should be '01E34' - finally's continue overrides catch's break expect(result).toBe('01E34'); }); }); describe('Normal finally behavior (no control flow in finally)', () => { it('should allow try return to pass through when finally has no control flow', () => { const code = ` function test() { try { return 1; } finally { let x = 2; // No control flow } } return test(); `; const result = sandbox.compile(code)().run(); expect(result).toBe(1); }); it('should allow try break to pass through', () => { const code = ` let result = ''; for (let i = 0; i < 5; i++) { try { result += i; if (i === 2) break; } finally { result += '-'; } } return result; `; const result = sandbox.compile(code)().run(); expect(result).toBe('0-1-2-'); }); it('should allow try continue to pass through', () => { const code = ` let result = ''; for (let i = 0; i < 5; i++) { try { result += i; if (i === 2) continue; result += 'X'; } finally { result += '-'; } } return result; `; const result = sandbox.compile(code)().run(); expect(result).toBe('0X-1X-2-3X-4X-'); }); }); describe('Finally with throw still overrides', () => { it('should throw from finally overriding try return', () => { const code = ` function test() { try { return 1; } finally { throw new Error('finally error'); } } try { return test(); } catch (e) { return e.message; } `; const result = sandbox.compile(code)().run(); expect(result).toBe('finally error'); }); it('should throw from finally overriding catch return', () => { const code = ` function test() { try { throw new Error('try error'); } catch (e) { return 'caught'; } finally { throw new Error('finally error'); } } try { return test(); } catch (e) { return e.message; } `; const result = sandbox.compile(code)().run(); expect(result).toBe('finally error'); }); }); describe('Nested try/finally', () => { it('should handle nested finally with break', () => { const code = ` let result = ''; for (let i = 0; i < 3; i++) { try { try { result += i; } finally { result += 'A'; if (i === 1) break; } } finally { result += 'B'; } } return result; `; const result = sandbox.compile(code)().run(); expect(result).toBe('0AB1AB'); }); }); }); ================================================ FILE: test/tsconfig.json ================================================ { "extends": "../tsconfig.json", "compilerOptions": { "types": ["jest", "node"], "resolveJsonModule": true, "rootDir": "..", "allowSyntheticDefaultImports": true }, "files": [], "include": ["**/*.ts", "../scripts/export-tests.ts"] } ================================================ FILE: test/unraw.spec.ts ================================================ import { unraw } from '../src/utils/unraw.js'; describe('unraw Tests', () => { describe('Basic escape sequences', () => { it('should handle single character escapes', () => { expect(unraw('\\n')).toBe('\n'); expect(unraw('\\r')).toBe('\r'); expect(unraw('\\t')).toBe('\t'); expect(unraw('\\b')).toBe('\b'); expect(unraw('\\f')).toBe('\f'); expect(unraw('\\v')).toBe('\v'); }); it('should handle backslash escape', () => { expect(unraw('\\\\')).toBe('\\'); }); it('should handle quote escapes', () => { expect(unraw("\\\'")).toBe("'"); expect(unraw('\\"')).toBe('"'); }); }); describe('Hexadecimal escape sequences', () => { it('should handle valid hexadecimal escapes', () => { expect(unraw('\\x41')).toBe('A'); expect(unraw('\\x42')).toBe('B'); expect(unraw('\\x30')).toBe('0'); }); it('should throw error for invalid hexadecimal escapes', () => { expect(() => unraw('\\xGG')).toThrow(SyntaxError); expect(() => unraw('\\xGG')).toThrow('Malformed Hexadecimal'); }); it('should throw error for wrong length hexadecimal escapes', () => { expect(() => unraw('\\x1')).toThrow(SyntaxError); expect(() => unraw('\\x1')).toThrow('Malformed Hexadecimal'); }); }); describe('Unicode escape sequences', () => { it('should handle valid 4-digit unicode escapes', () => { expect(unraw('\\u0041')).toBe('A'); expect(unraw('\\u4E2D')).toBe('中'); }); it('should throw error for invalid unicode escapes', () => { expect(() => unraw('\\uGGGG')).toThrow(SyntaxError); expect(() => unraw('\\uGGGG')).toThrow('Malformed Unicode'); }); it('should throw error for wrong length unicode escapes', () => { expect(() => unraw('\\u041')).toThrow(SyntaxError); expect(() => unraw('\\u041')).toThrow('Malformed Unicode'); }); it('should handle unicode with surrogate pairs', () => { // Test surrogate pair for emoji expect(unraw('\\uD83D\\uDE00')).toBe('😀'); expect(unraw('\\uD834\\uDD1E')).toBe('𝄞'); // Musical symbol }); }); describe('Unicode code point escape sequences', () => { it('should handle valid unicode code point escapes', () => { expect(unraw('\\u{41}')).toBe('A'); expect(unraw('\\u{1F600}')).toBe('😀'); expect(unraw('\\u{4E2D}')).toBe('中'); }); it('should throw error for malformed unicode code points (missing braces)', () => { expect(() => unraw('\\u{41')).toThrow(SyntaxError); expect(() => unraw('\\u{41')).toThrow('Malformed Unicode'); expect(() => unraw('\\u41}')).toThrow(SyntaxError); }); it('should throw error for invalid hex in unicode code points', () => { expect(() => unraw('\\u{GGGG}')).toThrow(SyntaxError); expect(() => unraw('\\u{GGGG}')).toThrow('Malformed Unicode'); }); it('should throw error for code points beyond limit', () => { // Test a code point beyond the valid range (> 0x10FFFF) expect(() => unraw('\\u{110000}')).toThrow(SyntaxError); expect(() => unraw('\\u{110000}')).toThrow('Code Point Limit'); }); }); describe('Octal escape sequences', () => { it('should handle null character \\0', () => { expect(unraw('\\0')).toBe('\0'); }); it('should throw error for deprecated octal escapes', () => { expect(() => unraw('\\1')).toThrow(SyntaxError); expect(() => unraw('\\1')).toThrow('Octal Deprecation'); expect(() => unraw('\\77')).toThrow(SyntaxError); expect(() => unraw('\\77')).toThrow('Octal Deprecation'); expect(() => unraw('\\123')).toThrow(SyntaxError); expect(() => unraw('\\123')).toThrow('Octal Deprecation'); }); }); describe('End of string handling', () => { it('should throw error for incomplete escape at end of string', () => { expect(() => unraw('\\')).toThrow(SyntaxError); expect(() => unraw('\\')).toThrow('End of string'); }); }); describe('Complex strings', () => { it('should handle mixed escape sequences', () => { expect(unraw('Hello\\nWorld\\t!')).toBe('Hello\nWorld\t!'); expect(unraw('\\x41\\u0042')).toBe('AB'); }); it('should handle strings with no escapes', () => { expect(unraw('Hello World')).toBe('Hello World'); }); it('should handle empty string', () => { expect(unraw('')).toBe(''); }); }); }); ================================================ FILE: tsconfig.jest.json ================================================ { "compilerOptions": { "module": "ES2020", "target": "ES2020", "rootDir": "src/", "baseUrl": ".", "strict": true, "moduleResolution": "node", "resolveJsonModule": true, "esModuleInterop": true, "noImplicitAny": false, "isolatedModules": true }, "files": ["src/SandboxExec.ts"], "include": ["src/**/*"], "exclude": [] } ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "module": "ES2020", "target": "ES2020", "moduleResolution": "node", "rootDir": "src/", "strict": true }, "files": ["src/SandboxExec.ts"], "include": ["src/**/*"], "exclude": [] }