import MatchL33t from '../dictionary/variants/matching/l33t.mjs'; import MatchReverse from '../dictionary/variants/matching/reverse.mjs'; import MatchDictionary from '../dictionary/matching.mjs'; import { MatcherBaseClass } from '../../types.mjs'; /* *------------------------------------------------------------------------------- * word sequences (oneTwoThree, fourFiveSix) ------------------------------ *------------------------------------------------------------------------------- */ class MatchWordSequence extends MatcherBaseClass { constructor(options) { super(options); this.dictionary = new MatchDictionary(this.options, true); this.dictionaryL33t = new MatchL33t(this.options, true); this.dictionaryReverse = new MatchReverse(this.options, true); } match(matchOptions) { const { password } = matchOptions; // Get all dictionary matches first const dictionaryMatches = this.dictionary.match(matchOptions); const dictionaryL33tMatches = this.dictionaryL33t.match(matchOptions); const dictionaryReverseMatches = this.dictionaryReverse.match(matchOptions); const filteredDictionaryMatches = this.filterDictionaryMatches([...dictionaryMatches, ...dictionaryL33tMatches, ...dictionaryReverseMatches]); // Convert dictionary matches to our internal format const wordMatches = this.convertToWordMatches(filteredDictionaryMatches); // Find sequences of consecutive words return this.findWordSequences(wordMatches, password); } filterDictionaryMatches(matches) { // sort by start index, then by end index return [...matches].sort((a, b) => { if (a.i !== b.i) return a.i - b.i; if (a.j !== b.j) return a.j - b.j; // prefer forward over reversed if (a.reversed !== b.reversed) return a.reversed ? 1 : -1; // prefer non-l33t if (a.l33t !== b.l33t) return a.l33t ? 1 : -1; // prefer better rank return a.rank - b.rank; }) // Keep only non-overlapping matches, favoring earlier ones .reduce((acc, match) => { const last = acc[acc.length - 1]; if (!last || match.i > last.j) { acc.push(match); } return acc; }, []); } convertToWordMatches(dictionaryMatches) { return dictionaryMatches.map(match => ({ word: match.matchedWord, i: match.i, j: match.j, rank: match.rank, dictionaryName: match.dictionaryName })); } findWordSequences(wordMatches, password) { const sequences = []; if (wordMatches.length === 0) { return sequences; } // Sort matches by start position const sortedMatches = [...wordMatches].sort((a, b) => a.i - b.i); // Find all possible sequences for (let startIdx = 0; startIdx < sortedMatches.length; startIdx += 1) { const sequencesFromStart = this.findSequencesFromStart(sortedMatches, startIdx, password); sequences.push(...sequencesFromStart); } return sequences; } // eslint-disable-next-line max-statements findSequencesFromStart(sortedMatches, startIdx, password) { const sequences = []; const startMatch = sortedMatches[startIdx]; // Start with single word sequence let currentSequence = [startMatch]; let currentEnd = startMatch.j; // Try to extend the sequence for (let nextIdx = startIdx + 1; nextIdx < sortedMatches.length; nextIdx += 1) { const nextMatch = sortedMatches[nextIdx]; // Only extend if the next word can form a valid sequence if (this.isValidWordSequence(currentSequence, nextMatch, password)) { currentSequence.push(nextMatch); currentEnd = nextMatch.j; } else if (nextMatch.i > currentEnd) { // Gap found, save current sequence and start new one if (currentSequence.length > 1) { sequences.push(this.createWordSequenceMatch(currentSequence, password)); } currentSequence = [nextMatch]; currentEnd = nextMatch.j; } // If nextMatch.i <= currentEnd, it overlaps, so we skip it } // Don't forget the last sequence if (currentSequence.length > 1) { sequences.push(this.createWordSequenceMatch(currentSequence, password)); } return sequences; } isValidWordSequence(currentSequence, nextMatch, password) { // Get the text between the last word and the next word const lastWord = currentSequence[currentSequence.length - 1]; const textBetween = password.slice(lastWord.j + 1, nextMatch.i); // Check for common word separators const separators = ['', ' ', '-', '_', '.', '']; // For simple concatenation (no separator) const isSimpleConcat = textBetween === ''; // For snake_case or kebab-case, check for separators const hasValidSeparator = separators.some(sep => textBetween === sep); // For camelCase, we need to check if the next word starts with uppercase // and there's no separator (or just a single character that could be uppercase) const isCamelCase = textBetween.length === 1 && textBetween === textBetween.toUpperCase() && textBetween !== textBetween.toLowerCase(); return hasValidSeparator || isSimpleConcat || isCamelCase; } createWordSequenceMatch(sequence, password) { const firstMatch = sequence[0]; const lastMatch = sequence[sequence.length - 1]; const words = sequence.map(match => match.word); // Determine if sequence is ascending (by rank) const ranks = sequence.map(match => match.rank); const ascending = ranks.every((rank, i) => i === 0 || rank >= ranks[i - 1]); // Use the most common dictionary name, or the first one const dictionaryNames = sequence.map(match => match.dictionaryName); const dictionaryName = this.getMostCommon(dictionaryNames) || firstMatch.dictionaryName; return { pattern: 'wordSequence', i: firstMatch.i, j: lastMatch.j, token: password.slice(firstMatch.i, lastMatch.j + 1), words, wordCount: words.length, dictionaryName, ascending }; } getMostCommon(array) { if (array.length === 0) return null; const counts = new Map(); let maxCount = 0; let mostCommon = null; array.forEach(item => { const count = (counts.get(item) || 0) + 1; counts.set(item, count); if (count > maxCount) { maxCount = count; mostCommon = item; } }); return mostCommon; } } export { MatchWordSequence as default }; //# sourceMappingURL=matching.mjs.map