Files
SnowLuma/.pnpm-store/v11/files/a4/17a071edf65eb70c412030737cb74e21c9271c39543c2b0ca7f34c85c43f66e3de5f3e81f5a8018c4952059e0662a1388e24d9c57b0ee0998265e04022fba5
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) Skipped
docs: 仓库链接指向本镜像仓库
2026-11-13 09:27:07 +08:00

191 lines
4.6 KiB
Plaintext

import TrieNode from './TrieNode'
interface GetAllSubCombosHelperOptions {
substr: string
limit: number
trieRoot: TrieNode
}
export interface PasswordChanges {
letter: string
substitution: string
}
export type IndexedPasswordChanges = PasswordChanges & { i: number }
export interface PasswordWithSubs {
password: string
changes: IndexedPasswordChanges[]
isFullSubstitution: boolean
}
interface HelperOptions {
onlyFullSub: boolean
isFullSub: boolean
index: number
subIndex: number
changes: IndexedPasswordChanges[]
lastSubLetter?: string
consecutiveSubCount: number
}
class CleanPasswords {
private substr: string
private buffer: string[] = []
private limit: number
private trieRoot: TrieNode
private finalPasswords: PasswordWithSubs[] = []
constructor({ substr, limit, trieRoot }: GetAllSubCombosHelperOptions) {
this.substr = substr
this.limit = limit
this.trieRoot = trieRoot
}
private getAllPossibleSubsAtIndex(index: number) {
const nodes: TrieNode[] = []
let cur = this.trieRoot
for (let i = index; i < this.substr.length; i += 1) {
const character = this.substr.charAt(i)
cur = cur.getChild(character)!
if (!cur) {
break
}
nodes.push(cur)
}
return nodes
}
// eslint-disable-next-line complexity,max-statements
private helper({
onlyFullSub,
isFullSub,
index,
subIndex,
changes,
lastSubLetter,
consecutiveSubCount,
}: HelperOptions): void {
if (this.finalPasswords.length >= this.limit) {
return
}
if (index === this.substr.length) {
if (onlyFullSub === isFullSub) {
this.finalPasswords.push({
password: this.buffer.join(''),
changes,
isFullSubstitution: onlyFullSub,
})
}
return
}
// first, exhaust all possible substitutions at this index
const nodes: TrieNode[] = [...this.getAllPossibleSubsAtIndex(index)]
let hasSubs = false
// iterate backward to get wider substitutions first
for (let i = index + nodes.length - 1; i >= index; i -= 1) {
const cur = nodes[i - index]
const sub = cur.parents.join('')
if (cur.isTerminal()) {
// Skip if this would be a 4th or more consecutive substitution of the same letter
// this should work in all language as there shouldn't be the same letter more than four times in a row
// So we can ignore the rest to save calculation time
if (lastSubLetter === sub && consecutiveSubCount >= 3) {
continue
}
hasSubs = true
const letters = cur.subs!
for (const letter of letters) {
this.buffer.push(letter)
const newSubs = changes.concat({
i: subIndex,
letter,
substitution: sub,
})
// recursively build the rest of the string
this.helper({
onlyFullSub,
isFullSub,
index: index + sub.length,
subIndex: subIndex + letter.length,
changes: newSubs,
lastSubLetter: sub,
consecutiveSubCount:
lastSubLetter === sub ? consecutiveSubCount + 1 : 1,
})
// backtrack by ignoring the added postfix
this.buffer.pop()
if (this.finalPasswords.length >= this.limit) {
return
}
}
}
}
// next, generate all combos without doing a substitution at this index
// if a partial substitution is requested or there are no substitutions at this index
if (!onlyFullSub || !hasSubs) {
const firstChar = this.substr.charAt(index)
this.buffer.push(firstChar)
this.helper({
onlyFullSub,
isFullSub: isFullSub && !hasSubs,
index: index + 1,
subIndex: subIndex + 1,
changes,
lastSubLetter,
consecutiveSubCount,
})
this.buffer.pop()
}
}
getAll() {
// only full substitution
this.helper({
onlyFullSub: true,
isFullSub: true,
index: 0,
subIndex: 0,
changes: [],
lastSubLetter: undefined,
consecutiveSubCount: 0,
})
// only partial substitution
this.helper({
onlyFullSub: false,
isFullSub: true,
index: 0,
subIndex: 0,
changes: [],
lastSubLetter: undefined,
consecutiveSubCount: 0,
})
return this.finalPasswords
}
}
const getCleanPasswords = (
password: string,
limit: number,
trieRoot: TrieNode,
): PasswordWithSubs[] => {
const helper = new CleanPasswords({
substr: password,
limit,
trieRoot,
})
return helper.getAll()
}
export default getCleanPasswords