Files
SnowLuma/.pnpm-store/v11/files/eb/58c5feaf43b0131759b287761a3df2d3a3af4b00e3f0d93afe987b236c655f22e53d467aa62517c0ccbd82462128596144647a7e87d3cdf34cec3586a2a23b
TanTan 2a69b23b47
Dev Build / cleanup (push) Canceled after 0s
Dev Build / typecheck (push) Canceled after 0s
Dev Build / build (linux-arm64, ubuntu-24.04-arm, linux-arm64) (push) Canceled after 0s
Dev Build / build (linux-x64, ubuntu-latest, linux-x64) (push) Canceled after 0s
Dev Build / build (win-x64, windows-latest, win32-x64) (push) Canceled after 0s
Dev Build / dispatch-dev-docker (push) Canceled after 0s
docs: 仓库链接指向本镜像仓库
2026-11-13 09:27:07 +08:00

115 lines
3.8 KiB
Plaintext

import findLevenshteinDistance, {
FindLevenshteinDistanceResult,
} from '../../utils/levenshtein'
import Options from '../../Options'
import {
DictionaryNames,
DictionaryMatch,
UserInputsOptions,
MatcherBaseClass,
RankedDictionaries,
} from '../../types'
import { DictionaryMatchOptions } from './types'
import mergeUserInputDictionary from '../../utils/mergeUserInputDictionary'
class MatchDictionary extends MatcherBaseClass {
constructor(
options: Options,
protected wordSequenceCheck?: boolean,
) {
super(options)
}
private getRangedDictionaries(userInputsOptions?: UserInputsOptions) {
if (this.wordSequenceCheck) {
const rankedDictionaries: RankedDictionaries = {}
const rankedDictionariesMaxWordSize: Record<string, number> = {}
Object.keys(this.options.rankedDictionaries).forEach((key) => {
if (this.options.isWordSequence(key)) {
rankedDictionaries[key] = this.options.rankedDictionaries[key]
rankedDictionariesMaxWordSize[key] =
this.options.rankedDictionariesMaxWordSize[key]
}
})
return {
rankedDictionaries,
rankedDictionariesMaxWordSize,
}
}
return mergeUserInputDictionary(
this.options.rankedDictionaries,
this.options.rankedDictionariesMaxWordSize,
userInputsOptions,
)
}
public match({
password,
userInputsOptions,
useLevenshtein = true,
}: DictionaryMatchOptions) {
const matches: DictionaryMatch[] = []
const passwordLength = password.length
const passwordLower = password.toLowerCase()
const { rankedDictionaries, rankedDictionariesMaxWordSize } =
this.getRangedDictionaries(userInputsOptions)
// eslint-disable-next-line complexity,max-statements
Object.keys(rankedDictionaries).forEach((dictionaryName) => {
const rankedDict = rankedDictionaries[dictionaryName as DictionaryNames]
const longestDictionaryWordSize =
rankedDictionariesMaxWordSize[dictionaryName]
const searchWidth = Math.min(longestDictionaryWordSize, passwordLength)
for (let i = 0; i < passwordLength; i += 1) {
const searchEnd = Math.min(i + searchWidth, passwordLength)
for (let j = i; j < searchEnd; j += 1) {
const usedPassword = passwordLower.slice(i, j + 1 || 9e9)
const isInDictionary = usedPassword in rankedDict
let foundLevenshteinDistance: Partial<FindLevenshteinDistanceResult> =
{}
// only use levenshtein distance on full password to minimize the performance drop
// and because otherwise there would be to many false positives
const isFullPassword = i === 0 && j === passwordLength - 1
if (
this.options.useLevenshteinDistance &&
isFullPassword &&
!isInDictionary &&
useLevenshtein
) {
foundLevenshteinDistance = findLevenshteinDistance(
usedPassword,
rankedDict,
this.options.levenshteinThreshold,
)
}
const isLevenshteinMatch =
Object.keys(foundLevenshteinDistance).length !== 0
if (isInDictionary || isLevenshteinMatch) {
const usedRankPassword = isLevenshteinMatch
? foundLevenshteinDistance.levenshteinDistanceEntry!
: usedPassword
const rank = rankedDict[usedRankPassword]
matches.push({
pattern: 'dictionary',
i,
j,
token: password.slice(i, j + 1 || 9e9),
matchedWord: usedPassword,
rank,
dictionaryName: dictionaryName,
reversed: false,
l33t: false,
...foundLevenshteinDistance,
})
}
}
}
})
return matches
}
}
export default MatchDictionary