Files
SnowLuma/.pnpm-store/v11/files/64/4ff21cb0acc50c42cd3bc1733c9cd12b61d2880d49a07ce37e925293ced123789252427af0762d7d3c53fc3dd729af7a5413d08830148a32e6e78219b706d2
T
TanTan 2a69b23b47
Dev Build / cleanup (push) Waiting to run
Dev Build / typecheck (push) Waiting to run
docs: 仓库链接指向本镜像仓库
2026-11-13 09:27:07 +08:00

112 lines
3.5 KiB
Plaintext

import { DictionaryMatch, L33tMatch } from '../../../../types'
import { DictionaryMatchOptions } from '../../types'
import getCleanPasswords, {
PasswordChanges,
PasswordWithSubs,
} from './unmunger/getCleanPasswords'
import MatchDictionary from '../../matching'
const getExtras = (
passwordWithSubs: PasswordWithSubs,
i: number,
j: number,
) => {
const previousChanges = passwordWithSubs.changes.filter((changes) => {
return changes.i < i
})
const iUnsubbed = previousChanges.reduce((value, change) => {
return value - change.letter.length + change.substitution.length
}, i)
const usedChanges = passwordWithSubs.changes.filter((changes) => {
return changes.i >= i && changes.i <= j
})
const jUnsubbed = usedChanges.reduce(
(value, change) => {
return value - change.letter.length + change.substitution.length
},
j - i + iUnsubbed,
)
const filtered: PasswordChanges[] = []
const subDisplay: string[] = []
usedChanges.forEach((value) => {
const existingIndex = filtered.findIndex((t) => {
return t.letter === value.letter && t.substitution === value.substitution
})
if (existingIndex < 0) {
filtered.push({
letter: value.letter,
substitution: value.substitution,
})
subDisplay.push(`${value.substitution} -> ${value.letter}`)
}
})
return {
i: iUnsubbed,
j: jUnsubbed,
subs: filtered,
subDisplay: subDisplay.join(', '),
}
}
/*
* -------------------------------------------------------------------------------
* Dictionary l33t matching -----------------------------------------------------
* -------------------------------------------------------------------------------
*/
class MatchL33t extends MatchDictionary {
private isAlreadyIncluded(matches: L33tMatch[], newMatch: L33tMatch) {
return matches.some((l33tMatch) => {
return Object.entries(l33tMatch).every(([key, value]) => {
return key === 'subs' || value === newMatch[key]
})
})
}
public match(matchOptions: DictionaryMatchOptions) {
const matches: L33tMatch[] = []
const subbedPasswords = getCleanPasswords(
matchOptions.password,
this.options.l33tMaxSubstitutions,
this.options.trieNodeRoot,
)
let hasFullMatch = false
subbedPasswords.forEach((subbedPassword) => {
if (hasFullMatch) {
return
}
const matchedDictionary = super.match({
...matchOptions,
password: subbedPassword.password,
useLevenshtein: subbedPassword.isFullSubstitution,
})
matchedDictionary.forEach((match: DictionaryMatch) => {
if (!hasFullMatch) {
hasFullMatch =
match.i === 0 && match.j === matchOptions.password.length - 1
}
const extras = getExtras(subbedPassword, match.i, match.j)
const token = matchOptions.password.slice(extras.i, extras.j + 1 || 9e9)
const newMatch: L33tMatch = {
...match,
l33t: true,
token,
...extras,
}
const alreadyIncluded = this.isAlreadyIncluded(matches, newMatch)
// only return the matches that contain an actual substitution
if (token.toLowerCase() !== match.matchedWord && !alreadyIncluded) {
matches.push(newMatch)
}
})
})
// filter single-character l33t matches to reduce noise.
// otherwise '1' matches 'i', '4' matches 'a', both very common English words
// with low dictionary rank.
return matches.filter((match) => match.token.length > 1)
}
}
export default MatchL33t