Skip to content

Commit

Permalink
Stop calculating accurate backtrack count if it's too expensive
Browse files Browse the repository at this point in the history
  • Loading branch information
tjenkinson committed Oct 19, 2024
1 parent 38846de commit 31d6f8d
Show file tree
Hide file tree
Showing 3 changed files with 40 additions and 27 deletions.
2 changes: 1 addition & 1 deletion src/__snapshots__/redos-detector.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -324739,7 +324739,7 @@ exports[`RedosDetector isSafe cases /^(a{252,253}){1,2}$/ 1`] = `null`;
exports[`RedosDetector isSafe cases /^(a{252,253}){1,2}$/ 2`] = `
{
"infinite": false,
"value": 1,
"value": 100,
}
`;

Expand Down
63 changes: 38 additions & 25 deletions src/collect-results.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,13 @@ function getLongestMatch(
inputStringSchema: readonly CharacterGroups[],
trailSides: readonly TrailEntrySide[],
): readonly TrailEntrySide[] {
/* istanbul ignore next */
if (trailSides.length > inputStringSchema.length) {
throw new Error(
'Internal error: trail should be <= than input string schema',
);
}
const noMatchOffset = trailSides.findIndex((side, i) => {
if (i >= inputStringSchema.length) return true;
const res = isEmptyCache.getResult(
side.characterGroups,
inputStringSchema[i],
Expand All @@ -47,7 +52,7 @@ class EnhancedTrail {
private readonly inputStringSchema: readonly CharacterGroups[];
private readonly tree = new Tree<readonly TrailEntrySide[]>((x) => x);

public get otherPotentialMatches(): readonly (readonly TrailEntrySide[])[] {
public get matches(): readonly (readonly TrailEntrySide[])[] {
return this.tree.items;
}

Expand All @@ -56,7 +61,12 @@ class EnhancedTrail {
this.onNewTrail(this);
}

public onNewTrail(otherTrail: EnhancedTrail): void {
public onNewTrail(otherTrail: EnhancedTrail): number /* cost */ {
if (otherTrail.trail.length > this.inputStringSchema.length) {
// this one will just be an extension of one we already saw
return 0;
}

const leftSide: TrailEntrySide[] = [];
const rightSide: TrailEntrySide[] = [];
for (const entry of otherTrail.trail) {
Expand All @@ -69,17 +79,14 @@ class EnhancedTrail {

const rightMatch = getLongestMatch(this.inputStringSchema, rightSide);
if (rightMatch.length > 0) this.tree.add(rightMatch);

return otherTrail.trail.length;
}
}

export type CollectResultsTrail = {
trail: Trail;
otherPotentialMatches: readonly (readonly TrailEntrySide[])[];
};

export type CollectResultsResult = Readonly<{
error: RedosDetectorError | null;
trails: readonly CollectResultsTrail[];
trails: readonly Trail[];
worstCaseBacktrackCount: number;
}>;

Expand Down Expand Up @@ -124,27 +131,38 @@ export function collectResults({

const trailsTree: Tree<EnhancedTrail> = new Tree(({ trail }) => trail);
let worstCaseBacktrackCount = 0;
let work = 0;
let next: ReaderResult<CheckerReaderValue, CheckerReaderReturn>;

outer: while (!(next = reader.next()).done) {
switch (next.value.type) {
case checkerReaderTypeTrail: {
const trail = new EnhancedTrail(next.value.trail);

for (const existingTrail of trailsTree.items) {
trail.onNewTrail(existingTrail);
existingTrail.onNewTrail(trail);
const updateWorstBacktrackCount = ({
matches,
}: EnhancedTrail): void => {
const { length } = matches;
if (length - 1 > worstCaseBacktrackCount) {
worstCaseBacktrackCount = length - 1;
}
};

if (work < 50_000) {
for (const existingTrail of trailsTree.items) {
work += trail.onNewTrail(existingTrail);
work += existingTrail.onNewTrail(trail);
updateWorstBacktrackCount(existingTrail);
}
updateWorstBacktrackCount(trail);
} else {
// it's too costly to continue calculating an accurate count, so fall back to assuming the input string that matches the largest
// group of trails would also match every new trail
worstCaseBacktrackCount += 1;
}

trailsTree.add(trail);

worstCaseBacktrackCount =
Math.max(
...trailsTree.items.map(
({ otherPotentialMatches }) => otherPotentialMatches.length,
),
) - 1;

if (worstCaseBacktrackCount > maxBacktracks) {
break outer;
}
Expand All @@ -169,12 +187,7 @@ export function collectResults({

return {
error,
trails: trailsTree.items.map(({ otherPotentialMatches, trail }) => {
return {
otherPotentialMatches,
trail,
};
}),
trails: trailsTree.items.map(({ trail }) => trail),
worstCaseBacktrackCount,
};
}
2 changes: 1 addition & 1 deletion src/redos-detector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@ export function isSafePattern(
}),
pattern,
patternDowngraded,
trails: result.trails.map(({ trail }) => {
trails: result.trails.map((trail) => {
const safeRegexTrail: RedosDetectorTrail = {
trail: trail.map(({ left, right }) => {
const entry: RedosDetectorTrailEntry = {
Expand Down

0 comments on commit 31d6f8d

Please sign in to comment.