@mh-alikhani/bunready 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -9,6 +9,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  Nothing yet.
11
11
 
12
+ ## [0.3.1] - 2026-09-19
13
+
14
+ ### Fixed
15
+
16
+ - Three polynomial-time regular expressions, reported by CodeQL as
17
+ `js/polynomial-redos`. bunready parses files it does not control - a
18
+ `pnpm-workspace.yaml` from a repository being scanned, a semver range in its
19
+ `engines` - so a pathological string could have made a scan take quadratic
20
+ time. The pnpm list item and the semver operator split are now parsed
21
+ directly, and trailing-slash stripping is a linear loop instead of `/+$`.
22
+ Behaviour is unchanged; `tests/workspaces.test.ts` and `tests/semver.test.ts`
23
+ cover the affected paths.
24
+
12
25
  ## [0.3.0] - 2026-09-18
13
26
 
14
27
  ### Added
@@ -188,7 +201,8 @@ Nothing yet.
188
201
  and that claim needs a primary source. Until then the rule reports what the
189
202
  repository imports and cites the compatibility table.
190
203
 
191
- [Unreleased]: https://github.com/MHAlikhani/bunready/compare/v0.3.0...HEAD
204
+ [Unreleased]: https://github.com/MHAlikhani/bunready/compare/v0.3.1...HEAD
205
+ [0.3.1]: https://github.com/MHAlikhani/bunready/releases/tag/v0.3.1
192
206
  [0.3.0]: https://github.com/MHAlikhani/bunready/releases/tag/v0.3.0
193
207
  [0.2.0]: https://github.com/MHAlikhani/bunready/releases/tag/v0.2.0
194
208
  [0.1.0]: https://github.com/MHAlikhani/bunready/releases/tag/v0.1.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mh-alikhani/bunready",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Bun-readiness scanner: one command that shows what will break before you move a Node/TS repo to Bun.",
5
5
  "keywords": [
6
6
  "bun",
@@ -92,12 +92,12 @@ function comparatorsForToken(token: string): Comparator[] | undefined {
92
92
  return [];
93
93
  }
94
94
 
95
- const operatorMatch = /^(>=|<=|>|<|=|\^|~)?\s*(.*)$/.exec(trimmed);
96
- if (operatorMatch === null) {
97
- return undefined;
98
- }
99
- const operator = operatorMatch[1];
100
- const rest = operatorMatch[2] ?? "";
95
+ // Anchored-prefix parse rather than a trailing \s*(.*)$, which backtracks
96
+ // quadratically on a long run of spaces - and the range comes from a
97
+ // repository bunready does not control.
98
+ const operatorMatch = /^(>=|<=|>|<|=|\^|~)?/.exec(trimmed);
99
+ const operator = operatorMatch?.[1];
100
+ const rest = trimmed.slice(operatorMatch?.[0].length ?? 0).trim();
101
101
  if (rest === "" || rest === "*" || rest === "x") {
102
102
  return [];
103
103
  }
@@ -54,9 +54,22 @@ export function patternsFromPnpmWorkspace(text: string): string[] {
54
54
  if (!inPackages) {
55
55
  continue;
56
56
  }
57
- const entry = /^-+\s*(.+?)\s*$/.exec(trimmed);
58
- if (entry?.[1] !== undefined) {
59
- patterns.push(entry[1].replace(/^['"]|['"]$/g, ""));
57
+ // Walked rather than matched: a pattern mixing a dash run, a space run and a
58
+ // catch-all is ambiguous, and CodeQL is right that it backtracks on a string
59
+ // of dashes and spaces. The value is trimmed here instead.
60
+ let cursor = 0;
61
+ while (cursor < trimmed.length && trimmed[cursor] === "-") {
62
+ cursor += 1;
63
+ }
64
+ const value =
65
+ cursor === 0
66
+ ? ""
67
+ : trimmed
68
+ .slice(cursor)
69
+ .trim()
70
+ .replace(/^['"]|['"]$/g, "");
71
+ if (value !== "") {
72
+ patterns.push(value);
60
73
  }
61
74
  }
62
75
 
@@ -123,6 +136,15 @@ async function expandGlob(root: string, segments: string[], fs: FileSystem): Pro
123
136
  return prefixes.filter((prefix) => prefix !== "" && prefix !== ".");
124
137
  }
125
138
 
139
+ /** Linear replacement for `/+$`, which backtracks on a run of slashes. */
140
+ function stripTrailingSlashes(value: string): string {
141
+ let end = value.length;
142
+ while (end > 0 && value[end - 1] === "/") {
143
+ end -= 1;
144
+ }
145
+ return value.slice(0, end);
146
+ }
147
+
126
148
  export async function findWorkspacePackages(
127
149
  root: string,
128
150
  patterns: readonly string[],
@@ -131,7 +153,7 @@ export async function findWorkspacePackages(
131
153
  const found = new Map<string, WorkspacePackage>();
132
154
 
133
155
  for (const pattern of patterns) {
134
- const cleaned = pattern.replace(/\\/g, "/").replace(/\/+$/, "").replace(/^\.\//, "");
156
+ const cleaned = stripTrailingSlashes(pattern.replace(/\\/g, "/")).replace(/^\.\//, "");
135
157
  if (cleaned === "" || cleaned === ".") {
136
158
  continue;
137
159
  }
@@ -165,6 +187,6 @@ export async function readPnpmWorkspace(root: string, fs: FileSystem): Promise<s
165
187
 
166
188
  /** Keep only the packages matching a scope string, by relative path or name. */
167
189
  export function scopeMatches(relative: string, scope: string): boolean {
168
- const needle = scope.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
190
+ const needle = stripTrailingSlashes(scope.replace(/\\/g, "/")).replace(/^\.\//, "");
169
191
  return relative === needle || relative.includes(needle);
170
192
  }