mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-15 04:18:05 +00:00
fix(r): reject only a real (?< group in exportPattern, not one inside a character class
The linear-regex parser refused any pattern whose text contained "(?<", so [(?<] and \(?<x were dropped as look-behind. Reject at the group the parser reaches, which is never inside a class or after a backslash. A real look-behind or named group is still refused, after any class, with the same linear parse.
This commit is contained in:
parent
3f786ef94a
commit
7980dba6ce
2 changed files with 48 additions and 7 deletions
|
|
@ -325,15 +325,10 @@ const BRACES = /\{(\d+)(?:(,)(\d*))?\}/y;
|
|||
class Parser {
|
||||
private pos = 0;
|
||||
private depth = 0;
|
||||
/** `\k` is an identity escape only when the pattern has no named group. */
|
||||
private readonly hasNamedGroup: boolean;
|
||||
|
||||
constructor(private readonly src: string) {
|
||||
this.hasNamedGroup = src.includes('(?<');
|
||||
}
|
||||
constructor(private readonly src: string) {}
|
||||
|
||||
parse(): Node {
|
||||
if (this.hasNamedGroup) reject('named groups and look-behind are unsupported');
|
||||
const node = this.parseDisjunction();
|
||||
if (this.pos < this.src.length) reject('unmatched )');
|
||||
return node;
|
||||
|
|
@ -414,6 +409,9 @@ class Parser {
|
|||
this.pos++; // (
|
||||
if (this.peek() === '?') {
|
||||
// `(?:` is a plain group; `(?=`, `(?!`, `(?<` and everything else is unsupported or invalid.
|
||||
// Only a `(` the parser reaches here opens a group: one inside a character class
|
||||
// (`[(?<]`) or after a backslash (`\(?<`) is a literal and never gets this far.
|
||||
if (this.peek(1) === '<') reject('named groups and look-behind are unsupported');
|
||||
if (this.peek(1) !== ':') reject('look-around and group modifiers are unsupported');
|
||||
this.pos += 2;
|
||||
}
|
||||
|
|
@ -543,7 +541,7 @@ class Parser {
|
|||
return { code: 0x75 };
|
||||
}
|
||||
case 'k':
|
||||
// Identity escape (no named groups exist; `parse` already rejected those).
|
||||
// Identity escape (no named groups exist; `parseGroup` rejects those).
|
||||
return { code: 0x6b };
|
||||
default:
|
||||
if (c >= '1' && c <= '9') reject('back-reference or legacy octal escape');
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|||
import { compileRExportPattern } from '../../src/core/ingestion/languages/r/export-pattern.js';
|
||||
import {
|
||||
compileLinearRegex,
|
||||
compileLinearRegexDetailed,
|
||||
MAX_STATES,
|
||||
} from '../../src/core/ingestion/languages/r/linear-regex.js';
|
||||
|
||||
|
|
@ -733,6 +734,48 @@ describe('compileRExportPattern is linear-time on catastrophic patterns', () =>
|
|||
}
|
||||
});
|
||||
|
||||
describe('"(?<" is only a group opener outside a character class and an escape', () => {
|
||||
it.each([
|
||||
['[(?<]', ['(', '?', '<'], ['a', '']],
|
||||
['[^(?<]x', ['ax'], ['(x', '<x']],
|
||||
['^[(?<]+$', ['(<?', '<'], ['a<', '']],
|
||||
['[(?<]\\k', ['(k'], ['k']],
|
||||
['\\(?<x', ['<x', '(<x'], ['x']],
|
||||
['\\[[(?<]', ['[<', '[('], ['<']],
|
||||
['[\\]](?:a)', [']a'], ['a']],
|
||||
['[\\](?<]x', ['(x', ']x', '<x'], ['ax']],
|
||||
['[[(?<]', ['[', '('], ['a']],
|
||||
])('compiles %s and matches it as written', (pattern, matching, notMatching) => {
|
||||
const regex = compileLinearRegex(pattern);
|
||||
expect(regex, pattern).not.toBeNull();
|
||||
for (const name of matching) expect(regex?.test(name), `${pattern} ~ ${name}`).toBe(true);
|
||||
for (const name of notMatching) {
|
||||
expect(regex?.test(name), `${pattern} !~ ${name}`).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it.each([
|
||||
['a real look-behind', '(?<=x)y'],
|
||||
['a negative look-behind', '(?<!x)y'],
|
||||
['a named group', '(?<n>a)'],
|
||||
['after a closed class', '[ab](?<=x)'],
|
||||
['after a class holding an escaped ]', '[\\]](?<=x)'],
|
||||
['after an escaped [', '\\[(?<=x)'],
|
||||
['after a class that holds (?<', '[(?<](?<=x)'],
|
||||
['inside a group', '(a|(?<=x)b)'],
|
||||
])('still rejects %s', (_label, pattern) => {
|
||||
expect(compileLinearRegex(pattern), pattern).toBeNull();
|
||||
});
|
||||
|
||||
it('reports a named group or look-behind as such', () => {
|
||||
for (const pattern of ['(?<=x)y', '(?<n>a)']) {
|
||||
const result = compileLinearRegexDetailed(pattern);
|
||||
expect(result.regex).toBeNull();
|
||||
expect(result.reason).toContain('look-behind');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('bounds the matcher by pattern size, not by the name it is run against', () => {
|
||||
// The state count is fixed at compile time and capped, so the work per tested name
|
||||
// is at most name length x MAX_STATES whatever the pattern says.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue