Skip to content

Commit 6bc8e84

Browse files
authored
Remove trie deduplication (#431)
1 parent 5bcd30b commit 6bc8e84

File tree

4 files changed

+164
-76
lines changed

4 files changed

+164
-76
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
"size-limit": [
5151
{
5252
"path": "dist/index.js",
53-
"limit": "2.15 kB"
53+
"limit": "2 kB"
5454
}
5555
],
5656
"ts-scripts": {

src/cases.spec.ts

Lines changed: 130 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1504,6 +1504,13 @@ export const MATCH_TESTS: MatchTestSet[] = [
15041504
params: { path: ["test"], ext: "html" },
15051505
},
15061506
},
1507+
{
1508+
input: "/test/nested.html",
1509+
expected: {
1510+
path: "/test/nested.html",
1511+
params: { path: ["test", "nested"], ext: "html" },
1512+
},
1513+
},
15071514
{
15081515
input: "/test.html/nested",
15091516
expected: {
@@ -1550,6 +1557,128 @@ export const MATCH_TESTS: MatchTestSet[] = [
15501557
},
15511558
],
15521559
},
1560+
{
1561+
path: "{*path}",
1562+
tests: [
1563+
{
1564+
input: "",
1565+
expected: { path: "", params: {} },
1566+
},
1567+
{
1568+
input: "/",
1569+
expected: { path: "/", params: { path: ["", ""] } },
1570+
},
1571+
{
1572+
input: "/test",
1573+
expected: { path: "/test", params: { path: ["", "test"] } },
1574+
},
1575+
{
1576+
input: "/test/nested",
1577+
expected: {
1578+
path: "/test/nested",
1579+
params: { path: ["", "test", "nested"] },
1580+
},
1581+
},
1582+
],
1583+
},
1584+
{
1585+
path: "{*path}",
1586+
options: { end: false },
1587+
tests: [
1588+
{
1589+
input: "",
1590+
expected: { path: "", params: {} },
1591+
},
1592+
{
1593+
input: "/",
1594+
expected: { path: "/", params: { path: ["", ""] } },
1595+
},
1596+
{
1597+
input: "/test",
1598+
expected: { path: "/test", params: { path: ["", "test"] } },
1599+
},
1600+
{
1601+
input: "/test/nested",
1602+
expected: {
1603+
path: "/test/nested",
1604+
params: { path: ["", "test", "nested"] },
1605+
},
1606+
},
1607+
],
1608+
},
1609+
{
1610+
path: "/*path",
1611+
options: { end: false },
1612+
tests: [
1613+
{
1614+
input: "",
1615+
expected: false,
1616+
},
1617+
{
1618+
input: "/",
1619+
expected: false,
1620+
},
1621+
{
1622+
input: "/test",
1623+
expected: { path: "/test", params: { path: ["test"] } },
1624+
},
1625+
{
1626+
input: "/test/nested",
1627+
expected: {
1628+
path: "/test/nested",
1629+
params: { path: ["test", "nested"] },
1630+
},
1631+
},
1632+
],
1633+
},
1634+
{
1635+
path: "/*path/",
1636+
options: { end: false },
1637+
tests: [
1638+
{
1639+
input: "/foo/bar/",
1640+
expected: { path: "/foo/bar/", params: { path: ["foo", "bar"] } },
1641+
},
1642+
{
1643+
input: "/foo/bar/baz",
1644+
expected: false,
1645+
},
1646+
{
1647+
input: "/foo/bar/baz/",
1648+
expected: {
1649+
path: "/foo/bar/baz/",
1650+
params: { path: ["foo", "bar", "baz"] },
1651+
},
1652+
},
1653+
],
1654+
},
1655+
{
1656+
path: "/*path.:ext",
1657+
options: { end: false },
1658+
tests: [
1659+
{
1660+
input: "/foo/bar.html",
1661+
expected: {
1662+
path: "/foo/bar.html",
1663+
params: { path: ["foo", "bar"], ext: "html" },
1664+
},
1665+
},
1666+
{
1667+
input: "/foo/bar.html/baz",
1668+
expected: {
1669+
path: "/foo/bar.html",
1670+
params: { path: ["foo", "bar"], ext: "html" },
1671+
},
1672+
},
1673+
{
1674+
input: "/foo/bar.html/baz.html",
1675+
expected: {
1676+
path: "/foo/bar.html/baz.html",
1677+
params: { path: ["foo", "bar.html", "baz"], ext: "html" },
1678+
},
1679+
},
1680+
],
1681+
},
15531682

15541683
/**
15551684
* Longer prefix.
@@ -2239,7 +2368,7 @@ export const MATCH_TESTS: MatchTestSet[] = [
22392368
input: "%25555....222%25",
22402369
expected: {
22412370
path: "%25555....222%25",
2242-
params: { foo: "555.", bar: ".222" },
2371+
params: { foo: "555..", bar: "222" },
22432372
},
22442373
},
22452374
],

src/index.ts

Lines changed: 23 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -463,7 +463,8 @@ export function pathToRegexp(
463463
sensitive = false,
464464
trailing = true,
465465
} = options;
466-
const root = new SourceNode("^");
466+
const keys: Keys = [];
467+
const sources: string[] = [];
467468
const paths: Array<Path | Path[]> = [path];
468469
let combinations = 0;
469470

@@ -481,49 +482,17 @@ export function pathToRegexp(
481482
throw new PathError("Too many path combinations", data.originalPath);
482483
}
483484

484-
let node = root;
485-
486-
for (const part of toRegExpSource(tokens, delimiter, data.originalPath)) {
487-
node = node.add(part.source, part.key);
488-
}
489-
490-
node.add(""); // Mark the end of the source.
485+
sources.push(toRegExpSource(tokens, delimiter, keys, data.originalPath));
491486
});
492487
}
493488

494-
const keys: Keys = [];
495-
let pattern = toRegExp(root, keys);
489+
let pattern = `^(?:${sources.join("|")})`;
496490
if (trailing) pattern += "(?:" + escape(delimiter) + "$)?";
497491
pattern += end ? "$" : "(?=" + escape(delimiter) + "|$)";
498492

499493
return { regexp: new RegExp(pattern, sensitive ? "" : "i"), keys };
500494
}
501495

502-
function toRegExp(node: SourceNode, keys: Keys): string {
503-
if (node.key) keys.push(node.key);
504-
505-
const children = Object.keys(node.children);
506-
const text = children
507-
.map((id) => toRegExp(node.children[id], keys))
508-
.join("|");
509-
510-
return node.source + (children.length < 2 ? text : `(?:${text})`);
511-
}
512-
513-
class SourceNode {
514-
children: Record<string, SourceNode> = Object.create(null);
515-
516-
constructor(
517-
public source: string,
518-
public key?: Key,
519-
) {}
520-
521-
add(source: string, key?: Key) {
522-
const id = source + ":" + (key ? key.name : "");
523-
return (this.children[id] ||= new SourceNode(source, key));
524-
}
525-
}
526-
527496
/**
528497
* Generate a flat list of sequence tokens from the given tokens.
529498
*/
@@ -549,23 +518,16 @@ function flatten(
549518
callback(result);
550519
}
551520

552-
/**
553-
* Simplest token for the trie deduplication.
554-
*/
555-
interface RegExpPart {
556-
source: string;
557-
key?: Key;
558-
}
559-
560521
/**
561522
* Transform a flat sequence of tokens into a regular expression.
562523
*/
563524
function toRegExpSource(
564525
tokens: Exclude<Token, Group>[],
565526
delimiter: string,
527+
keys: Keys,
566528
originalPath: string | undefined,
567-
): RegExpPart[] {
568-
let result: RegExpPart[] = [];
529+
): string {
530+
let result = "";
569531
let backtrack = "";
570532
let wildcardBacktrack = "";
571533
let prevCaptureType: 0 | 1 | 2 = 0;
@@ -597,7 +559,7 @@ function toRegExpSource(
597559
const token = tokens[index++];
598560

599561
if (token.type === "text") {
600-
result.push({ source: escape(token.value) });
562+
result += escape(token.value);
601563
backtrack += token.value;
602564
if (prevCaptureType === 2) wildcardBacktrack += token.value;
603565
if (token.value.includes(delimiter)) hasSegmentCapture = 0;
@@ -613,34 +575,29 @@ function toRegExpSource(
613575
}
614576

615577
if (token.type === "param") {
616-
result.push({
617-
source:
618-
hasSegmentCapture & 2 // Seen wildcard in segment.
619-
? `(${negate(delimiter, backtrack)}+)`
620-
: hasInSegment(index, "wildcard") // See wildcard later in segment.
621-
? `(${negate(delimiter, peekText(index))}+)`
622-
: hasSegmentCapture & 1 // Seen parameter in segment.
623-
? `(${negate(delimiter, backtrack)}+|${escape(backtrack)})`
624-
: `(${negate(delimiter, "")}+?)`,
625-
key: token,
626-
});
578+
result +=
579+
hasSegmentCapture & 2 // Seen wildcard in segment.
580+
? `(${negate(delimiter, backtrack)}+)`
581+
: hasInSegment(index, "wildcard") // See wildcard later in segment.
582+
? `(${negate(delimiter, peekText(index))}+)`
583+
: hasSegmentCapture & 1 // Seen parameter in segment.
584+
? `(${negate(delimiter, backtrack)}+|${escape(backtrack)})`
585+
: `(${negate(delimiter, "")}+)`;
627586

628587
hasSegmentCapture |= prevCaptureType = 1;
629588
} else {
630-
result.push({
631-
source:
632-
hasSegmentCapture & 2 // Seen wildcard in segment.
633-
? `(${negate(backtrack, "")}+)`
634-
: wildcardBacktrack // No capture in segment, seen wildcard in path.
635-
? `(${negate(wildcardBacktrack, "")}+|${negate(delimiter, "")}+)`
636-
: `([^]+?)`,
637-
key: token,
638-
});
589+
result +=
590+
hasSegmentCapture & 2 // Seen wildcard in segment.
591+
? `(${negate(backtrack, "")}+)`
592+
: wildcardBacktrack // No capture in segment, seen wildcard in path.
593+
? `(${negate(wildcardBacktrack, "")}+|${negate(delimiter, "")}+)`
594+
: `([^]+)`;
639595

640596
wildcardBacktrack = "";
641597
hasSegmentCapture |= prevCaptureType = 2;
642598
}
643599

600+
keys.push(token);
644601
backtrack = "";
645602
continue;
646603
}

src/redos.spec.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ import { MATCH_TESTS } from "./cases.spec.js";
44
import { describe, expect, it } from "vitest";
55

66
describe.concurrent("redos", () => {
7-
it.each(MATCH_TESTS.map((x) => [x.path, pathToRegexp(x.path).regexp]))(
8-
"%s - %s",
9-
{ timeout: 10_000 },
10-
async (_, regexp) => {
11-
const result = await check(regexp.source, regexp.flags);
12-
expect(result.status).toBe("safe");
13-
},
14-
);
7+
it.each(
8+
// Array regex currently has false positives.
9+
MATCH_TESTS.filter((x) => typeof x.path === "string").map((x) => [
10+
x.path,
11+
pathToRegexp(x.path).regexp,
12+
]),
13+
)("%s - %s", { timeout: 10_000 }, async (_, regexp) => {
14+
const result = await check(regexp.source, regexp.flags);
15+
expect(result.status).toBe("safe");
16+
});
1517
});

0 commit comments

Comments
 (0)