Files
SnowLuma/.pnpm-store/v11/files/26/ef15e5c9f8eabba46c37e952e381b82760e46223e2b805012e5f34962fa43fbeeb5660895a61bd8a6741dd210b24f9c3fa390146627e6cfa3790bd52cf9dc5
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

118 lines
3.9 KiB
Plaintext

import { sorted, extend } from '../../utils/helper'
import {
MatcherBaseClass,
MatchOptions,
OptionsGraphEntry,
SpatialMatch,
} from '../../types'
type SpatialMatchOptions = Pick<MatchOptions, 'password'>
/*
* ------------------------------------------------------------------------------
* spatial match (qwerty/dvorak/keypad and so on) -----------------------------------------
* ------------------------------------------------------------------------------
*/
class MatchSpatial extends MatcherBaseClass {
SHIFTED_RX = /[~!@#$%^&*()_+QWERTYUIOP{}|ASDFGHJKL:"ZXCVBNM<>?]/
match({ password }: SpatialMatchOptions) {
const matches: SpatialMatch[] = []
Object.keys(this.options.graphs).forEach((graphName) => {
const graph = this.options.graphs[graphName]
extend(matches, this.helper(password, graph, graphName))
})
return sorted(matches)
}
checkIfShifted(graphName: string, password: string, index: number) {
if (
!graphName.includes('keypad') &&
// initial character is shifted
this.SHIFTED_RX.test(password.charAt(index))
) {
return 1
}
return 0
}
// eslint-disable-next-line complexity, max-statements
helper(password: string, graph: OptionsGraphEntry, graphName: string) {
let shiftedCount
const matches: SpatialMatch[] = []
let i = 0
const passwordLength = password.length
while (i < passwordLength - 1) {
let j = i + 1
let lastDirection: number | null = null
let turns = 0
shiftedCount = this.checkIfShifted(graphName, password, i)
while (true) {
const prevChar = password.charAt(j - 1)
const adjacents = graph[prevChar] || []
let found = false
let curDirection = -1
// consider growing pattern by one character if j hasn't gone over the edge.
if (j < passwordLength) {
const curChar = password.charAt(j)
const adjacentsLength = adjacents.length
for (let k = 0; k < adjacentsLength; k += 1) {
const adjacent = adjacents[k]
curDirection += 1
// eslint-disable-next-line max-depth
if (adjacent) {
const adjacentIndex = adjacent.indexOf(curChar)
// eslint-disable-next-line max-depth
if (adjacentIndex !== -1) {
found = true
// eslint-disable-next-line max-depth
if (adjacentIndex === 1) {
// # index 1 in the adjacency means the key is shifted,
// # 0 means unshifted: A vs a, % vs 5, etc.
// # for example, 'q' is adjacent to the entry '2@'.
// # @ is shifted w/ index 1, 2 is unshifted.
shiftedCount += 1
}
const foundDirection = curDirection
// eslint-disable-next-line max-depth
if (lastDirection !== foundDirection) {
// # adding a turn is correct even in the initial
// case when last_direction is null:
// # every spatial pattern starts with a turn.
turns += 1
lastDirection = foundDirection
}
break
}
}
}
}
// if the current pattern continued, extend j and try to grow again
if (found) {
j += 1
// otherwise push the pattern discovered so far, if any...
} else {
// don't consider length 1 or 2 chains.
if (j - i > 2) {
matches.push({
pattern: 'spatial',
i,
j: j - 1,
token: password.slice(i, j),
graph: graphName,
turns,
shiftedCount,
})
}
// ...and then start a new search for the rest of the password.
i = j
break
}
}
}
return matches
}
}
export default MatchSpatial