@neurolint/cli
Version:
NeuroLint CLI - Deterministic code fixing for TypeScript, JavaScript, React, and Next.js with 8-layer architecture including Security Forensics, Next.js 16, React Compiler, and Turbopack support
239 lines (193 loc) • 6.45 kB
JavaScript
/**
* Copyright (c) 2025 NeuroLint
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
;
const REGEX_TIMEOUT_MS = 100;
const MAX_INPUT_LENGTH = 1024 * 1024;
const MAX_PATTERN_COMPLEXITY = 500;
function isPatternSafe(patternStr) {
const dangerousPatterns = [
/\(\.\*\)\+/,
/\(\.\+\)\+/,
/\(\.\*\)\*/,
/\(\.\+\)\*/,
/\([\s\S]*?\)\{0,\d{4,}\}/,
/\[\\s\\S\]\{0,\d{4,}\}/,
/\(\?:[^)]+\)\+\+/,
/\(\?:[^)]+\)\*\*/,
];
for (const dangerous of dangerousPatterns) {
if (dangerous.test(patternStr)) {
return { safe: false, reason: 'Contains catastrophic backtracking pattern' };
}
}
const nestedQuantifiers = /\([^)]*[+*]\)[+*]/;
if (nestedQuantifiers.test(patternStr)) {
return { safe: false, reason: 'Contains nested quantifiers' };
}
if (patternStr.length > MAX_PATTERN_COMPLEXITY) {
return { safe: false, reason: 'Pattern too complex' };
}
return { safe: true };
}
function simplifyPatternForSafety(pattern) {
let source = typeof pattern === 'string' ? pattern : pattern.source;
let flags = typeof pattern === 'string' ? 'gi' : pattern.flags;
source = source
.replace(/\[\\s\\S\]\{0,1000\}/g, '[\\s\\S]{0,150}')
.replace(/\[\\s\\S\]\{0,500\}/g, '[\\s\\S]{0,150}')
.replace(/\[\\s\\S\]\{0,300\}/g, '[\\s\\S]{0,100}')
.replace(/\[\\s\\S\]\{0,200\}/g, '[\\s\\S]{0,100}');
source = source
.replace(/\(\.\*\)\+/g, '(.*?)')
.replace(/\(\.\+\)\+/g, '(.+?)')
.replace(/\(\.\*\)\*/g, '(.*?)')
.replace(/\(\.\+\)\*/g, '(.+?)');
return new RegExp(source, flags);
}
function chunkInput(input, maxChunkSize = 50000) {
if (input.length <= maxChunkSize) {
return [input];
}
const chunks = [];
const overlap = 500;
for (let i = 0; i < input.length; i += maxChunkSize - overlap) {
chunks.push(input.substring(i, i + maxChunkSize));
}
return chunks;
}
class SafeRegex {
constructor(pattern, flags = '') {
const source = typeof pattern === 'string' ? pattern : pattern.source;
const patternFlags = typeof pattern === 'string' ? flags : (pattern.flags || flags);
const safetyCheck = isPatternSafe(source);
if (!safetyCheck.safe) {
const simplified = simplifyPatternForSafety(pattern);
this.pattern = simplified.source;
this.flags = simplified.flags;
this.wasSimplified = true;
this.simplificationReason = safetyCheck.reason;
} else {
this.pattern = source;
this.flags = patternFlags;
this.wasSimplified = false;
}
this.regex = new RegExp(this.pattern, this.flags);
this.execCount = 0;
this.maxExecCount = 1000;
}
static fromRegExp(regex) {
return new SafeRegex(regex.source, regex.flags);
}
exec(input, options = {}) {
const maxLength = options.maxInputLength || MAX_INPUT_LENGTH;
if (input.length > maxLength) {
return { match: null, truncated: true, originalLength: input.length };
}
this.execCount++;
if (this.execCount > this.maxExecCount) {
return { match: null, limitReached: true };
}
const startTime = Date.now();
try {
const match = this.regex.exec(input);
const elapsed = Date.now() - startTime;
return { match, elapsed, wasSimplified: this.wasSimplified };
} catch (error) {
return { match: null, error: error.message };
}
}
test(input, options = {}) {
const result = this.exec(input, options);
return result.match !== null;
}
matchAll(input, options = {}) {
const maxLength = options.maxInputLength || MAX_INPUT_LENGTH;
const maxMatches = options.maxMatches || 100;
const chunkSize = options.chunkSize || 50000;
if (input.length > maxLength) {
return { matches: [], truncated: true, originalLength: input.length };
}
const matches = [];
const startTime = Date.now();
const maxTotalTime = options.timeout || REGEX_TIMEOUT_MS * 10;
const chunks = chunkInput(input, chunkSize);
let globalOffset = 0;
for (const chunk of chunks) {
if (Date.now() - startTime > maxTotalTime) {
return { matches, timedOut: true, elapsed: Date.now() - startTime };
}
const globalFlags = this.flags.includes('g') ? this.flags : this.flags + 'g';
const globalRegex = new RegExp(this.pattern, globalFlags);
let match;
let iterCount = 0;
const maxIterations = 1000;
while ((match = globalRegex.exec(chunk)) !== null) {
iterCount++;
if (iterCount > maxIterations) {
break;
}
if (matches.length >= maxMatches) {
return { matches, limitReached: true };
}
matches.push({
text: match[0],
index: globalOffset + match.index,
groups: match.slice(1)
});
if (match.index === globalRegex.lastIndex) {
globalRegex.lastIndex++;
}
}
globalOffset += chunk.length - 500;
}
return {
matches: this.deduplicateMatches(matches),
elapsed: Date.now() - startTime,
wasSimplified: this.wasSimplified
};
}
deduplicateMatches(matches) {
const seen = new Set();
return matches.filter(m => {
const key = `${m.index}:${m.text}`;
if (seen.has(key)) return false;
seen.add(key);
return true;
});
}
reset() {
this.execCount = 0;
this.regex.lastIndex = 0;
}
}
function executeWithTimeout(regex, input, timeoutMs = REGEX_TIMEOUT_MS) {
const safeRegex = SafeRegex.fromRegExp(regex);
return safeRegex.matchAll(input, { timeout: timeoutMs });
}
function prevalidatePattern(pattern) {
const source = typeof pattern === 'string' ? pattern : pattern.source;
return isPatternSafe(source);
}
module.exports = {
SafeRegex,
executeWithTimeout,
prevalidatePattern,
simplifyPatternForSafety,
isPatternSafe,
chunkInput,
REGEX_TIMEOUT_MS,
MAX_INPUT_LENGTH,
MAX_PATTERN_COMPLEXITY
};