UNPKG

ailock

Version:

AI-Proof File Guard - Protect sensitive files from accidental AI modifications

144 lines 5.04 kB
import glob from 'fast-glob'; import path from 'path'; import ignore from 'ignore'; import { existsSync, readFileSync } from 'fs'; /** * Glob pattern validation and matching for file operations */ export class GlobValidator { maxPatternLength = 1024; dangerousPatterns = [ '/**/*', // Recursive all files '**/**', // Double recursive '/*', // Root directory ]; /** * Validate a glob pattern for safety */ validateGlobPattern(pattern) { if (!pattern || typeof pattern !== 'string') { throw new Error('Pattern must be a non-empty string'); } // Check pattern length if (pattern.length > this.maxPatternLength) { throw new Error(`Pattern exceeds maximum length of ${this.maxPatternLength} characters`); } // Warn about potentially dangerous patterns for (const dangerous of this.dangerousPatterns) { if (pattern === dangerous || pattern.startsWith(dangerous)) { console.warn(`Warning: Pattern '${pattern}' may match a large number of files`); } } // Check for invalid glob characters at the start if (pattern.startsWith('!')) { throw new Error('Negation patterns are not supported directly'); } } /** * Find files matching glob patterns */ async findMatchingFiles(patterns, options = {}) { const { cwd = process.cwd(), ignore: ignorePatterns = [], absolute = true } = options; const matchedFiles = new Set(); for (const pattern of patterns) { this.validateGlobPattern(pattern); try { const matches = await glob(pattern, { cwd, absolute, dot: true, ignore: ignorePatterns, onlyFiles: true, }); for (const match of matches) { matchedFiles.add(match); } } catch (error) { console.warn(`Failed to match pattern '${pattern}': ${error}`); } } return Array.from(matchedFiles); } /** * Create an ignore instance from gitignore-style patterns */ createIgnoreFilter(patterns) { const ig = ignore(); // Add patterns for (const pattern of patterns) { if (pattern && !pattern.startsWith('#')) { ig.add(pattern.trim()); } } return (filePath) => !ig.ignores(filePath); } /** * Load patterns from a gitignore-style file */ loadPatternsFromFile(filePath) { if (!existsSync(filePath)) { return []; } try { const content = readFileSync(filePath, 'utf-8'); return content .split('\n') .map(line => line.trim()) .filter(line => line && !line.startsWith('#')); } catch (error) { console.warn(`Failed to load patterns from ${filePath}: ${error}`); return []; } } /** * Check if a path matches any of the given patterns */ matchesPattern(filePath, patterns) { const relativePath = path.relative(process.cwd(), filePath); for (const pattern of patterns) { // Simple pattern matching (can be enhanced with minimatch if needed) if (pattern.includes('*') || pattern.includes('?')) { // Convert glob to regex (simplified) const regexPattern = pattern .replace(/\./g, '\\.') .replace(/\*/g, '.*') .replace(/\?/g, '.'); const regex = new RegExp(`^${regexPattern}$`); if (regex.test(relativePath)) { return true; } } else { // Exact match or directory prefix if (relativePath === pattern || relativePath.startsWith(pattern + path.sep)) { return true; } } } return false; } /** * Expand glob patterns to actual file paths */ async expandPatterns(patterns, cwd) { const expanded = []; for (const pattern of patterns) { // Check if it's a glob pattern if (pattern.includes('*') || pattern.includes('?') || pattern.includes('[')) { const matches = await this.findMatchingFiles([pattern], { cwd }); expanded.push(...matches); } else { // It's a direct path const absolutePath = path.isAbsolute(pattern) ? pattern : path.join(cwd || process.cwd(), pattern); if (existsSync(absolutePath)) { expanded.push(absolutePath); } } } return expanded; } } //# sourceMappingURL=GlobValidator.js.map