import { MatcherBaseClass, MatchOptions, RepeatMatch } from '../../types' import Scoring from '../../scoring' import Matching from '../../Matching' import Options from '../../Options' import { MaybePromise } from 'rollup' /* *------------------------------------------------------------------------------- * repeats (aaa, abcabcabc) ------------------------------ *------------------------------------------------------------------------------- */ class MatchRepeat extends MatcherBaseClass { private scoring: Scoring constructor(options: Options) { super(options) this.scoring = new Scoring(options) } // eslint-disable-next-line max-statements match({ password, omniMatch }: MatchOptions) { const matches: (RepeatMatch | Promise)[] = [] let lastIndex = 0 while (lastIndex < password.length) { const greedyMatch = this.getGreedyMatch(password, lastIndex) const lazyMatch = this.getLazyMatch(password, lastIndex) if (greedyMatch == null) { break } const { match, baseToken } = this.setMatchToken(greedyMatch, lazyMatch) if (match) { const j = match.index + match[0].length - 1 const baseGuesses = this.getBaseGuesses(baseToken, omniMatch) matches.push(this.normalizeMatch(baseToken, j, match, baseGuesses)) lastIndex = j + 1 } } const hasPromises = matches.some((match) => { return match instanceof Promise }) if (hasPromises) { // eslint-disable-next-line @typescript-eslint/await-thenable return Promise.all(matches) } return matches as RepeatMatch[] } // eslint-disable-next-line max-params normalizeMatch( baseToken: string, j: number, match: RegExpExecArray, baseGuesses: number | Promise, ): MaybePromise { const baseMatch: RepeatMatch = { pattern: 'repeat', i: match.index, j, token: match[0], baseToken, baseGuesses: 0, repeatCount: match[0].length / baseToken.length, } if (baseGuesses instanceof Promise) { return baseGuesses.then((resolvedBaseGuesses) => { return { ...baseMatch, baseGuesses: resolvedBaseGuesses, } }) } return { ...baseMatch, baseGuesses, } } getGreedyMatch(password: string, lastIndex: number) { const greedy = /(.+)\1+/g greedy.lastIndex = lastIndex return greedy.exec(password) } getLazyMatch(password: string, lastIndex: number) { const lazy = /(.+?)\1+/g lazy.lastIndex = lastIndex return lazy.exec(password) } setMatchToken( greedyMatch: RegExpExecArray, lazyMatch: RegExpExecArray | null, ) { const lazyAnchored = /^(.+?)\1+$/ let match let baseToken = '' if (lazyMatch && greedyMatch[0].length > lazyMatch[0].length) { // greedy beats lazy for 'aabaab' // greedy: [aabaab, aab] // lazy: [aa, a] match = greedyMatch // greedy's repeated string might itself be repeated, eg. // aabaab in aabaabaabaab. // run an anchored lazy match on greedy's repeated string // to find the shortest repeated string const temp = lazyAnchored.exec(match[0]) if (temp) { baseToken = temp[1] } } else { // lazy beats greedy for 'aaaaa' // greedy: [aaaa, aa] // lazy: [aaaaa, a] match = lazyMatch if (match) { baseToken = match[1] } } return { match, baseToken, } } getBaseGuesses(baseToken: string, omniMatch: Matching) { const matches = omniMatch.match(baseToken) if (matches instanceof Promise) { return matches.then((resolvedMatches) => { const baseAnalysis = this.scoring.mostGuessableMatchSequence( baseToken, resolvedMatches, ) return baseAnalysis.guesses }) } const baseAnalysis = this.scoring.mostGuessableMatchSequence( baseToken, matches, ) return baseAnalysis.guesses } } export default MatchRepeat