import type { IMatcher, IDomainMatchOptions } from '../types.js'; /** * DomainMatcher provides comprehensive domain matching functionality * Supporting exact matches, wildcards, and case-insensitive matching */ export class DomainMatcher implements IMatcher { private static wildcardToRegex(pattern: string): RegExp { // Escape special regex characters except * const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&'); // Replace * with regex equivalent const regexPattern = escaped.replace(/\*/g, '.*'); return new RegExp(`^${regexPattern}$`, 'i'); } /** * Match a domain pattern against a hostname * @param pattern The pattern to match (supports wildcards like *.example.com) * @param hostname The hostname to test * @param options Matching options * @returns true if the hostname matches the pattern */ static match( pattern: string, hostname: string, options: IDomainMatchOptions = {} ): boolean { // Handle null/undefined cases if (!pattern || !hostname) { return false; } // Normalize inputs const normalizedPattern = pattern.toLowerCase().trim(); const normalizedHostname = hostname.toLowerCase().trim(); // Remove trailing dots (FQDN normalization) const cleanPattern = normalizedPattern.replace(/\.$/, ''); const cleanHostname = normalizedHostname.replace(/\.$/, ''); // Exact match (most common case) if (cleanPattern === cleanHostname) { return true; } // Wildcard matching if (options.allowWildcards !== false && cleanPattern.includes('*')) { const regex = this.wildcardToRegex(cleanPattern); return regex.test(cleanHostname); } // No match return false; } /** * Check if a pattern contains wildcards */ static isWildcardPattern(pattern: string): boolean { return pattern.includes('*'); } /** * Calculate the specificity of a domain pattern * Higher values mean more specific patterns */ static calculateSpecificity(pattern: string): number { if (!pattern) return 0; let score = 0; // Exact domains are most specific if (!pattern.includes('*')) { score += 100; } // Count domain segments const segments = pattern.split('.'); score += segments.length * 10; // Penalize wildcards based on position if (pattern.startsWith('*')) { score -= 50; // Leading wildcard is very generic } else if (pattern.includes('*')) { score -= 20; // Wildcard elsewhere is less generic } // Bonus for longer patterns score += pattern.length; return score; } /** * Find all matching patterns from a list * Returns patterns sorted by specificity (most specific first) */ static findAllMatches( patterns: string[], hostname: string, options: IDomainMatchOptions = {} ): string[] { const matches = patterns.filter(pattern => this.match(pattern, hostname, options) ); // Sort by specificity (highest first) return matches.sort((a, b) => this.calculateSpecificity(b) - this.calculateSpecificity(a) ); } /** * Instance method for interface compliance */ match(pattern: string, hostname: string, options?: IDomainMatchOptions): boolean { return DomainMatcher.match(pattern, hostname, options); } }