feat(boc): Complete Business Operations Center v1.0

- Go backend API with full CRUD for all modules (CRM, Sales, Finance, HR, Legal, Marketing, Support, Purchase, Inventory, Projects, Automation, Analytics)
- Rust analytics service with parallel report generation
- C runtime with POSIX shared memory IPC
- PostgreSQL schema with 30+ tables, full migrations
- Redis cache, sessions, pub/sub
- Kafka event streaming with Zookeeper
- WebSocket hub for real-time updates
- Automation engine with cron jobs, workflows, event triggers
- JWT authentication, multi-tenant from start
- Docker Compose with all services
- Nginx reverse proxy with rate limiting
- Integration tests passing
- Feature gap analysis against Fortnox/Odoo/Visma

Refs: BOC-001
This commit is contained in:
Bernt
2026-07-12 12:41:35 +00:00
parent 4789a7fb48
commit 58ca4e68db
26666 changed files with 575891 additions and 2074516 deletions
File diff suppressed because one or more lines are too long
@@ -0,0 +1,719 @@
{
"date": "2026-07-04T09:15:24.678Z",
"argv": [
"/usr/local/bin/node",
"/workspace/test/index.js"
],
"execArgv": [
"--import=file:///workspace/node_modules/@tapjs/typescript/dist/esm/import.mjs",
"--import=file:///workspace/node_modules/@tapjs/mock/dist/esm/import.mjs",
"--enable-source-maps",
"--import=file:///workspace/node_modules/@tapjs/processinfo/dist/esm/import.mjs"
],
"NODE_OPTIONS": "\"--import=file:///workspace/node_modules/@tapjs/processinfo/dist/esm/import.mjs\"",
"cwd": "/workspace",
"pid": 814,
"ppid": 803,
"parent": null,
"uuid": "8b906b8c-e21f-40b5-8c0d-214d8c2682c5",
"files": [
"/workspace/test/index.js",
"/workspace/node_modules/@tapjs/typescript/dist/esm/import.mjs",
"/workspace/node_modules/@isaacs/ts-node-temp-fork-for-pr-2009/import.mjs",
"/workspace/node_modules/@isaacs/ts-node-temp-fork-for-pr-2009/import-loader.mjs",
"/workspace/node_modules/@tapjs/mock/dist/esm/import.mjs",
"/workspace/node_modules/@tapjs/mock/dist/esm/mock-service.js",
"/workspace/node_modules/@tapjs/core/dist/esm/index.js",
"/workspace/node_modules/@tapjs/core/dist/esm/base.js",
"/workspace/node_modules/@tapjs/core/dist/esm/counts.js",
"/workspace/node_modules/@tapjs/core/dist/esm/extra-from-error.js",
"/workspace/node_modules/resolve-import/dist/esm/resolve-import-sync.min.js",
"/workspace/node_modules/resolve-import/dist/esm/is-relative-require.js",
"/workspace/node_modules/@tapjs/mock/dist/esm/resolve-mock-entry-point.js",
"/workspace/node_modules/@tapjs/mock/dist/esm/export-line.js",
"/workspace/node_modules/@tapjs/mock/dist/esm/service-key.js",
"/workspace/node_modules/@tapjs/mock/dist/esm/munge-mocks.js",
"/workspace/node_modules/@tapjs/stack/dist/esm/index.js",
"/workspace/node_modules/@tapjs/core/dist/esm/lists.js",
"/workspace/node_modules/@tapjs/core/dist/esm/main-script.js",
"/workspace/node_modules/@tapjs/core/dist/esm/minimal.js",
"/workspace/node_modules/@tapjs/core/dist/esm/normalize-message-extra.js",
"/workspace/node_modules/@tapjs/core/dist/esm/parse-test-args.js",
"/workspace/node_modules/@tapjs/core/dist/esm/proc.js",
"/workspace/node_modules/@tapjs/core/dist/esm/tap-file.js",
"/workspace/node_modules/@tapjs/core/dist/esm/spawn.js",
"/workspace/node_modules/@tapjs/core/dist/esm/stdin.js",
"/workspace/node_modules/@tapjs/core/dist/esm/use-sync-hooks.js",
"/workspace/node_modules/@tapjs/core/dist/esm/waiter.js",
"/workspace/node_modules/@tapjs/core/dist/esm/tap-dir.js",
"/workspace/node_modules/@tapjs/core/dist/esm/test-base.js",
"/workspace/node_modules/@tapjs/core/dist/esm/tap.js",
"/workspace/node_modules/@tapjs/core/dist/esm/worker.js",
"/workspace/node_modules/@tapjs/core/dist/esm/test-point.js",
"/workspace/node_modules/async-hook-domain/dist/mjs/index.js",
"/workspace/node_modules/minipass/dist/esm/index.js",
"/workspace/node_modules/tap-parser/dist/esm/index.js",
"/workspace/node_modules/@tapjs/core/dist/esm/diags.js",
"/workspace/node_modules/@tapjs/core/dist/esm/message-from-error.js",
"/workspace/node_modules/resolve-import/dist/esm/is-windows.js",
"/workspace/node_modules/@tapjs/stack/dist/esm/call-site-like.js",
"/workspace/node_modules/@tapjs/stack/dist/esm/require-resolve.js",
"/workspace/node_modules/@tapjs/processinfo/dist/esm/index.js",
"/workspace/node_modules/@tapjs/core/dist/esm/throw-to-parser.js",
"/workspace/node_modules/@tapjs/core/dist/esm/esc.js",
"/workspace/node_modules/@tapjs/test/dist/esm/index.js",
"/workspace/node_modules/@tapjs/core/dist/esm/implicit-end-sigil.js",
"/workspace/node_modules/is-actual-promise/dist/esm/index.js",
"/workspace/node_modules/trivial-deferred/dist/mjs/index.js",
"/workspace/node_modules/tap-yaml/dist/esm/index.js",
"/workspace/node_modules/tap-parser/dist/esm/final-results.js",
"/workspace/node_modules/tap-parser/dist/esm/plan.js",
"/workspace/node_modules/tap-parser/dist/esm/line-type.js",
"/workspace/node_modules/tap-parser/dist/esm/parse-directive.js",
"/workspace/node_modules/tap-parser/dist/esm/result.js",
"/workspace/node_modules/tap-parser/dist/esm/statics.js",
"/workspace/node_modules/@tapjs/core/dist/esm/clean-yaml-object.js",
"/workspace/node_modules/tap-parser/dist/esm/escape.js",
"/workspace/node_modules/@tapjs/stack/dist/esm/parse.js",
"/workspace/node_modules/@tapjs/processinfo/dist/esm/child_process.js",
"/workspace/node_modules/@tapjs/processinfo/dist/esm/process-info-node.js",
"/workspace/node_modules/@tapjs/processinfo/dist/esm/json-file.js",
"/workspace/node_modules/@tapjs/test/test-built/dist/esm/index.js",
"/workspace/node_modules/tap-parser/dist/esm/final-plan.js",
"/workspace/node_modules/tap-yaml/dist/esm/parse.js",
"/workspace/node_modules/tap-yaml/dist/esm/stringify.js",
"/workspace/node_modules/@tapjs/test/dist/esm/default-plugins.js",
"/workspace/node_modules/tap-parser/dist/esm/brace-patterns.js",
"/workspace/node_modules/diff/libesm/index.js",
"/workspace/node_modules/tcompare/dist/esm/index.js",
"/workspace/node_modules/@tapjs/processinfo/dist/esm/spawn-opts.js",
"/workspace/node_modules/@tapjs/after/dist/esm/index.js",
"/workspace/node_modules/@tapjs/after-each/dist/esm/index.js",
"/workspace/node_modules/@tapjs/asserts/dist/esm/index.js",
"/workspace/node_modules/@tapjs/before/dist/esm/index.js",
"/workspace/node_modules/@tapjs/before-each/dist/esm/index.js",
"/workspace/node_modules/@tapjs/chdir/dist/esm/index.js",
"/workspace/node_modules/@tapjs/filter/dist/esm/index.js",
"/workspace/node_modules/@tapjs/fixture/dist/esm/index.js",
"/workspace/node_modules/@tapjs/intercept/dist/esm/index.js",
"/workspace/node_modules/@tapjs/mock/dist/esm/index.js",
"/workspace/node_modules/@tapjs/node-serialize/dist/esm/index.js",
"/workspace/node_modules/@tapjs/snapshot/dist/esm/index.js",
"/workspace/node_modules/@tapjs/spawn/dist/esm/index.js",
"/workspace/node_modules/@tapjs/stdin/dist/esm/index.js",
"/workspace/node_modules/@tapjs/typescript/dist/esm/index.js",
"/workspace/node_modules/@tapjs/worker/dist/esm/index.js",
"/workspace/node_modules/jackspeak/dist/esm/index.js",
"/workspace/node_modules/tap-yaml/dist/esm/types/index.js",
"/workspace/node_modules/diff/libesm/patch/parse.js",
"/workspace/node_modules/diff/libesm/patch/reverse.js",
"/workspace/node_modules/diff/libesm/patch/create.js",
"/workspace/node_modules/diff/libesm/convert/dmp.js",
"/workspace/node_modules/diff/libesm/convert/xml.js",
"/workspace/node_modules/diff/libesm/diff/base.js",
"/workspace/node_modules/diff/libesm/diff/character.js",
"/workspace/node_modules/diff/libesm/diff/word.js",
"/workspace/node_modules/diff/libesm/diff/line.js",
"/workspace/node_modules/diff/libesm/diff/sentence.js",
"/workspace/node_modules/diff/libesm/diff/css.js",
"/workspace/node_modules/diff/libesm/diff/array.js",
"/workspace/node_modules/diff/libesm/diff/json.js",
"/workspace/node_modules/diff/libesm/patch/apply.js",
"/workspace/node_modules/tcompare/dist/esm/format.js",
"/workspace/node_modules/tcompare/dist/esm/has-strict.js",
"/workspace/node_modules/tcompare/dist/esm/has.js",
"/workspace/node_modules/tcompare/dist/esm/match-strict.js",
"/workspace/node_modules/tcompare/dist/esm/match-only.js",
"/workspace/node_modules/tcompare/dist/esm/match-only-strict.js",
"/workspace/node_modules/tcompare/dist/esm/match.js",
"/workspace/node_modules/tcompare/dist/esm/strict.js",
"/workspace/node_modules/tcompare/dist/esm/styles.js",
"/workspace/node_modules/tcompare/dist/esm/same.js",
"/workspace/node_modules/function-loop/dist/mjs/index.js",
"/workspace/node_modules/@tapjs/asserts/dist/esm/normalize-throws-args.js",
"/workspace/node_modules/rimraf/dist/esm/index.js",
"/workspace/node_modules/@tapjs/fixture/dist/esm/fixture.js",
"/workspace/node_modules/@tapjs/node-serialize/dist/esm/serialize.js",
"/workspace/node_modules/@tapjs/mock/dist/esm/mock-require.js",
"/workspace/node_modules/@tapjs/snapshot/dist/esm/clean-cwd.js",
"/workspace/node_modules/@tapjs/snapshot/dist/esm/provider.js",
"/workspace/node_modules/@isaacs/cliui/dist/esm/index.min.js",
"/workspace/node_modules/tap-yaml/dist/esm/types/date.js",
"/workspace/node_modules/tap-yaml/dist/esm/types/timestamp.js",
"/workspace/node_modules/diff/libesm/util/string.js",
"/workspace/node_modules/diff/libesm/util/distance-iterator.js",
"/workspace/node_modules/diff/libesm/patch/line-endings.js",
"/workspace/node_modules/diff/libesm/util/params.js",
"/workspace/node_modules/tcompare/dist/esm/react-element-to-jsx-string.js",
"/workspace/node_modules/rimraf/dist/esm/opt-arg.js",
"/workspace/node_modules/glob/dist/esm/index.min.js",
"/workspace/node_modules/rimraf/dist/esm/path-arg.js",
"/workspace/node_modules/rimraf/dist/esm/rimraf-manual.js",
"/workspace/node_modules/rimraf/dist/esm/rimraf-move-remove.js",
"/workspace/node_modules/rimraf/dist/esm/rimraf-native.js",
"/workspace/node_modules/rimraf/dist/esm/rimraf-posix.js",
"/workspace/node_modules/rimraf/dist/esm/rimraf-windows.js",
"/workspace/node_modules/rimraf/dist/esm/use-native.js",
"/workspace/node_modules/mkdirp/dist/mjs/index.js",
"/workspace/node_modules/walk-up-path/dist/esm/index.js",
"/workspace/node_modules/@tapjs/error-serdes/dist/esm/index.js",
"/workspace/node_modules/@tapjs/node-serialize/dist/esm/comment.js",
"/workspace/node_modules/@tapjs/node-serialize/dist/esm/on-add.js",
"/workspace/node_modules/@tapjs/node-serialize/dist/esm/print-messages.js",
"/workspace/node_modules/@tapjs/node-serialize/dist/esm/stdio.js",
"/workspace/node_modules/@tapjs/snapshot/dist/esm/require.js",
"/workspace/node_modules/@tapjs/node-serialize/dist/esm/test-map.js",
"/workspace/node_modules/mkdirp/dist/mjs/mkdirp-manual.js",
"/workspace/node_modules/mkdirp/dist/mjs/opts-arg.js",
"/workspace/node_modules/mkdirp/dist/mjs/mkdirp-native.js",
"/workspace/node_modules/rimraf/dist/esm/default-tmp.js",
"/workspace/node_modules/mkdirp/dist/mjs/path-arg.js",
"/workspace/node_modules/mkdirp/dist/mjs/use-native.js",
"/workspace/node_modules/rimraf/dist/esm/ignore-enoent.js",
"/workspace/node_modules/rimraf/dist/esm/readdir-or-error.js",
"/workspace/node_modules/rimraf/dist/esm/fix-eperm.js",
"/workspace/node_modules/rimraf/dist/esm/error.js",
"/workspace/node_modules/rimraf/dist/esm/retry-busy.js",
"/workspace/node_modules/rimraf/dist/esm/fs.js",
"/workspace/node_modules/@tapjs/error-serdes/dist/esm/test-stream-serialize.js",
"/workspace/node_modules/@tapjs/error-serdes/dist/esm/serialize.js",
"/workspace/node_modules/@tapjs/error-serdes/dist/esm/deserialize.js",
"/workspace/node_modules/@tapjs/error-serdes/dist/esm/test-stream-deserialize.js",
"/workspace/node_modules/@tapjs/node-serialize/dist/esm/test-nested-location.js",
"/workspace/node_modules/@tapjs/node-serialize/dist/esm/loc-from-at.js",
"/workspace/node_modules/@tapjs/error-serdes/dist/esm/constants.js",
"/workspace/node_modules/@tapjs/error-serdes/dist/esm/messages.js",
"/workspace/node_modules/@tapjs/node-serialize/dist/esm/test-results.js",
"/workspace/node_modules/@tapjs/node-serialize/dist/esm/test-point-message-data.js",
"/workspace/node_modules/@tapjs/node-serialize/dist/esm/test-message-data.js",
"/workspace/node_modules/@tapjs/node-serialize/dist/esm/test-point-results.js",
"/workspace/node_modules/@tapjs/node-serialize/dist/esm/loc-from-callsite.js",
"/workspace/node_modules/mkdirp/dist/mjs/find-made.js",
"/workspace/node_modules/@tapjs/node-serialize/dist/esm/result-to-error.js",
"/workspace/node_modules/@tapjs/node-serialize/dist/esm/pretty-diff.js",
"/workspace/node_modules/@tapjs/mock/dist/esm/loader.mjs",
"/workspace/node_modules/@tapjs/mock/dist/esm/hooks.mjs",
"/workspace/node_modules/@tapjs/mock/dist/esm/mock-service-client.js",
"/workspace/node_modules/tap/dist/esm/index.js",
"/workspace/dist/esm/index.js",
"/workspace/node_modules/tap/dist/esm/main.js",
"/workspace/node_modules/balanced-match/dist/esm/index.js"
],
"sources": {
"/workspace/node_modules/@tapjs/typescript/dist/esm/import.mjs": [
"/workspace/node_modules/@tapjs/typescript/src/import.mts"
],
"/workspace/node_modules/@tapjs/mock/dist/esm/import.mjs": [
"/workspace/node_modules/@tapjs/mock/src/import.mts"
],
"/workspace/node_modules/@tapjs/mock/dist/esm/mock-service.js": [
"/workspace/node_modules/@tapjs/mock/src/mock-service.ts"
],
"/workspace/node_modules/@tapjs/core/dist/esm/index.js": [
"/workspace/node_modules/@tapjs/core/src/index.ts"
],
"/workspace/node_modules/@tapjs/core/dist/esm/base.js": [
"/workspace/node_modules/@tapjs/core/src/base.ts"
],
"/workspace/node_modules/@tapjs/core/dist/esm/counts.js": [
"/workspace/node_modules/@tapjs/core/src/counts.ts"
],
"/workspace/node_modules/@tapjs/core/dist/esm/extra-from-error.js": [
"/workspace/node_modules/@tapjs/core/src/extra-from-error.ts"
],
"/workspace/node_modules/resolve-import/dist/esm/resolve-import-sync.min.js": [
"/workspace/node_modules/resolve-import/src/resolve-import-sync.ts",
"/workspace/node_modules/resolve-import/src/resolve-import-async.ts",
"/workspace/node_modules/resolve-import/src/file-exists.ts",
"/workspace/node_modules/resolve-import/src/is-windows.ts",
"/workspace/node_modules/resolve-import/src/is-relative-require.ts",
"/workspace/node_modules/resolve-import/src/resolve-dependency-export.ts",
"/workspace/node_modules/resolve-import/src/find-dep-package.ts",
"/workspace/node_modules/resolve-import/node_modules/walk-up-path/src/index.ts",
"/workspace/node_modules/resolve-import/src/read-json.ts",
"/workspace/node_modules/resolve-import/src/read-pkg.ts",
"/workspace/node_modules/resolve-import/src/find-star-match.ts",
"/workspace/node_modules/resolve-import/src/resolve-conditional-value.ts",
"/workspace/node_modules/resolve-import/src/resolve-export.ts",
"/workspace/node_modules/resolve-import/src/resolve-dependency-export-sync.ts",
"/workspace/node_modules/resolve-import/src/resolve-package-import.ts",
"/workspace/node_modules/resolve-import/src/resolve-package-import-sync.ts",
"/workspace/node_modules/resolve-import/src/to-file-url.ts",
"/workspace/node_modules/resolve-import/src/to-path.ts",
"/workspace/node_modules/resolve-import/src/errors.ts"
],
"/workspace/node_modules/resolve-import/dist/esm/is-relative-require.js": [
"/workspace/node_modules/resolve-import/src/is-relative-require.ts"
],
"/workspace/node_modules/@tapjs/mock/dist/esm/resolve-mock-entry-point.js": [
"/workspace/node_modules/@tapjs/mock/src/resolve-mock-entry-point.ts"
],
"/workspace/node_modules/@tapjs/mock/dist/esm/export-line.js": [
"/workspace/node_modules/@tapjs/mock/src/export-line.ts"
],
"/workspace/node_modules/@tapjs/mock/dist/esm/service-key.js": [
"/workspace/node_modules/@tapjs/mock/src/service-key.ts"
],
"/workspace/node_modules/@tapjs/mock/dist/esm/munge-mocks.js": [
"/workspace/node_modules/@tapjs/mock/src/munge-mocks.ts"
],
"/workspace/node_modules/@tapjs/stack/dist/esm/index.js": [
"/workspace/node_modules/@tapjs/stack/src/index.ts"
],
"/workspace/node_modules/@tapjs/core/dist/esm/lists.js": [
"/workspace/node_modules/@tapjs/core/src/lists.ts"
],
"/workspace/node_modules/@tapjs/core/dist/esm/main-script.js": [
"/workspace/node_modules/@tapjs/core/src/main-script.ts"
],
"/workspace/node_modules/@tapjs/core/dist/esm/minimal.js": [
"/workspace/node_modules/@tapjs/core/src/minimal.ts"
],
"/workspace/node_modules/@tapjs/core/dist/esm/normalize-message-extra.js": [
"/workspace/node_modules/@tapjs/core/src/normalize-message-extra.ts"
],
"/workspace/node_modules/@tapjs/core/dist/esm/parse-test-args.js": [
"/workspace/node_modules/@tapjs/core/src/parse-test-args.ts"
],
"/workspace/node_modules/@tapjs/core/dist/esm/proc.js": [
"/workspace/node_modules/@tapjs/core/src/proc.ts"
],
"/workspace/node_modules/@tapjs/core/dist/esm/tap-file.js": [
"/workspace/node_modules/@tapjs/core/src/tap-file.ts"
],
"/workspace/node_modules/@tapjs/core/dist/esm/spawn.js": [
"/workspace/node_modules/@tapjs/core/src/spawn.ts"
],
"/workspace/node_modules/@tapjs/core/dist/esm/stdin.js": [
"/workspace/node_modules/@tapjs/core/src/stdin.ts"
],
"/workspace/node_modules/@tapjs/core/dist/esm/use-sync-hooks.js": [
"/workspace/node_modules/@tapjs/core/src/use-sync-hooks.ts"
],
"/workspace/node_modules/@tapjs/core/dist/esm/waiter.js": [
"/workspace/node_modules/@tapjs/core/src/waiter.ts"
],
"/workspace/node_modules/@tapjs/core/dist/esm/tap-dir.js": [
"/workspace/node_modules/@tapjs/core/src/tap-dir.ts"
],
"/workspace/node_modules/@tapjs/core/dist/esm/test-base.js": [
"/workspace/node_modules/@tapjs/core/src/test-base.ts"
],
"/workspace/node_modules/@tapjs/core/dist/esm/tap.js": [
"/workspace/node_modules/@tapjs/core/src/tap.ts"
],
"/workspace/node_modules/@tapjs/core/dist/esm/worker.js": [
"/workspace/node_modules/@tapjs/core/src/worker.ts"
],
"/workspace/node_modules/@tapjs/core/dist/esm/test-point.js": [
"/workspace/node_modules/@tapjs/core/src/test-point.ts"
],
"/workspace/node_modules/async-hook-domain/dist/mjs/index.js": [
"/workspace/node_modules/async-hook-domain/src/index.ts"
],
"/workspace/node_modules/minipass/dist/esm/index.js": [
"/workspace/node_modules/minipass/src/index.ts"
],
"/workspace/node_modules/tap-parser/dist/esm/index.js": [
"/workspace/node_modules/tap-parser/src/index.ts"
],
"/workspace/node_modules/@tapjs/core/dist/esm/diags.js": [
"/workspace/node_modules/@tapjs/core/src/diags.ts"
],
"/workspace/node_modules/@tapjs/core/dist/esm/message-from-error.js": [
"/workspace/node_modules/@tapjs/core/src/message-from-error.ts"
],
"/workspace/node_modules/resolve-import/dist/esm/is-windows.js": [
"/workspace/node_modules/resolve-import/src/is-windows.ts"
],
"/workspace/node_modules/@tapjs/stack/dist/esm/call-site-like.js": [
"/workspace/node_modules/@tapjs/stack/src/call-site-like.ts"
],
"/workspace/node_modules/@tapjs/stack/dist/esm/require-resolve.js": [
"/workspace/node_modules/@tapjs/stack/src/require-resolve.ts"
],
"/workspace/node_modules/@tapjs/processinfo/dist/esm/index.js": [
"/workspace/node_modules/@tapjs/processinfo/src/index.ts"
],
"/workspace/node_modules/@tapjs/core/dist/esm/throw-to-parser.js": [
"/workspace/node_modules/@tapjs/core/src/throw-to-parser.ts"
],
"/workspace/node_modules/@tapjs/core/dist/esm/esc.js": [
"/workspace/node_modules/@tapjs/core/src/esc.ts"
],
"/workspace/node_modules/@tapjs/test/dist/esm/index.js": [
"/workspace/node_modules/@tapjs/test/src/index.ts"
],
"/workspace/node_modules/@tapjs/core/dist/esm/implicit-end-sigil.js": [
"/workspace/node_modules/@tapjs/core/src/implicit-end-sigil.ts"
],
"/workspace/node_modules/is-actual-promise/dist/esm/index.js": [
"/workspace/node_modules/is-actual-promise/src/index.ts"
],
"/workspace/node_modules/trivial-deferred/dist/mjs/index.js": [
"/workspace/node_modules/trivial-deferred/src/index.ts"
],
"/workspace/node_modules/tap-yaml/dist/esm/index.js": [
"/workspace/node_modules/tap-yaml/src/index.ts"
],
"/workspace/node_modules/tap-parser/dist/esm/final-results.js": [
"/workspace/node_modules/tap-parser/src/final-results.ts"
],
"/workspace/node_modules/tap-parser/dist/esm/plan.js": [
"/workspace/node_modules/tap-parser/src/plan.ts"
],
"/workspace/node_modules/tap-parser/dist/esm/line-type.js": [
"/workspace/node_modules/tap-parser/src/line-type.ts"
],
"/workspace/node_modules/tap-parser/dist/esm/parse-directive.js": [
"/workspace/node_modules/tap-parser/src/parse-directive.ts"
],
"/workspace/node_modules/tap-parser/dist/esm/result.js": [
"/workspace/node_modules/tap-parser/src/result.ts"
],
"/workspace/node_modules/tap-parser/dist/esm/statics.js": [
"/workspace/node_modules/tap-parser/src/statics.ts"
],
"/workspace/node_modules/@tapjs/core/dist/esm/clean-yaml-object.js": [
"/workspace/node_modules/@tapjs/core/src/clean-yaml-object.ts"
],
"/workspace/node_modules/tap-parser/dist/esm/escape.js": [
"/workspace/node_modules/tap-parser/src/escape.ts"
],
"/workspace/node_modules/@tapjs/stack/dist/esm/parse.js": [
"/workspace/node_modules/@tapjs/stack/src/parse.ts"
],
"/workspace/node_modules/@tapjs/processinfo/dist/esm/child_process.js": [
"/workspace/node_modules/@tapjs/processinfo/src/child_process.ts"
],
"/workspace/node_modules/@tapjs/processinfo/dist/esm/process-info-node.js": [
"/workspace/node_modules/@tapjs/processinfo/src/process-info-node.ts"
],
"/workspace/node_modules/@tapjs/processinfo/dist/esm/json-file.js": [
"/workspace/node_modules/@tapjs/processinfo/src/json-file.ts"
],
"/workspace/node_modules/@tapjs/test/test-built/dist/esm/index.js": [
"/workspace/node_modules/@tapjs/test/test-built/src/index.ts"
],
"/workspace/node_modules/tap-parser/dist/esm/final-plan.js": [
"/workspace/node_modules/tap-parser/src/final-plan.ts"
],
"/workspace/node_modules/tap-yaml/dist/esm/parse.js": [
"/workspace/node_modules/tap-yaml/src/parse.ts"
],
"/workspace/node_modules/tap-yaml/dist/esm/stringify.js": [
"/workspace/node_modules/tap-yaml/src/stringify.ts"
],
"/workspace/node_modules/@tapjs/test/dist/esm/default-plugins.js": [
"/workspace/node_modules/@tapjs/test/src/default-plugins.ts"
],
"/workspace/node_modules/tap-parser/dist/esm/brace-patterns.js": [
"/workspace/node_modules/tap-parser/src/brace-patterns.ts"
],
"/workspace/node_modules/tcompare/dist/esm/index.js": [
"/workspace/node_modules/tcompare/src/index.ts"
],
"/workspace/node_modules/@tapjs/processinfo/dist/esm/spawn-opts.js": [
"/workspace/node_modules/@tapjs/processinfo/src/spawn-opts.ts"
],
"/workspace/node_modules/@tapjs/after/dist/esm/index.js": [
"/workspace/node_modules/@tapjs/after/src/index.ts"
],
"/workspace/node_modules/@tapjs/after-each/dist/esm/index.js": [
"/workspace/node_modules/@tapjs/after-each/src/index.ts"
],
"/workspace/node_modules/@tapjs/asserts/dist/esm/index.js": [
"/workspace/node_modules/@tapjs/asserts/src/index.ts"
],
"/workspace/node_modules/@tapjs/before/dist/esm/index.js": [
"/workspace/node_modules/@tapjs/before/src/index.ts"
],
"/workspace/node_modules/@tapjs/before-each/dist/esm/index.js": [
"/workspace/node_modules/@tapjs/before-each/src/index.ts"
],
"/workspace/node_modules/@tapjs/chdir/dist/esm/index.js": [
"/workspace/node_modules/@tapjs/chdir/src/index.ts"
],
"/workspace/node_modules/@tapjs/filter/dist/esm/index.js": [
"/workspace/node_modules/@tapjs/filter/src/index.ts"
],
"/workspace/node_modules/@tapjs/fixture/dist/esm/index.js": [
"/workspace/node_modules/@tapjs/fixture/src/index.ts"
],
"/workspace/node_modules/@tapjs/intercept/dist/esm/index.js": [
"/workspace/node_modules/@tapjs/intercept/src/index.ts"
],
"/workspace/node_modules/@tapjs/mock/dist/esm/index.js": [
"/workspace/node_modules/@tapjs/mock/src/index.ts"
],
"/workspace/node_modules/@tapjs/node-serialize/dist/esm/index.js": [
"/workspace/node_modules/@tapjs/node-serialize/src/index.ts"
],
"/workspace/node_modules/@tapjs/snapshot/dist/esm/index.js": [
"/workspace/node_modules/@tapjs/snapshot/src/index.ts"
],
"/workspace/node_modules/@tapjs/spawn/dist/esm/index.js": [
"/workspace/node_modules/@tapjs/spawn/src/index.ts"
],
"/workspace/node_modules/@tapjs/stdin/dist/esm/index.js": [
"/workspace/node_modules/@tapjs/stdin/src/index.ts"
],
"/workspace/node_modules/@tapjs/typescript/dist/esm/index.js": [
"/workspace/node_modules/@tapjs/typescript/src/index.ts"
],
"/workspace/node_modules/@tapjs/worker/dist/esm/index.js": [
"/workspace/node_modules/@tapjs/worker/src/index.ts"
],
"/workspace/node_modules/jackspeak/dist/esm/index.js": [
"/workspace/node_modules/jackspeak/src/index.ts"
],
"/workspace/node_modules/tap-yaml/dist/esm/types/index.js": [
"/workspace/node_modules/tap-yaml/src/types/index.ts"
],
"/workspace/node_modules/tcompare/dist/esm/format.js": [
"/workspace/node_modules/tcompare/src/format.ts"
],
"/workspace/node_modules/tcompare/dist/esm/has-strict.js": [
"/workspace/node_modules/tcompare/src/has-strict.ts"
],
"/workspace/node_modules/tcompare/dist/esm/has.js": [
"/workspace/node_modules/tcompare/src/has.ts"
],
"/workspace/node_modules/tcompare/dist/esm/match-strict.js": [
"/workspace/node_modules/tcompare/src/match-strict.ts"
],
"/workspace/node_modules/tcompare/dist/esm/match-only.js": [
"/workspace/node_modules/tcompare/src/match-only.ts"
],
"/workspace/node_modules/tcompare/dist/esm/match-only-strict.js": [
"/workspace/node_modules/tcompare/src/match-only-strict.ts"
],
"/workspace/node_modules/tcompare/dist/esm/match.js": [
"/workspace/node_modules/tcompare/src/match.ts"
],
"/workspace/node_modules/tcompare/dist/esm/strict.js": [
"/workspace/node_modules/tcompare/src/strict.ts"
],
"/workspace/node_modules/tcompare/dist/esm/styles.js": [
"/workspace/node_modules/tcompare/src/styles.ts"
],
"/workspace/node_modules/tcompare/dist/esm/same.js": [
"/workspace/node_modules/tcompare/src/same.ts"
],
"/workspace/node_modules/function-loop/dist/mjs/index.js": [
"/workspace/node_modules/function-loop/src/index.ts"
],
"/workspace/node_modules/@tapjs/asserts/dist/esm/normalize-throws-args.js": [
"/workspace/node_modules/@tapjs/asserts/src/normalize-throws-args.ts"
],
"/workspace/node_modules/rimraf/dist/esm/index.js": [
"/workspace/node_modules/rimraf/src/index.ts"
],
"/workspace/node_modules/@tapjs/fixture/dist/esm/fixture.js": [
"/workspace/node_modules/@tapjs/fixture/src/fixture.ts"
],
"/workspace/node_modules/@tapjs/node-serialize/dist/esm/serialize.js": [
"/workspace/node_modules/@tapjs/node-serialize/src/serialize.ts"
],
"/workspace/node_modules/@tapjs/mock/dist/esm/mock-require.js": [
"/workspace/node_modules/@tapjs/mock/src/mock-require.ts"
],
"/workspace/node_modules/@tapjs/snapshot/dist/esm/clean-cwd.js": [
"/workspace/node_modules/@tapjs/snapshot/src/clean-cwd.ts"
],
"/workspace/node_modules/@tapjs/snapshot/dist/esm/provider.js": [
"/workspace/node_modules/@tapjs/snapshot/src/provider.ts"
],
"/workspace/node_modules/@isaacs/cliui/dist/esm/index.min.js": [
"/workspace/node_modules/@isaacs/cliui/src/ansi-regex/index.ts",
"/workspace/node_modules/@isaacs/cliui/src/strip-ansi/index.ts",
"/workspace/node_modules/@isaacs/cliui/src/eastasianwidth/index.ts",
"/workspace/node_modules/@isaacs/cliui/src/emoji-regex/index.ts",
"/workspace/node_modules/@isaacs/cliui/src/string-width/index.ts",
"/workspace/node_modules/@isaacs/cliui/src/ansi-styles/index.ts",
"/workspace/node_modules/@isaacs/cliui/src/wrap-ansi/index.ts",
"/workspace/node_modules/@isaacs/cliui/src/index.ts"
],
"/workspace/node_modules/tap-yaml/dist/esm/types/date.js": [
"/workspace/node_modules/tap-yaml/src/types/date.ts"
],
"/workspace/node_modules/tap-yaml/dist/esm/types/timestamp.js": [
"/workspace/node_modules/tap-yaml/src/types/timestamp.ts"
],
"/workspace/node_modules/tcompare/dist/esm/react-element-to-jsx-string.js": [
"/workspace/node_modules/tcompare/src/react-element-to-jsx-string.ts"
],
"/workspace/node_modules/rimraf/dist/esm/opt-arg.js": [
"/workspace/node_modules/rimraf/src/opt-arg.ts"
],
"/workspace/node_modules/glob/dist/esm/index.min.js": [
"/workspace/node_modules/glob/node_modules/balanced-match/src/index.ts",
"/workspace/node_modules/glob/node_modules/brace-expansion/src/index.ts",
"/workspace/node_modules/glob/node_modules/minimatch/src/assert-valid-pattern.ts",
"/workspace/node_modules/glob/node_modules/minimatch/src/brace-expressions.ts",
"/workspace/node_modules/glob/node_modules/minimatch/src/unescape.ts",
"/workspace/node_modules/glob/node_modules/minimatch/src/ast.ts",
"/workspace/node_modules/glob/node_modules/minimatch/src/escape.ts",
"/workspace/node_modules/glob/node_modules/minimatch/src/index.ts",
"/workspace/node_modules/glob/src/glob.ts",
"/workspace/node_modules/glob/node_modules/lru-cache/src/index.ts",
"/workspace/node_modules/glob/node_modules/path-scurry/src/index.ts",
"/workspace/node_modules/glob/node_modules/minipass/src/index.ts",
"/workspace/node_modules/glob/src/pattern.ts",
"/workspace/node_modules/glob/src/ignore.ts",
"/workspace/node_modules/glob/src/processor.ts",
"/workspace/node_modules/glob/src/walker.ts",
"/workspace/node_modules/glob/src/has-magic.ts",
"/workspace/node_modules/glob/src/index.ts"
],
"/workspace/node_modules/rimraf/dist/esm/path-arg.js": [
"/workspace/node_modules/rimraf/src/path-arg.ts"
],
"/workspace/node_modules/rimraf/dist/esm/rimraf-manual.js": [
"/workspace/node_modules/rimraf/src/rimraf-manual.ts"
],
"/workspace/node_modules/rimraf/dist/esm/rimraf-move-remove.js": [
"/workspace/node_modules/rimraf/src/rimraf-move-remove.ts"
],
"/workspace/node_modules/rimraf/dist/esm/rimraf-native.js": [
"/workspace/node_modules/rimraf/src/rimraf-native.ts"
],
"/workspace/node_modules/rimraf/dist/esm/rimraf-posix.js": [
"/workspace/node_modules/rimraf/src/rimraf-posix.ts"
],
"/workspace/node_modules/rimraf/dist/esm/rimraf-windows.js": [
"/workspace/node_modules/rimraf/src/rimraf-windows.ts"
],
"/workspace/node_modules/rimraf/dist/esm/use-native.js": [
"/workspace/node_modules/rimraf/src/use-native.ts"
],
"/workspace/node_modules/mkdirp/dist/mjs/index.js": [
"/workspace/node_modules/mkdirp/src/index.ts"
],
"/workspace/node_modules/walk-up-path/dist/esm/index.js": [
"/workspace/node_modules/walk-up-path/src/index.ts"
],
"/workspace/node_modules/@tapjs/error-serdes/dist/esm/index.js": [
"/workspace/node_modules/@tapjs/error-serdes/src/index.ts"
],
"/workspace/node_modules/@tapjs/node-serialize/dist/esm/comment.js": [
"/workspace/node_modules/@tapjs/node-serialize/src/comment.ts"
],
"/workspace/node_modules/@tapjs/node-serialize/dist/esm/on-add.js": [
"/workspace/node_modules/@tapjs/node-serialize/src/on-add.ts"
],
"/workspace/node_modules/@tapjs/node-serialize/dist/esm/print-messages.js": [
"/workspace/node_modules/@tapjs/node-serialize/src/print-messages.ts"
],
"/workspace/node_modules/@tapjs/node-serialize/dist/esm/stdio.js": [
"/workspace/node_modules/@tapjs/node-serialize/src/stdio.ts"
],
"/workspace/node_modules/@tapjs/snapshot/dist/esm/require.js": [
"/workspace/node_modules/@tapjs/snapshot/src/require.ts"
],
"/workspace/node_modules/@tapjs/node-serialize/dist/esm/test-map.js": [
"/workspace/node_modules/@tapjs/node-serialize/src/test-map.ts"
],
"/workspace/node_modules/mkdirp/dist/mjs/mkdirp-manual.js": [
"/workspace/node_modules/mkdirp/src/mkdirp-manual.ts"
],
"/workspace/node_modules/mkdirp/dist/mjs/opts-arg.js": [
"/workspace/node_modules/mkdirp/src/opts-arg.ts"
],
"/workspace/node_modules/mkdirp/dist/mjs/mkdirp-native.js": [
"/workspace/node_modules/mkdirp/src/mkdirp-native.ts"
],
"/workspace/node_modules/rimraf/dist/esm/default-tmp.js": [
"/workspace/node_modules/rimraf/src/default-tmp.ts"
],
"/workspace/node_modules/mkdirp/dist/mjs/path-arg.js": [
"/workspace/node_modules/mkdirp/src/path-arg.ts"
],
"/workspace/node_modules/mkdirp/dist/mjs/use-native.js": [
"/workspace/node_modules/mkdirp/src/use-native.ts"
],
"/workspace/node_modules/rimraf/dist/esm/ignore-enoent.js": [
"/workspace/node_modules/rimraf/src/ignore-enoent.ts"
],
"/workspace/node_modules/rimraf/dist/esm/readdir-or-error.js": [
"/workspace/node_modules/rimraf/src/readdir-or-error.ts"
],
"/workspace/node_modules/rimraf/dist/esm/fix-eperm.js": [
"/workspace/node_modules/rimraf/src/fix-eperm.ts"
],
"/workspace/node_modules/rimraf/dist/esm/error.js": [
"/workspace/node_modules/rimraf/src/error.ts"
],
"/workspace/node_modules/rimraf/dist/esm/retry-busy.js": [
"/workspace/node_modules/rimraf/src/retry-busy.ts"
],
"/workspace/node_modules/rimraf/dist/esm/fs.js": [
"/workspace/node_modules/rimraf/src/fs.ts"
],
"/workspace/node_modules/@tapjs/error-serdes/dist/esm/test-stream-serialize.js": [
"/workspace/node_modules/@tapjs/error-serdes/src/test-stream-serialize.ts"
],
"/workspace/node_modules/@tapjs/error-serdes/dist/esm/serialize.js": [
"/workspace/node_modules/@tapjs/error-serdes/src/serialize.ts"
],
"/workspace/node_modules/@tapjs/error-serdes/dist/esm/deserialize.js": [
"/workspace/node_modules/@tapjs/error-serdes/src/deserialize.ts"
],
"/workspace/node_modules/@tapjs/error-serdes/dist/esm/test-stream-deserialize.js": [
"/workspace/node_modules/@tapjs/error-serdes/src/test-stream-deserialize.ts"
],
"/workspace/node_modules/@tapjs/node-serialize/dist/esm/test-nested-location.js": [
"/workspace/node_modules/@tapjs/node-serialize/src/test-nested-location.ts"
],
"/workspace/node_modules/@tapjs/node-serialize/dist/esm/loc-from-at.js": [
"/workspace/node_modules/@tapjs/node-serialize/src/loc-from-at.ts"
],
"/workspace/node_modules/@tapjs/error-serdes/dist/esm/constants.js": [
"/workspace/node_modules/@tapjs/error-serdes/src/constants.ts"
],
"/workspace/node_modules/@tapjs/error-serdes/dist/esm/messages.js": [
"/workspace/node_modules/@tapjs/error-serdes/src/messages.ts"
],
"/workspace/node_modules/@tapjs/node-serialize/dist/esm/test-results.js": [
"/workspace/node_modules/@tapjs/node-serialize/src/test-results.ts"
],
"/workspace/node_modules/@tapjs/node-serialize/dist/esm/test-point-message-data.js": [
"/workspace/node_modules/@tapjs/node-serialize/src/test-point-message-data.ts"
],
"/workspace/node_modules/@tapjs/node-serialize/dist/esm/test-message-data.js": [
"/workspace/node_modules/@tapjs/node-serialize/src/test-message-data.ts"
],
"/workspace/node_modules/@tapjs/node-serialize/dist/esm/test-point-results.js": [
"/workspace/node_modules/@tapjs/node-serialize/src/test-point-results.ts"
],
"/workspace/node_modules/@tapjs/node-serialize/dist/esm/loc-from-callsite.js": [
"/workspace/node_modules/@tapjs/node-serialize/src/loc-from-callsite.ts"
],
"/workspace/node_modules/mkdirp/dist/mjs/find-made.js": [
"/workspace/node_modules/mkdirp/src/find-made.ts"
],
"/workspace/node_modules/@tapjs/node-serialize/dist/esm/result-to-error.js": [
"/workspace/node_modules/@tapjs/node-serialize/src/result-to-error.ts"
],
"/workspace/node_modules/@tapjs/node-serialize/dist/esm/pretty-diff.js": [
"/workspace/node_modules/@tapjs/node-serialize/src/pretty-diff.ts"
],
"/workspace/node_modules/tap/dist/esm/index.js": [
"/workspace/node_modules/tap/src/index.ts"
],
"/workspace/dist/esm/index.js": [
"/workspace/src/index.ts"
],
"/workspace/node_modules/tap/dist/esm/main.js": [
"/workspace/node_modules/tap/src/main.ts"
],
"/workspace/node_modules/balanced-match/dist/esm/index.js": [
"/workspace/node_modules/balanced-match/src/index.ts"
]
},
"root": "8b906b8c-e21f-40b5-8c0d-214d8c2682c5",
"externalID": "test/index.js",
"code": 0,
"signal": null,
"runtime": 2740.9173339999998
}
@@ -0,0 +1,107 @@
# DoS via unbounded expansion length — out-of-memory process crash
- **CVE:** CVE-2026-14257
- **Package:** brace-expansion (npm)
- **Reporter:** @bnbdr
- **Severity (proposed):** High — `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H` (7.5)
- **Weakness:** CWE-770 (Allocation of Resources Without Limits or Throttling) / CWE-400 (Uncontrolled Resource Consumption)
- **Affected:** all versions up to and including `5.0.7` (the `1.x`, `2.x`, `3.x` and `4.x` lines share the same combine logic and are expected to be affected)
### Summary
`expand()` bounds the *number* of results it produces (the `max` option,
`100_000` by default) but not their *length*. By chaining many brace groups,
an attacker keeps the result count under `max` while making every result grow
with the number of groups. Building `max` long results — plus the intermediate
arrays combined at each brace group — exhausts memory and crashes the Node
process with an **uncatchable** out-of-memory error. `try/catch` around
`expand()` does not help: the fatal error terminates the process.
A ~7.5 KB input (`'{a,b}'.repeat(1500)`) is enough to crash a default Node
process.
### Details
For `N` chained brace groups such as `'{a,b}'.repeat(N)`:
- the result count is `2^N`, immediately capped at `max` (`100_000`), so the
`max` protection appears to hold, but
- each result is `N` characters long, so the total output size is
`max × N` characters, which grows without bound in `N`.
`expand_` combines each brace set with the fully-expanded tail:
```js
const post = m.post.length ? expand_(m.post, max, false) : ['']
...
for (let j = 0; j < N.length; j++) {
for (let k = 0; k < post.length && expansions.length < max; k++) {
const expansion = pre + N[j] + post[k] // grows one group longer per level
...
expansions.push(expansion)
}
}
```
The loop guard `expansions.length < max` limits how many strings are built, but
nothing limits how long they get. Each recursion level materializes another
array of up to `max` strings, one character longer than the level below, and —
because V8 represents `pre + N[j] + post[k]` as a cons-string (rope) that
references `post[k]` — those intermediate strings stay reachable through the
whole chain. Memory therefore scales with `max × N`.
Measured on `5.0.7` (`'{a,b}'.repeat(N)`, default `max`):
| groups (N) | input bytes | result count | peak RSS |
|---|---|---|---|
| 20 | 100 | 100,000 | ~80 MB |
| 50 | 250 | 100,000 | ~214 MB |
| 100 | 500 | 100,000 | ~409 MB |
| 300 | 1,500 | 100,000 | ~1,148 MB |
| 1500 | 7,500 | — | **OOM crash** |
### Proof of concept
```js
const { expand } = require('brace-expansion')
// ~7.5 KB input — crashes the process with a fatal, uncatchable OOM:
// FATAL ERROR: ... JavaScript heap out of memory
try {
expand('{a,b}'.repeat(1500))
} catch (e) {
// never reached — the process is already dead
}
```
### Impact
Any application that passes attacker-influenced strings to
`brace-expansion.expand()` — directly, or transitively via `minimatch` / `glob`
brace patterns — can be crashed by a small request. Because the failure is a
fatal V8 out-of-memory error rather than a thrown exception, it cannot be caught
and it takes down the whole worker/process, denying service.
### Remediation
Upgrade to a patched release. The fix bounds the total number of characters a
single `expand()` call may accumulate (`EXPANSION_MAX_LENGTH`, default
`4_000_000`, configurable via a new `maxLength` option), applied inside the
output-building loops so intermediate arrays are bounded too. Once the limit is
reached, output is truncated — consistent with how `max` already truncates —
instead of growing without bound. The limit sits well above any realistic
expansion (100,000 results hitting `max` measure ~1M characters), so legitimate
input is unaffected.
After the fix, `'{a,b}'.repeat(1500)` returns a bounded, truncated result in
~0.7 s using ~340 MB and never crashes, including under a constrained 512 MB
heap.
The fix bounds memory but the algorithm still rebuilds intermediate arrays at
each level (roughly `O(N × maxLength)` work on this input class). A streaming
rewrite that produces output in `O(total output size)` can be a non-urgent
follow-up.
If immediate upgrade isn't possible, avoid passing untrusted input to
`expand()` / glob brace patterns, or pass a small explicit `max` **and**
`maxLength`.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2013 Julian Gruber <julian@juliangruber.com>
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.
+129
View File
@@ -0,0 +1,129 @@
# brace-expansion
[Brace expansion](https://www.gnu.org/software/bash/manual/html_node/Brace-Expansion.html),
as known from sh/bash, in JavaScript.
[![build status](https://secure.travis-ci.org/juliangruber/brace-expansion.svg)](http://travis-ci.org/juliangruber/brace-expansion)
[![downloads](https://img.shields.io/npm/dm/brace-expansion.svg)](https://www.npmjs.org/package/brace-expansion)
[![Greenkeeper badge](https://badges.greenkeeper.io/juliangruber/brace-expansion.svg)](https://greenkeeper.io/)
[![testling badge](https://ci.testling.com/juliangruber/brace-expansion.png)](https://ci.testling.com/juliangruber/brace-expansion)
## Example
```js
var expand = require('brace-expansion');
expand('file-{a,b,c}.jpg')
// => ['file-a.jpg', 'file-b.jpg', 'file-c.jpg']
expand('-v{,,}')
// => ['-v', '-v', '-v']
expand('file{0..2}.jpg')
// => ['file0.jpg', 'file1.jpg', 'file2.jpg']
expand('file-{a..c}.jpg')
// => ['file-a.jpg', 'file-b.jpg', 'file-c.jpg']
expand('file{2..0}.jpg')
// => ['file2.jpg', 'file1.jpg', 'file0.jpg']
expand('file{0..4..2}.jpg')
// => ['file0.jpg', 'file2.jpg', 'file4.jpg']
expand('file-{a..e..2}.jpg')
// => ['file-a.jpg', 'file-c.jpg', 'file-e.jpg']
expand('file{00..10..5}.jpg')
// => ['file00.jpg', 'file05.jpg', 'file10.jpg']
expand('{{A..C},{a..c}}')
// => ['A', 'B', 'C', 'a', 'b', 'c']
expand('ppp{,config,oe{,conf}}')
// => ['ppp', 'pppconfig', 'pppoe', 'pppoeconf']
```
## API
```js
var expand = require('brace-expansion');
```
### var expanded = expand(str)
Return an array of all possible and valid expansions of `str`. If none are
found, `[str]` is returned.
Valid expansions are:
```js
/^(.*,)+(.+)?$/
// {a,b,...}
```
A comma separated list of options, like `{a,b}` or `{a,{b,c}}` or `{,a,}`.
```js
/^-?\d+\.\.-?\d+(\.\.-?\d+)?$/
// {x..y[..incr]}
```
A numeric sequence from `x` to `y` inclusive, with optional increment.
If `x` or `y` start with a leading `0`, all the numbers will be padded
to have equal length. Negative numbers and backwards iteration work too.
```js
/^-?\d+\.\.-?\d+(\.\.-?\d+)?$/
// {x..y[..incr]}
```
An alphabetic sequence from `x` to `y` inclusive, with optional increment.
`x` and `y` must be exactly one character, and if given, `incr` must be a
number.
For compatibility reasons, the string `${` is not eligible for brace expansion.
## Installation
With [npm](https://npmjs.org) do:
```bash
npm install brace-expansion
```
## Contributors
- [Julian Gruber](https://github.com/juliangruber)
- [Isaac Z. Schlueter](https://github.com/isaacs)
## Sponsors
This module is proudly supported by my [Sponsors](https://github.com/juliangruber/sponsors)!
Do you want to support modules like this to improve their quality, stability and weigh in on new features? Then please consider donating to my [Patreon](https://www.patreon.com/juliangruber). Not sure how much of my modules you're using? Try [feross/thanks](https://github.com/feross/thanks)!
## License
(MIT)
Copyright (c) 2013 Julian Gruber &lt;julian@juliangruber.com&gt;
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.
@@ -0,0 +1,8 @@
export declare const EXPANSION_MAX = 100000;
export declare const EXPANSION_MAX_LENGTH = 4000000;
export type BraceExpansionOptions = {
max?: number;
maxLength?: number;
};
export declare function expand(str: string, options?: BraceExpansionOptions): string[];
//# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAkBA,eAAO,MAAM,aAAa,SAAU,CAAA;AAYpC,eAAO,MAAM,oBAAoB,UAAY,CAAA;AAwD7C,MAAM,MAAM,qBAAqB,GAAG;IAClC,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,qBAA0B,YAkBtE"}
@@ -0,0 +1,263 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.EXPANSION_MAX_LENGTH = exports.EXPANSION_MAX = void 0;
exports.expand = expand;
const balanced_match_1 = require("balanced-match");
const escSlash = '\0SLASH' + Math.random() + '\0';
const escOpen = '\0OPEN' + Math.random() + '\0';
const escClose = '\0CLOSE' + Math.random() + '\0';
const escComma = '\0COMMA' + Math.random() + '\0';
const escPeriod = '\0PERIOD' + Math.random() + '\0';
const escSlashPattern = new RegExp(escSlash, 'g');
const escOpenPattern = new RegExp(escOpen, 'g');
const escClosePattern = new RegExp(escClose, 'g');
const escCommaPattern = new RegExp(escComma, 'g');
const escPeriodPattern = new RegExp(escPeriod, 'g');
const slashPattern = /\\\\/g;
const openPattern = /\\{/g;
const closePattern = /\\}/g;
const commaPattern = /\\,/g;
const periodPattern = /\\\./g;
exports.EXPANSION_MAX = 100_000;
// `EXPANSION_MAX` caps the *number* of expansions, but not their length. An
// input like `'{a,b}'.repeat(1500)` stays under that count - its output is
// truncated to 100k results - while making every result ~1500 characters
// long. The result set, and the intermediate arrays built while combining
// brace sets, then grow large enough to exhaust memory and crash the process
// (CVE-2026-14257). `EXPANSION_MAX_LENGTH` bounds the total number of
// characters the accumulator may hold at any point, so memory stays flat no
// matter how many brace groups are chained. The limit sits well above any
// realistic expansion (100k results hitting `EXPANSION_MAX` measure ~1M
// characters) so legitimate input is unaffected.
exports.EXPANSION_MAX_LENGTH = 4_000_000;
function numeric(str) {
return !isNaN(str) ? parseInt(str, 10) : str.charCodeAt(0);
}
function escapeBraces(str) {
return str
.replace(slashPattern, escSlash)
.replace(openPattern, escOpen)
.replace(closePattern, escClose)
.replace(commaPattern, escComma)
.replace(periodPattern, escPeriod);
}
function unescapeBraces(str) {
return str
.replace(escSlashPattern, '\\')
.replace(escOpenPattern, '{')
.replace(escClosePattern, '}')
.replace(escCommaPattern, ',')
.replace(escPeriodPattern, '.');
}
/**
* Basically just str.split(","), but handling cases
* where we have nested braced sections, which should be
* treated as individual members, like {a,{b,c},d}
*/
function parseCommaParts(str) {
if (!str) {
return [''];
}
const parts = [];
const m = (0, balanced_match_1.balanced)('{', '}', str);
if (!m) {
return str.split(',');
}
const { pre, body, post } = m;
const p = pre.split(',');
p[p.length - 1] += '{' + body + '}';
const postParts = parseCommaParts(post);
if (post.length) {
;
p[p.length - 1] += postParts.shift();
p.push.apply(p, postParts);
}
parts.push.apply(parts, p);
return parts;
}
function expand(str, options = {}) {
if (!str) {
return [];
}
const { max = exports.EXPANSION_MAX, maxLength = exports.EXPANSION_MAX_LENGTH } = options;
// I don't know why Bash 4.3 does this, but it does.
// Anything starting with {} will have the first two bytes preserved
// but *only* at the top level, so {},a}b will not expand to anything,
// but a{},b}c will be expanded to [a}c,abc].
// One could argue that this is a bug in Bash, but since the goal of
// this module is to match Bash's rules, we escape a leading {}
if (str.slice(0, 2) === '{}') {
str = '\\{\\}' + str.slice(2);
}
return expand_(escapeBraces(str), max, maxLength, true).map(unescapeBraces);
}
function embrace(str) {
return '{' + str + '}';
}
function isPadded(el) {
return /^-?0\d/.test(el);
}
function lte(i, y) {
return i <= y;
}
function gte(i, y) {
return i >= y;
}
// Build `{ acc[a] + pre + values[v] }` for every combination, capping the
// number of results at `max` and the total number of characters at `maxLength`.
// This is the one place output grows, so bounding it here keeps the single
// accumulator - and therefore memory - flat regardless of how many brace groups
// are combined (CVE-2026-14257).
function combine(acc, pre, values, max, maxLength, dropEmpties) {
const out = [];
let length = 0;
for (let a = 0; a < acc.length; a++) {
for (let v = 0; v < values.length; v++) {
if (out.length >= max)
return out;
const expansion = acc[a] + pre + values[v];
// Bash drops empty results at the top level. Skip them before they count
// against `max`, so `max` bounds the number of *kept* results.
if (dropEmpties && !expansion)
continue;
if (length + expansion.length > maxLength)
return out;
out.push(expansion);
length += expansion.length;
}
}
return out;
}
// The expansion values of a single numeric (`1..5`) or alphabetic (`a..e..2`)
// sequence body.
function expandSequence(body, isAlphaSequence, max) {
const n = body.split(/\.\./);
const N = [];
// A sequence body always splits into two or three parts, but the compiler
// can't know that.
/* c8 ignore start */
if (n[0] === undefined || n[1] === undefined) {
return N;
}
/* c8 ignore stop */
const x = numeric(n[0]);
const y = numeric(n[1]);
const width = Math.max(n[0].length, n[1].length);
let incr = n.length === 3 && n[2] !== undefined ?
Math.max(Math.abs(numeric(n[2])), 1)
: 1;
let test = lte;
const reverse = y < x;
if (reverse) {
incr *= -1;
test = gte;
}
const pad = n.some(isPadded);
for (let i = x; test(i, y) && N.length < max; i += incr) {
let c;
if (isAlphaSequence) {
c = String.fromCharCode(i);
if (c === '\\') {
c = '';
}
}
else {
c = String(i);
if (pad) {
const need = width - c.length;
if (need > 0) {
const z = new Array(need + 1).join('0');
if (i < 0) {
c = '-' + z + c.slice(1);
}
else {
c = z + c;
}
}
}
}
N.push(c);
}
return N;
}
function expand_(str, max, maxLength, isTop) {
// Consume the string's top-level brace groups left to right, threading a
// running set of combined prefixes (`acc`). Expanding the tail iteratively -
// rather than recursing on `m.post` once per group - keeps the native stack
// depth constant, so deeply chained input (`'{a,b}'.repeat(3000)`) can no
// longer overflow the stack, and leaves a single accumulator whose size
// `maxLength` bounds directly (CVE-2026-14257).
let acc = [''];
// Bash drops empty results, but only when the *first* top-level group is a
// comma set - a sequence like `{a..\}` may legitimately yield ''. The drop
// is on the final strings, so it is applied to whichever `combine` produces
// them (the one with no brace set left in the tail).
let dropEmpties = false;
let firstGroup = true;
for (;;) {
const m = (0, balanced_match_1.balanced)('{', '}', str);
// No brace set left: the rest of the string is literal.
if (!m) {
return combine(acc, str, [''], max, maxLength, dropEmpties);
}
// no need to expand pre, since it is guaranteed to be free of brace-sets
const pre = m.pre;
if (/\$$/.test(pre)) {
acc = combine(acc, pre + '{' + m.body + '}', [''], max, maxLength, dropEmpties && !m.post.length);
firstGroup = false;
if (!m.post.length)
break;
str = m.post;
continue;
}
const isNumericSequence = /^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(m.body);
const isAlphaSequence = /^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(m.body);
const isSequence = isNumericSequence || isAlphaSequence;
const isOptions = m.body.indexOf(',') >= 0;
if (!isSequence && !isOptions) {
// {a},b}
if (m.post.match(/,(?!,).*\}/)) {
str = m.pre + '{' + m.body + escClose + m.post;
isTop = true;
continue;
}
// Nothing here expands, so the whole remaining string is literal.
return combine(acc, pre + '{' + m.body + '}' + m.post, [''], max, maxLength, dropEmpties);
}
if (firstGroup) {
dropEmpties = isTop && !isSequence;
firstGroup = false;
}
let values;
if (isSequence) {
values = expandSequence(m.body, isAlphaSequence, max);
}
else {
let n = parseCommaParts(m.body);
if (n.length === 1 && n[0] !== undefined) {
// x{{a,b}}y ==> x{a}y x{b}y
n = expand_(n[0], max, maxLength, false).map(embrace);
//XXX is this necessary? Can't seem to hit it in tests.
/* c8 ignore start */
if (n.length === 1) {
acc = combine(acc, pre + n[0], [''], max, maxLength, dropEmpties && !m.post.length);
if (!m.post.length)
break;
str = m.post;
continue;
}
/* c8 ignore stop */
}
values = [];
for (let j = 0; j < n.length; j++) {
values.push.apply(values, expand_(n[j], max, maxLength, false));
}
}
acc = combine(acc, pre, values, max, maxLength, dropEmpties && !m.post.length);
if (!m.post.length)
break;
str = m.post;
}
return acc;
}
//# sourceMappingURL=index.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
{
"type": "commonjs"
}
@@ -0,0 +1,8 @@
export declare const EXPANSION_MAX = 100000;
export declare const EXPANSION_MAX_LENGTH = 4000000;
export type BraceExpansionOptions = {
max?: number;
maxLength?: number;
};
export declare function expand(str: string, options?: BraceExpansionOptions): string[];
//# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAkBA,eAAO,MAAM,aAAa,SAAU,CAAA;AAYpC,eAAO,MAAM,oBAAoB,UAAY,CAAA;AAwD7C,MAAM,MAAM,qBAAqB,GAAG;IAClC,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,qBAA0B,YAkBtE"}
@@ -0,0 +1,259 @@
import { balanced } from 'balanced-match';
const escSlash = '\0SLASH' + Math.random() + '\0';
const escOpen = '\0OPEN' + Math.random() + '\0';
const escClose = '\0CLOSE' + Math.random() + '\0';
const escComma = '\0COMMA' + Math.random() + '\0';
const escPeriod = '\0PERIOD' + Math.random() + '\0';
const escSlashPattern = new RegExp(escSlash, 'g');
const escOpenPattern = new RegExp(escOpen, 'g');
const escClosePattern = new RegExp(escClose, 'g');
const escCommaPattern = new RegExp(escComma, 'g');
const escPeriodPattern = new RegExp(escPeriod, 'g');
const slashPattern = /\\\\/g;
const openPattern = /\\{/g;
const closePattern = /\\}/g;
const commaPattern = /\\,/g;
const periodPattern = /\\\./g;
export const EXPANSION_MAX = 100_000;
// `EXPANSION_MAX` caps the *number* of expansions, but not their length. An
// input like `'{a,b}'.repeat(1500)` stays under that count - its output is
// truncated to 100k results - while making every result ~1500 characters
// long. The result set, and the intermediate arrays built while combining
// brace sets, then grow large enough to exhaust memory and crash the process
// (CVE-2026-14257). `EXPANSION_MAX_LENGTH` bounds the total number of
// characters the accumulator may hold at any point, so memory stays flat no
// matter how many brace groups are chained. The limit sits well above any
// realistic expansion (100k results hitting `EXPANSION_MAX` measure ~1M
// characters) so legitimate input is unaffected.
export const EXPANSION_MAX_LENGTH = 4_000_000;
function numeric(str) {
return !isNaN(str) ? parseInt(str, 10) : str.charCodeAt(0);
}
function escapeBraces(str) {
return str
.replace(slashPattern, escSlash)
.replace(openPattern, escOpen)
.replace(closePattern, escClose)
.replace(commaPattern, escComma)
.replace(periodPattern, escPeriod);
}
function unescapeBraces(str) {
return str
.replace(escSlashPattern, '\\')
.replace(escOpenPattern, '{')
.replace(escClosePattern, '}')
.replace(escCommaPattern, ',')
.replace(escPeriodPattern, '.');
}
/**
* Basically just str.split(","), but handling cases
* where we have nested braced sections, which should be
* treated as individual members, like {a,{b,c},d}
*/
function parseCommaParts(str) {
if (!str) {
return [''];
}
const parts = [];
const m = balanced('{', '}', str);
if (!m) {
return str.split(',');
}
const { pre, body, post } = m;
const p = pre.split(',');
p[p.length - 1] += '{' + body + '}';
const postParts = parseCommaParts(post);
if (post.length) {
;
p[p.length - 1] += postParts.shift();
p.push.apply(p, postParts);
}
parts.push.apply(parts, p);
return parts;
}
export function expand(str, options = {}) {
if (!str) {
return [];
}
const { max = EXPANSION_MAX, maxLength = EXPANSION_MAX_LENGTH } = options;
// I don't know why Bash 4.3 does this, but it does.
// Anything starting with {} will have the first two bytes preserved
// but *only* at the top level, so {},a}b will not expand to anything,
// but a{},b}c will be expanded to [a}c,abc].
// One could argue that this is a bug in Bash, but since the goal of
// this module is to match Bash's rules, we escape a leading {}
if (str.slice(0, 2) === '{}') {
str = '\\{\\}' + str.slice(2);
}
return expand_(escapeBraces(str), max, maxLength, true).map(unescapeBraces);
}
function embrace(str) {
return '{' + str + '}';
}
function isPadded(el) {
return /^-?0\d/.test(el);
}
function lte(i, y) {
return i <= y;
}
function gte(i, y) {
return i >= y;
}
// Build `{ acc[a] + pre + values[v] }` for every combination, capping the
// number of results at `max` and the total number of characters at `maxLength`.
// This is the one place output grows, so bounding it here keeps the single
// accumulator - and therefore memory - flat regardless of how many brace groups
// are combined (CVE-2026-14257).
function combine(acc, pre, values, max, maxLength, dropEmpties) {
const out = [];
let length = 0;
for (let a = 0; a < acc.length; a++) {
for (let v = 0; v < values.length; v++) {
if (out.length >= max)
return out;
const expansion = acc[a] + pre + values[v];
// Bash drops empty results at the top level. Skip them before they count
// against `max`, so `max` bounds the number of *kept* results.
if (dropEmpties && !expansion)
continue;
if (length + expansion.length > maxLength)
return out;
out.push(expansion);
length += expansion.length;
}
}
return out;
}
// The expansion values of a single numeric (`1..5`) or alphabetic (`a..e..2`)
// sequence body.
function expandSequence(body, isAlphaSequence, max) {
const n = body.split(/\.\./);
const N = [];
// A sequence body always splits into two or three parts, but the compiler
// can't know that.
/* c8 ignore start */
if (n[0] === undefined || n[1] === undefined) {
return N;
}
/* c8 ignore stop */
const x = numeric(n[0]);
const y = numeric(n[1]);
const width = Math.max(n[0].length, n[1].length);
let incr = n.length === 3 && n[2] !== undefined ?
Math.max(Math.abs(numeric(n[2])), 1)
: 1;
let test = lte;
const reverse = y < x;
if (reverse) {
incr *= -1;
test = gte;
}
const pad = n.some(isPadded);
for (let i = x; test(i, y) && N.length < max; i += incr) {
let c;
if (isAlphaSequence) {
c = String.fromCharCode(i);
if (c === '\\') {
c = '';
}
}
else {
c = String(i);
if (pad) {
const need = width - c.length;
if (need > 0) {
const z = new Array(need + 1).join('0');
if (i < 0) {
c = '-' + z + c.slice(1);
}
else {
c = z + c;
}
}
}
}
N.push(c);
}
return N;
}
function expand_(str, max, maxLength, isTop) {
// Consume the string's top-level brace groups left to right, threading a
// running set of combined prefixes (`acc`). Expanding the tail iteratively -
// rather than recursing on `m.post` once per group - keeps the native stack
// depth constant, so deeply chained input (`'{a,b}'.repeat(3000)`) can no
// longer overflow the stack, and leaves a single accumulator whose size
// `maxLength` bounds directly (CVE-2026-14257).
let acc = [''];
// Bash drops empty results, but only when the *first* top-level group is a
// comma set - a sequence like `{a..\}` may legitimately yield ''. The drop
// is on the final strings, so it is applied to whichever `combine` produces
// them (the one with no brace set left in the tail).
let dropEmpties = false;
let firstGroup = true;
for (;;) {
const m = balanced('{', '}', str);
// No brace set left: the rest of the string is literal.
if (!m) {
return combine(acc, str, [''], max, maxLength, dropEmpties);
}
// no need to expand pre, since it is guaranteed to be free of brace-sets
const pre = m.pre;
if (/\$$/.test(pre)) {
acc = combine(acc, pre + '{' + m.body + '}', [''], max, maxLength, dropEmpties && !m.post.length);
firstGroup = false;
if (!m.post.length)
break;
str = m.post;
continue;
}
const isNumericSequence = /^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(m.body);
const isAlphaSequence = /^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(m.body);
const isSequence = isNumericSequence || isAlphaSequence;
const isOptions = m.body.indexOf(',') >= 0;
if (!isSequence && !isOptions) {
// {a},b}
if (m.post.match(/,(?!,).*\}/)) {
str = m.pre + '{' + m.body + escClose + m.post;
isTop = true;
continue;
}
// Nothing here expands, so the whole remaining string is literal.
return combine(acc, pre + '{' + m.body + '}' + m.post, [''], max, maxLength, dropEmpties);
}
if (firstGroup) {
dropEmpties = isTop && !isSequence;
firstGroup = false;
}
let values;
if (isSequence) {
values = expandSequence(m.body, isAlphaSequence, max);
}
else {
let n = parseCommaParts(m.body);
if (n.length === 1 && n[0] !== undefined) {
// x{{a,b}}y ==> x{a}y x{b}y
n = expand_(n[0], max, maxLength, false).map(embrace);
//XXX is this necessary? Can't seem to hit it in tests.
/* c8 ignore start */
if (n.length === 1) {
acc = combine(acc, pre + n[0], [''], max, maxLength, dropEmpties && !m.post.length);
if (!m.post.length)
break;
str = m.post;
continue;
}
/* c8 ignore stop */
}
values = [];
for (let j = 0; j < n.length; j++) {
values.push.apply(values, expand_(n[j], max, maxLength, false));
}
}
acc = combine(acc, pre, values, max, maxLength, dropEmpties && !m.post.length);
if (!m.post.length)
break;
str = m.post;
}
return acc;
}
//# sourceMappingURL=index.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
{
"type": "module"
}
+209
View File
@@ -0,0 +1,209 @@
var concatMap = require('concat-map');
var balanced = require('balanced-match');
module.exports = expandTop;
var escSlash = '\0SLASH'+Math.random()+'\0';
var escOpen = '\0OPEN'+Math.random()+'\0';
var escClose = '\0CLOSE'+Math.random()+'\0';
var escComma = '\0COMMA'+Math.random()+'\0';
var escPeriod = '\0PERIOD'+Math.random()+'\0';
function numeric(str) {
return parseInt(str, 10) == str
? parseInt(str, 10)
: str.charCodeAt(0);
}
function escapeBraces(str) {
return str.split('\\\\').join(escSlash)
.split('\\{').join(escOpen)
.split('\\}').join(escClose)
.split('\\,').join(escComma)
.split('\\.').join(escPeriod);
}
function unescapeBraces(str) {
return str.split(escSlash).join('\\')
.split(escOpen).join('{')
.split(escClose).join('}')
.split(escComma).join(',')
.split(escPeriod).join('.');
}
// Basically just str.split(","), but handling cases
// where we have nested braced sections, which should be
// treated as individual members, like {a,{b,c},d}
function parseCommaParts(str) {
if (!str)
return [''];
var parts = [];
var m = balanced('{', '}', str);
if (!m)
return str.split(',');
var pre = m.pre;
var body = m.body;
var post = m.post;
var p = pre.split(',');
p[p.length-1] += '{' + body + '}';
var postParts = parseCommaParts(post);
if (post.length) {
p[p.length-1] += postParts.shift();
p.push.apply(p, postParts);
}
parts.push.apply(parts, p);
return parts;
}
function expandTop(str, options) {
if (!str)
return [];
options = options || {};
var max = options.max == null ? Infinity : options.max;
// I don't know why Bash 4.3 does this, but it does.
// Anything starting with {} will have the first two bytes preserved
// but *only* at the top level, so {},a}b will not expand to anything,
// but a{},b}c will be expanded to [a}c,abc].
// One could argue that this is a bug in Bash, but since the goal of
// this module is to match Bash's rules, we escape a leading {}
if (str.substr(0, 2) === '{}') {
str = '\\{\\}' + str.substr(2);
}
return expand(escapeBraces(str), max, true).map(unescapeBraces);
}
function identity(e) {
return e;
}
function embrace(str) {
return '{' + str + '}';
}
function isPadded(el) {
return /^-?0\d/.test(el);
}
function lte(i, y) {
return i <= y;
}
function gte(i, y) {
return i >= y;
}
function expand(str, max, isTop) {
var expansions = [];
// The `{a},b}` rewrite below restarts expansion on a rewritten string with
// the same `max` and `isTop = true`. Loop instead of recursing so a long run
// of non-expanding `{}` groups can't exhaust the call stack.
for (;;) {
var m = balanced('{', '}', str);
if (!m || /\$$/.test(m.pre)) return [str];
var isNumericSequence = /^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(m.body);
var isAlphaSequence = /^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(m.body);
var isSequence = isNumericSequence || isAlphaSequence;
var isOptions = m.body.indexOf(',') >= 0;
if (!isSequence && !isOptions) {
// {a},b}
if (m.post.match(/,(?!,).*\}/)) {
str = m.pre + '{' + m.body + escClose + m.post;
isTop = true
continue
}
return [str];
}
var n;
if (isSequence) {
n = m.body.split(/\.\./);
} else {
n = parseCommaParts(m.body);
if (n.length === 1) {
// x{{a,b}}y ==> x{a}y x{b}y
n = expand(n[0], max, false).map(embrace);
if (n.length === 1) {
var post = m.post.length
? expand(m.post, max, false)
: [''];
return post.map(function(p) {
return m.pre + n[0] + p;
});
}
}
}
// at this point, n is the parts, and we know it's not a comma set
// with a single entry.
// no need to expand pre, since it is guaranteed to be free of brace-sets
var pre = m.pre;
var post = m.post.length
? expand(m.post, max, false)
: [''];
var N;
if (isSequence) {
var x = numeric(n[0]);
var y = numeric(n[1]);
var width = Math.max(n[0].length, n[1].length)
var incr = n.length == 3
? Math.max(Math.abs(numeric(n[2])), 1)
: 1;
var test = lte;
var reverse = y < x;
if (reverse) {
incr *= -1;
test = gte;
}
var pad = n.some(isPadded);
N = [];
for (var i = x; test(i, y) && N.length < max; i += incr) {
var c;
if (isAlphaSequence) {
c = String.fromCharCode(i);
if (c === '\\')
c = '';
} else {
c = String(i);
if (pad) {
var need = width - c.length;
if (need > 0) {
var z = new Array(need + 1).join('0');
if (i < 0)
c = '-' + z + c.slice(1);
else
c = z + c;
}
}
}
N.push(c);
}
} else {
N = concatMap(n, function(el) { return expand(el, max, false) });
}
for (var j = 0; j < N.length; j++) {
for (var k = 0; k < post.length && expansions.length < max; k++) {
var expansion = pre + N[j] + post[k];
if (!isTop || isSequence || expansion)
expansions.push(expansion);
}
}
return expansions;
}
}
+50
View File
@@ -0,0 +1,50 @@
{
"name": "brace-expansion",
"description": "Brace expansion as known from sh/bash",
"version": "1.1.16",
"repository": {
"type": "git",
"url": "git://github.com/juliangruber/brace-expansion.git"
},
"homepage": "https://github.com/juliangruber/brace-expansion",
"main": "index.js",
"scripts": {
"test": "tape test/*.js",
"gentest": "bash test/generate.sh",
"bench": "matcha test/perf/bench.js"
},
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
},
"devDependencies": {
"matcha": "^0.7.0",
"tape": "^4.6.0"
},
"keywords": [],
"author": {
"name": "Julian Gruber",
"email": "mail@juliangruber.com",
"url": "http://juliangruber.com"
},
"license": "MIT",
"testling": {
"files": "test/*.js",
"browsers": [
"ie/8..latest",
"firefox/20..latest",
"firefox/nightly",
"chrome/25..latest",
"chrome/canary",
"opera/12..latest",
"opera/next",
"safari/5.1..latest",
"ipad/6.0..latest",
"iphone/6.0..latest",
"android-browser/4.2..latest"
]
},
"publishConfig": {
"tag": "1.x"
}
}