// 模式匹配:omnimatch 的 8 个匹配器 + 公共 Match 类型。
//
// 移植来源:dropbox/zxcvbn `src/matching.coffee`(MIT, (c) Dropbox, Inc.)
// 结构参考:shssoichiro/zxcvbn-rs `src/matching/mod.rs`(MIT)
//
// 与上游的两处刻意差异(README 已声明,都是修正):
// 1. 所有下标按 **Unicode 标量值** 计(MoonBit 的 Char 即标量值),而上游按
// UTF-16 码元。上游在 HIV 密码上出过经典的 UTF-16 边界 bug,这里从根上避免。
// 代价是 `token.length` 与上游的 `String::length` 在含非 BMP 字符时不同。
// 2. 词典匹配枚举长度上限为 `max_dictionary_word_length`,语义等价但避免了
// 对不可能命中的超长子串做 O(n²) 次查询。
// ---------------------------------------------------------------------------
// Match 类型
// ---------------------------------------------------------------------------
///|
/// 词典标识。命名与上游 `frequency_lists` 的键一致,便于与官方向量/差分结果对照。
/// (类型定义在 `frequency_lists.mbt`,此处 re-export 语义上属于匹配层输入。)
///|
/// 一次模式匹配。
///
/// 上游用动态对象承载所有字段;这里按 zxcvbn-rs 的强类型范式拆成 enum + 每模式
/// struct,让编译器捕获"某模式没有该字段"一类错误。
pub enum Match {
/// 未被任何模式覆盖的区间(DP 的兜底项)
Bruteforce(BruteforceMatch)
/// 词典(含反向、l33t 变体)
Dictionary(DictionaryMatch)
/// 键盘空间模式
Spatial(SpatialMatch)
/// 重复模式(`abcabcabc`)
Repeat(RepeatMatch)
/// 序列模式(`abc` / `123` / `97531`)
Sequence(SequenceMatch)
/// 正则模式(`recent_year`)
Regex(RegexMatch)
/// 日期模式
Date(DateMatch)
} derive(Eq, Debug)
///|
pub extend Match with Eq::{equal, not_equal}
///|
pub extend Match with @debug.Debug::{to_repr}
///|
/// 暴力匹配的区间。
pub struct BruteforceMatch {
i : Int
j : Int
token : String
mut guesses : Double
mut guesses_log10 : Double
} derive(Eq, Debug)
///|
pub extend BruteforceMatch with Eq::{equal, not_equal}
///|
pub extend BruteforceMatch with @debug.Debug::{to_repr}
///|
/// 词典匹配。`rank` 为 1-based 行序;`l33t`/`reversed` 标记变体;
/// `sub` 记录该匹配实际用到的 l33t 替换(subbed_char → 原字母)。
pub struct DictionaryMatch {
mut i : Int
mut j : Int
mut token : String
/// 查表用的(已转小写的)命中词
matched_word : String
rank : Int
dictionary_name : Dictionary
mut reversed : Bool
l33t : Bool
sub : Array[(Char, Char)]
mut guesses : Double
mut guesses_log10 : Double
} derive(Eq, Debug)
///|
pub extend DictionaryMatch with Eq::{equal, not_equal}
///|
pub extend DictionaryMatch with @debug.Debug::{to_repr}
///|
/// 键盘空间匹配。`turns` 为转向次数,`shifted_count` 为按了 Shift 的键数。
pub struct SpatialMatch {
i : Int
j : Int
token : String
/// 图名:`qwerty` / `dvorak` / `keypad` / `mac_keypad`
graph : String
turns : Int
shifted_count : Int
mut guesses : Double
mut guesses_log10 : Double
} derive(Eq, Debug)
///|
pub extend SpatialMatch with Eq::{equal, not_equal}
///|
pub extend SpatialMatch with @debug.Debug::{to_repr}
///|
/// 重复匹配。`base_matches` 是对 base_token 递归 omnimatch 的结果。
pub struct RepeatMatch {
i : Int
j : Int
token : String
base_token : String
base_guesses : Double
base_matches : Array[Match]
repeat_count : Int
mut guesses : Double
mut guesses_log10 : Double
} derive(Eq, Debug)
///|
pub extend RepeatMatch with Eq::{equal, not_equal}
///|
pub extend RepeatMatch with @debug.Debug::{to_repr}
///|
/// 序列匹配。`sequence_space` 为序列空间大小(lower/upper=26,digits=10)。
pub struct SequenceMatch {
i : Int
j : Int
token : String
/// `lower` / `upper` / `digits` / `unicode`
sequence_name : String
sequence_space : Int
ascending : Bool
mut guesses : Double
mut guesses_log10 : Double
} derive(Eq, Debug)
///|
pub extend SequenceMatch with Eq::{equal, not_equal}
///|
pub extend SequenceMatch with @debug.Debug::{to_repr}
///|
/// 正则匹配。目前只有 `recent_year`。
pub struct RegexMatch {
i : Int
j : Int
token : String
regex_name : String
mut guesses : Double
mut guesses_log10 : Double
} derive(Eq, Debug)
///|
pub extend RegexMatch with Eq::{equal, not_equal}
///|
pub extend RegexMatch with @debug.Debug::{to_repr}
///|
/// 日期匹配。上游不做真日期解析(`feb 31st` 合法、不查闰年)。
pub struct DateMatch {
i : Int
j : Int
token : String
/// 分隔符;无分隔符(如 `1191`)时为空串
separator : String
year : Int
month : Int
day : Int
mut guesses : Double
mut guesses_log10 : Double
} derive(Eq, Debug)
///|
pub extend DateMatch with Eq::{equal, not_equal}
///|
pub extend DateMatch with @debug.Debug::{to_repr}
///|
/// 匹配起点(闭区间,Unicode 标量值下标)。
pub fn Match::start_index(self : Match) -> Int {
match self {
Bruteforce(m) => m.i
Dictionary(m) => m.i
Spatial(m) => m.i
Repeat(m) => m.i
Sequence(m) => m.i
Regex(m) => m.i
Date(m) => m.i
}
}
///|
/// 匹配终点(闭区间,Unicode 标量值下标)。
pub fn Match::end_index(self : Match) -> Int {
match self {
Bruteforce(m) => m.j
Dictionary(m) => m.j
Spatial(m) => m.j
Repeat(m) => m.j
Sequence(m) => m.j
Regex(m) => m.j
Date(m) => m.j
}
}
///|
/// 命中的原文字符串。
pub fn Match::token(self : Match) -> String {
match self {
Bruteforce(m) => m.token
Dictionary(m) => m.token
Spatial(m) => m.token
Repeat(m) => m.token
Sequence(m) => m.token
Regex(m) => m.token
Date(m) => m.token
}
}
///|
/// 模式名(与上游 pattern 字符串一致)。
pub fn Match::pattern(self : Match) -> String {
match self {
Bruteforce(_) => "bruteforce"
Dictionary(_) => "dictionary"
Spatial(_) => "spatial"
Repeat(_) => "repeat"
Sequence(_) => "sequence"
Regex(_) => "regex"
Date(_) => "date"
}
}
///|
/// 匹配长度(标量值个数)。
pub fn Match::span_length(self : Match) -> Int {
self.end_index() - self.start_index() + 1
}
///|
/// 已缓存的猜测数(0 表示尚未计算)。
pub fn Match::guesses(self : Match) -> Double {
match self {
Bruteforce(m) => m.guesses
Dictionary(m) => m.guesses
Spatial(m) => m.guesses
Repeat(m) => m.guesses
Sequence(m) => m.guesses
Regex(m) => m.guesses
Date(m) => m.guesses
}
}
///|
/// `log10(guesses)`(上游用于 feedback 文案)。
pub fn Match::guesses_log10(self : Match) -> Double {
match self {
Bruteforce(m) => m.guesses_log10
Dictionary(m) => m.guesses_log10
Spatial(m) => m.guesses_log10
Repeat(m) => m.guesses_log10
Sequence(m) => m.guesses_log10
Regex(m) => m.guesses_log10
Date(m) => m.guesses_log10
}
}
///|
/// 写入猜测数缓存(DP 会多次调用 `estimate_guesses`,缓存避免重复计算)。
pub fn Match::set_guesses(self : Match, guesses : Double) -> Unit {
match self {
Bruteforce(m) => m.guesses = guesses
Dictionary(m) => m.guesses = guesses
Spatial(m) => m.guesses = guesses
Repeat(m) => m.guesses = guesses
Sequence(m) => m.guesses = guesses
Regex(m) => m.guesses = guesses
Date(m) => m.guesses = guesses
}
}
///|
/// 写入 `log10(guesses)`。
pub fn Match::set_guesses_log10(self : Match, log10 : Double) -> Unit {
match self {
Bruteforce(m) => m.guesses_log10 = log10
Dictionary(m) => m.guesses_log10 = log10
Spatial(m) => m.guesses_log10 = log10
Repeat(m) => m.guesses_log10 = log10
Sequence(m) => m.guesses_log10 = log10
Regex(m) => m.guesses_log10 = log10
Date(m) => m.guesses_log10 = log10
}
}
///|
/// 生成调试用的单行摘要(不含 guesses,避免噪声)。
pub fn Match::describe(self : Match) -> String {
"\{self.pattern()}(\{self.start_index()}, \{self.end_index()}) \{self.token()}"
}
// ---------------------------------------------------------------------------
// 工具函数(对应上游 `matching.empty/extend/translate/mod/sorted`)
// ---------------------------------------------------------------------------
///|
/// 上游 `matching.extend`:`lst.push.apply(lst, lst2)`。
fn[T] extend_matches(lst : Array[T], lst2 : Array[T]) -> Unit {
for item in lst2 {
lst.push(item)
}
}
///|
/// 上游 `matching.translate`:按 chr_map 替换字符。
fn translate(chars : Array[Char], chr_map : Map[Char, Char]) -> Array[Char] {
chars.map(c => {
match chr_map.get(c) {
Some(replacement) => replacement
None => c
}
})
}
///|
/// 上游 `matching.mod`:对负数也正确的取模。
pub fn mod_impl(n : Int, m : Int) -> Int {
(n % m + m) % m
}
///|
/// 上游 `matching.sorted`:按 `i` 主序、`j` 次序排序。
///
/// 注意:这里**不**按 guesses 排序(上游同样如此)。DP 内部会按 j 分区再按 i
/// 排序,所以全局顺序不影响结果,但保持确定性输出有助于测试与调试。
pub fn sort_matches(matches : Array[Match]) -> Array[Match] {
matches.sort_by((m1, m2) => {
if m1.start_index() != m2.start_index() {
m1.start_index() - m2.start_index()
} else {
m1.end_index() - m2.end_index()
}
})
matches
}
///|
/// 把 char 数组的闭区间 `[i, j]` 转成字符串。
fn slice_to_string(chars : Array[Char], i : Int, j : Int) -> String {
String::from_array(chars[i:j + 1])
}
///|
/// 是否某个字符是 ASCII 小写字母。
fn is_lower(c : Char) -> Bool {
c.is_ascii_lowercase()
}
///|
/// 是否某个字符是 ASCII 大写字母。
fn is_upper(c : Char) -> Bool {
c.is_ascii_uppercase()
}
///|
/// 是否某个字符是 ASCII 数字。
fn is_digit(c : Char) -> Bool {
c.is_ascii_digit()
}
///|
/// 上游 `scoring.START_UPPER` 等四个正则的字符级等价实现。
fn all_ascii_lower(chars : Array[Char]) -> Bool {
for c in chars {
if !is_lower(c) {
return false
}
}
true
}
///|
fn all_ascii_upper(chars : Array[Char]) -> Bool {
for c in chars {
if !is_upper(c) {
return false
}
}
true
}
///|
fn all_ascii_digit(chars : Array[Char]) -> Bool {
for c in chars {
if !is_digit(c) {
return false
}
}
true
}
// ---------------------------------------------------------------------------
// omnimatch:把 8 个匹配器跑一遍
// ---------------------------------------------------------------------------
///|
/// 一次 omnimatch 需要的上下文(上游用模块级全局变量传递用户输入词典)。
pub struct Omnimatcher {
dictionaries : Array[(Dictionary, Map[String, Int])]
max_word_length : Int
reference_year : Int
} derive(Eq, Debug)
///|
pub extend Omnimatcher with Eq::{equal, not_equal}
///|
pub extend Omnimatcher with @debug.Debug::{to_repr}
///|
/// 构造 omnimatch 上下文。`user_inputs` 已由调用方小写化并建表。
pub fn Omnimatcher::new(
user_inputs : Map[String, Int],
reference_year : Int,
) -> Omnimatcher {
// 用户词典:沿用上游 sanitize 后的语义(全部小写、按出现顺序 rank)。
// 用户词典不参与 max_word_length 的全局上限裁剪,单独按其最长词放宽。
let builtin = builtin_dictionaries
let dicts : Array[(Dictionary, Map[String, Int])] = []
for pair in builtin {
dicts.push((pair.0, pair.1.force()))
}
dicts.push((Dictionary::UserInputs, user_inputs))
let mut max_len = max_dictionary_word_length.force()
for word in user_inputs.keys() {
let len = word.to_array().length()
if len > max_len {
max_len = len
}
}
{ dictionaries: dicts, max_word_length: max_len, reference_year, }
}
///|
/// 上游 `omnimatch(password)`:跑完全部匹配器后按 `sorted` 排序。
pub fn Omnimatcher::run(self : Omnimatcher, password : String) -> Array[Match] {
full_match(
password,
self.dictionaries,
self.max_word_length,
l33t_table,
self.reference_year,
)
}
// ---------------------------------------------------------------------------
// 词典匹配 / 反向词典匹配
// ---------------------------------------------------------------------------
///|
/// 对一个密码跑完全部 8 个匹配器(`Omnimatcher::run` 与 `repeat_match` 的递归分析共用)。
fn full_match(
password : String,
dictionaries : Array[(Dictionary, Map[String, Int])],
max_word_length : Int,
table : Map[Char, Array[Char]],
reference_year : Int,
) -> Array[Match] {
let chars = password.to_array()
let matches : Array[Match] = []
extend_matches(
matches,
dictionary_match(chars, dictionaries, max_word_length),
)
extend_matches(
matches,
reverse_dictionary_match(chars, dictionaries, max_word_length),
)
extend_matches(
matches,
l33t_match(chars, dictionaries, table, max_word_length),
)
extend_matches(matches, spatial_match(chars, all_graphs()))
extend_matches(
matches,
repeat_match(chars, dictionaries, max_word_length, table, reference_year),
)
extend_matches(matches, sequence_match(chars))
extend_matches(matches, regex_match(chars))
extend_matches(matches, date_match(chars, reference_year))
sort_matches(matches)
}
///|
/// 上游 `dictionary_match`。
pub fn dictionary_match(
password : Array[Char],
dictionaries : Array[(Dictionary, Map[String, Int])],
max_word_length : Int,
) -> Array[Match] {
let password_lower = password.map(c => c.to_ascii_lowercase())
dictionary_match_inner(
password, password_lower, dictionaries, max_word_length,
)
}
///|
/// 词典匹配主体。`password_lower` 由调用方传入(l33t 匹配要传"替换后"的小写形式)。
pub fn dictionary_match_inner(
password : Array[Char],
password_lower : Array[Char],
dictionaries : Array[(Dictionary, Map[String, Int])],
max_word_length : Int,
) -> Array[Match] {
let len = password.length()
let matches : Array[Match] = []
// 长度上限裁剪(见文件头说明):不可能命中的长度不枚举。
let limit = if max_word_length < len { max_word_length } else { len }
for entry in dictionaries {
let (dict_name, ranked) = entry
let mut i = 0
while i < len {
let j_max = if i + limit < len { i + limit } else { len - 1 }
let mut j = i
while j <= j_max {
let word = slice_to_string(password_lower, i, j)
// get_from_string 接受 StringView,避免把 word 移进 Map 查询
match ranked.get_from_string(word) {
Some(rank) =>
matches.push(
Match::Dictionary({
i,
j,
token: slice_to_string(password, i, j),
matched_word: word,
rank,
dictionary_name: dict_name,
reversed: false,
l33t: false,
sub: [],
guesses: 0.0,
guesses_log10: 0.0,
}),
)
None => ()
}
j += 1
}
i += 1
}
}
sort_matches(matches)
}
///|
/// 上游 `reverse_dictionary_match`。
pub fn reverse_dictionary_match(
password : Array[Char],
dictionaries : Array[(Dictionary, Map[String, Int])],
max_word_length : Int,
) -> Array[Match] {
let len = password.length()
let reversed = password.rev()
let reversed_lower = reversed.map(c => c.to_ascii_lowercase())
let matches = dictionary_match_inner(
reversed, reversed_lower, dictionaries, max_word_length,
)
for m in matches {
match m {
Dictionary(d) => {
// 坐标映射回原串:i' = len-1-j, j' = len-1-i
let (orig_i, orig_j) = (d.i, d.j)
d.i = len - 1 - orig_j
d.j = len - 1 - orig_i
d.token = slice_to_string(password, d.i, d.j)
d.reversed = true
}
_ => ()
}
}
sort_matches(matches)
}
// ---------------------------------------------------------------------------
// l33t 替换
// ---------------------------------------------------------------------------
///|
/// 整数绝对值(避免为此单独依赖 `moonbitlang/core/int`)。
fn abs_int(n : Int) -> Int {
if n < 0 {
-n
} else {
n
}
}
///|
/// MoonBit 的数组切片(`arr[i:j]`)得到的是 `ArrayView[Char]`,
/// 而各分析帮例程序统一按 `Array[Char]` 接受。
fn view_to_array(view : ArrayView[Char]) -> Array[Char] {
Array::from_iter(view.iter())
}
///|
/// 上游 `L33T_TABLE`:字母 → 常见 l33t 替换字符。
let l33t_table : Map[Char, Array[Char]] = Map([
('a', ['4', '@']),
('b', ['8']),
('c', ['(', '{', '[', '<']),
('e', ['3']),
('g', ['6', '9']),
('i', ['1', '!', '|']),
('l', ['1', '|', '7']),
('o', ['0']),
('s', ['$', '5']),
('t', ['+', '7']),
('x', ['%']),
('z', ['2']),
])
///|
/// 上游 `relevant_l33t_subtable`:只保留密码里真的出现了的替换支。
pub fn relevant_l33t_subtable(
password : Array[Char],
table : Map[Char, Array[Char]],
) -> Map[Char, Array[Char]] {
let subtable : Map[Char, Array[Char]] = Map([])
for pair in table.iter() {
let (letter, subs) = pair
let relevant = subs.filter(sub => password.contains(sub))
if relevant.length() > 0 {
subtable[letter] = relevant
}
}
subtable
}
///|
/// 上游 `enumerate_l33t_subs`:枚举所有可能的 l33t 替换字典(带去重)。
fn enumerate_l33t_subs(
table : Map[Char, Array[Char]],
) -> Array[Map[Char, Char]] {
// Map 的迭代顺序未定义;为了让输出可复现,按键排序后枚举。
let keys = table.keys().to_array()
keys.sort()
// subs 以关联数组(Array[(Char, Char)])形式承载,最后转成 Map
let subs : Array[Array[(Char, Char)]] = [[]]
let mut key_index = 0
while key_index < keys.length() {
let first_key = keys[key_index]
let l33t_chrs = match table.get(first_key) {
Some(v) => v
None => []
}
let next_subs : Array[Array[(Char, Char)]] = []
for l33t_chr in l33t_chrs {
for sub in subs {
let mut dup_index = -1
let mut i = 0
while i < sub.length() {
if sub[i].0 == l33t_chr {
dup_index = i
i = sub.length()
break
}
i += 1
}
if dup_index == -1 {
// 该替换字符尚未占用,直接扩展
let sub_extension = sub.copy()
sub_extension.push((l33t_chr, first_key))
next_subs.push(sub_extension)
} else {
// 同一替换字符已被别的字母占用:生成"改投该字母"的备选
let sub_alternative = sub.copy()
ignore(sub_alternative.remove(dup_index))
sub_alternative.push((l33t_chr, first_key))
next_subs.push(sub)
next_subs.push(sub_alternative)
}
}
}
// subs = dedup(next_subs):原地替换,保持同一数组引用
subs.clear()
dedup_l33t_subs(next_subs).each(dup => subs.push(dup))
key_index += 1
}
subs.map(assoc => Map(assoc))
}
///|
/// 上游 `dedup`:按排序后的关联串去重,保留首个。
fn dedup_l33t_subs(
subs : Array[Array[(Char, Char)]],
) -> Array[Array[(Char, Char)]] {
let seen : Map[String, Unit] = Map([])
let out : Array[Array[(Char, Char)]] = []
for sub in subs {
let labels = sub.map(pair => "\{pair.0},\{pair.1}")
labels.sort()
let label = labels.join("-")
match seen.get(label) {
Some(_) => ()
None => {
seen[label] = ()
out.push(sub)
}
}
}
out
}
///|
/// 上游 `l33t_match`。
pub fn l33t_match(
password : Array[Char],
dictionaries : Array[(Dictionary, Map[String, Int])],
table : Map[Char, Array[Char]],
max_word_length : Int,
) -> Array[Match] {
let matches : Array[Match] = []
for sub in enumerate_l33t_subs(relevant_l33t_subtable(password, table)) {
// 角落情况:密码没有任何可用替换
if sub.length() == 0 {
continue
}
let subbed = translate(password, sub)
let subbed_lower = subbed.map(c => c.to_ascii_lowercase())
for
m in dictionary_match_inner(
subbed, subbed_lower, dictionaries, max_word_length,
) {
match m {
Dictionary(d) => {
let token = slice_to_string(password, d.i, d.j)
if token.to_lower() == d.matched_word {
// 只保留真正发生了替换的匹配
continue
}
// match_sub:该匹配实际用到的替换子集
let match_sub : Array[(Char, Char)] = []
for pair in sub.iter() {
let (subbed_chr, chr) = pair
if token.contains_char(subbed_chr) {
match_sub.push((subbed_chr, chr))
}
}
match_sub.sort_by((a, b) => a.0.to_int() - b.0.to_int())
matches.push(
Match::Dictionary({
i: d.i,
j: d.j,
token,
matched_word: d.matched_word,
rank: d.rank,
dictionary_name: d.dictionary_name,
reversed: false,
l33t: true,
sub: match_sub,
guesses: 0.0,
guesses_log10: 0.0,
}),
)
}
_ => ()
}
}
}
// 过滤单字符 l33t 匹配以降噪('1' 命中 'i'、'4' 命中 'a' 这类)
let filtered = matches.filter(m => m.span_length() > 1)
sort_matches(filtered)
}
// ---------------------------------------------------------------------------
// 键盘空间匹配
// ---------------------------------------------------------------------------
///|
/// 上游 `SHIFTED_RX` 的字符集合。
fn is_shifted_char(c : Char) -> Bool {
let shifted : String = "~!@#$%^&*()_+QWERTYUIOP{}|ASDFGHJKL:\"ZXCVBNM<>?"
shifted.contains_char(c)
}
///|
/// 上游 `spatial_match`。
pub fn spatial_match(
password : Array[Char],
graphs : Array[(String, Map[Char, Array[(Char, Char)?]])],
) -> Array[Match] {
let matches : Array[Match] = []
for entry in graphs {
let (name, graph) = entry
extend_matches(matches, spatial_match_helper(password, graph, name))
}
sort_matches(matches)
}
///|
/// 全部四个键盘图(顺序与上游 GRAPHS 一致)。
pub fn all_graphs() -> Array[(String, Map[Char, Array[(Char, Char)?]])] {
graph_names.map(name => (name, get_graph(name)))
}
///|
/// 上游 `spatial_match_helper`。
fn spatial_match_helper(
password : Array[Char],
graph : Map[Char, Array[(Char, Char)?]],
graph_name : String,
) -> Array[Match] {
let matches : Array[Match] = []
let len = password.length()
let mut i = 0
while i < len - 1 {
let mut j = i + 1
let mut last_direction : Int? = None
let mut turns = 0
let mut shifted_count = 0
if (graph_name == "qwerty" || graph_name == "dvorak") &&
is_shifted_char(password[i]) {
// 起始字符本身是 Shift 形态
shifted_count = 1
}
let mut stop = false
while !stop {
let prev_char = password[j - 1]
let mut found = false
let mut found_direction = -1
let mut cur_direction = -1
let adjacents : Array[(Char, Char)?] = match graph.get(prev_char) {
Some(arcs) => arcs
None => []
}
if j < len {
let cur_char = password[j]
for adj in adjacents {
cur_direction += 1
match adj {
Some((unshifted, shifted)) => {
// 等价于上游 adj.indexOf(cur_char):先看未移位位,再看移位位
let index : Int? = if cur_char == unshifted {
Some(0)
} else if cur_char == shifted {
Some(1)
} else {
None
}
match index {
Some(idx) => {
found = true
found_direction = cur_direction
if idx == 1 {
// 下标 1 表示该键是按 Shift 输入的
shifted_count += 1
}
if last_direction != Some(found_direction) {
// 起始时 last_direction 为 None,任何方向都算一次转向:
// 每个键盘模式都以一次转向开始。
turns += 1
last_direction = Some(found_direction)
}
stop = true
break
}
None => ()
}
}
None => ()
}
}
}
if found && !stop {
j += 1
} else if found {
// 内部循环因找到相邻键而退出:继续扩展 j
stop = false
j += 1
} else {
// 模式到此为止,记录(长度 1、2 的链不考虑)
if j - i > 2 {
matches.push(
Match::Spatial({
i,
j: j - 1,
token: slice_to_string(password, i, j - 1),
graph: graph_name,
turns,
shifted_count,
guesses: 0.0,
guesses_log10: 0.0,
}),
)
}
i = j
stop = true
}
}
}
matches
}
// ---------------------------------------------------------------------------
// 重复匹配
// ---------------------------------------------------------------------------
///|
/// 在 `p` 处是否存在"某个长度 L 的单元紧邻重复至少两次"。
/// `greedy` 为 true 时取最大可行 L,否则取最小。
/// 返回 `(块终止下标 j, 单元长度 L)`。
fn repeat_at(password : Array[Char], p : Int, greedy : Bool) -> (Int, Int)? {
let len = password.length()
let max_l = (len - p) / 2
if max_l < 1 {
return None
}
if greedy {
let mut l = max_l
while l >= 1 {
let count = count_repeats(password, p, l)
if count >= 2 {
return Some((p + count * l - 1, l))
}
l -= 1
}
None
} else {
let mut l = 1
while l <= max_l {
let count = count_repeats(password, p, l)
if count >= 2 {
return Some((p + count * l - 1, l))
}
l += 1
}
None
}
}
///|
/// 从 `p` 起,长度 L 的单元能连续重复多少次(至少 1)。
fn count_repeats(password : Array[Char], p : Int, l : Int) -> Int {
let len = password.length()
let mut count = 1
let mut start = p + l
while start + l <= len {
let mut equal = true
let mut k = 0
while k < l {
if password[start + k] != password[p + k] {
equal = false
k = l
break
}
k += 1
}
if !equal {
break
}
count += 1
start += l
}
count
}
///|
/// 从 `start` 起找第一个可重复的位置(对应正则 `lastIndex` 语义)。
fn repeat_from(
password : Array[Char],
start : Int,
greedy : Bool,
) -> (Int, Int, Int)? {
let len = password.length()
let mut p = start
while p < len {
match repeat_at(password, p, greedy) {
Some((j_end, l)) => return Some((p, j_end, l))
None => p += 1
}
}
None
}
///|
/// 上游 `lazy_anchored = /^(.+?)\1+$/`:整串都能被某个最短单元整除重复时,
/// 返回该最短单元长度。替代正则反向引用的手写实现(MoonBit core 的正则不支持 `\1`)。
fn shortest_period(password : Array[Char], i : Int, j : Int) -> Int {
let total = j - i + 1
let mut l = 1
while l <= total {
if total % l == 0 && count_repeats(password, i, l) == total / l {
return l
}
l += 1
}
total
}
///|
/// 上游 `repeat_match`。
pub fn repeat_match(
password : Array[Char],
dictionaries : Array[(Dictionary, Map[String, Int])],
max_word_length : Int,
table : Map[Char, Array[Char]],
reference_year : Int,
) -> Array[Match] {
let matches : Array[Match] = []
let len = password.length()
let mut last_index = 0
let mut stop = false
while last_index < len && !stop {
// 上游分别用 greedy / lazy 两个正则找重复块,再比较谁覆盖更长
let greedy = repeat_from(password, last_index, true)
let lazy_match = repeat_from(password, last_index, false)
match (greedy, lazy_match) {
// 上游 `break unless greedy_match?`:没有可重复块就结束
(None, _) | (_, None) => stop = true
(Some(g), Some(l)) => {
let use_greedy = g.1 - g.0 > l.1 - l.0
let (i, j, base_token_len) = if use_greedy {
// greedy 的重复串自身可能还是重复的(如 aabaab 里的 aab),
// 用 anchored lazy 的思想取最短单元
let period = shortest_period(password, g.0, g.1)
(g.0, g.1, period)
} else {
(l.0, l.1, l.2)
}
let token = slice_to_string(password, i, j)
let base_token = slice_to_string(password, i, i + base_token_len - 1)
// 递归分析并打分 base_token
let base_analysis = most_guessable_match_sequence(
base_token,
full_match(
base_token, dictionaries, max_word_length, table, reference_year,
),
reference_year,
)
matches.push(
Match::Repeat({
i,
j,
token,
base_token,
base_guesses: base_analysis.guesses,
base_matches: base_analysis.sequence,
repeat_count: token.to_array().length() /
base_token.to_array().length(),
guesses: 0.0,
guesses_log10: 0.0,
}),
)
last_index = j + 1
}
}
}
matches
}
// ---------------------------------------------------------------------------
// 序列匹配
// ---------------------------------------------------------------------------
///|
/// 上游 `max_delta`:允许的相邻字符码点差上限(支持 `9753` 这种跳跃)。
let max_delta = 5
///|
/// 上游 `sequence_match` 里的 `update`。
fn push_sequence_if_valid(
result : Array[Match],
password : Array[Char],
i : Int,
j : Int,
delta : Int,
) -> Unit {
if j - i > 1 || abs_int(delta) == 1 {
if 0 < abs_int(delta) && abs_int(delta) <= max_delta {
let token = slice_to_string(password, i, j)
let token_chars = token.to_array()
let (sequence_name, sequence_space) = if all_ascii_lower(token_chars) {
("lower", 26)
} else if all_ascii_upper(token_chars) {
("upper", 26)
} else if all_ascii_digit(token_chars) {
("digits", 10)
} else {
// 保守地仍按罗马字母表大小估计(可改进)
("unicode", 26)
}
result.push(
Match::Sequence({
i,
j,
token,
sequence_name,
sequence_space,
ascending: delta > 0,
guesses: 0.0,
guesses_log10: 0.0,
}),
)
}
}
}
///|
/// 上游 `sequence_match`:靠相邻字符码点差不变来识别序列(支持跳跃与非拉丁字母)。
pub fn sequence_match(password : Array[Char]) -> Array[Match] {
let result : Array[Match] = []
if password.length() == 1 {
return result
}
let mut i = 0
let mut last_delta : Int? = None
let mut k = 1
while k < password.length() {
let delta = password[k].to_int() - password[k - 1].to_int()
match last_delta {
None => last_delta = Some(delta)
Some(ld) =>
if delta != ld {
let j = k - 1
push_sequence_if_valid(result, password, i, j, ld)
i = j
last_delta = Some(delta)
}
}
k += 1
}
match last_delta {
Some(ld) =>
push_sequence_if_valid(result, password, i, password.length() - 1, ld)
None => ()
}
result
}
// ---------------------------------------------------------------------------
// 正则匹配
// ---------------------------------------------------------------------------
///|
/// 上游 `REGEXEN.recent_year` 用的是 `/19\d\d|200\d|201\d/g`(2017 年的数据)。
/// zxcvbn-rs 已把它更新为 `/19\d\d|20\d\d/`,本项目沿用更新后的范围,
/// 否则 2020 年之后的年份识别不出来。
fn is_recent_year_at(password : Array[Char], i : Int) -> Bool {
if i + 3 >= password.length() {
return false
}
let c0 = password[i]
let c1 = password[i + 1]
let ok_prefix = (c0 == '1' && c1 == '9') || (c0 == '2' && c1 == '0')
if !ok_prefix {
return false
}
is_digit(password[i + 2]) && is_digit(password[i + 3])
}
///|
/// 上游 `regex_match`。这里不用 Regex,直接手写等价扫描——
/// 一是 core 的正则不支持 `\d`,二是需要在 char 数组(标量值)下标上定位。
pub fn regex_match(password : Array[Char]) -> Array[Match] {
let matches : Array[Match] = []
let len = password.length()
let mut i = 0
// 与上游 g 标志语义一致:找到后跳到匹配末尾,非重叠
while i + 4 <= len {
if is_recent_year_at(password, i) {
matches.push(
Match::Regex({
i,
j: i + 3,
token: slice_to_string(password, i, i + 3),
regex_name: "recent_year",
guesses: 0.0,
guesses_log10: 0.0,
}),
)
i += 4
} else {
i += 1
}
}
sort_matches(matches)
}
// ---------------------------------------------------------------------------
// 日期匹配
// ---------------------------------------------------------------------------
///|
/// 上游 `date_max_year`。
let date_max_year = 2050
///|
/// 上游 `date_min_year`。
let date_min_year = 1000
///|
/// 上游 `date_splits`:无分隔符日期按长度切分的 (k, l) 组合。
let date_splits : Map[Int, Array[(Int, Int)]] = Map([
(4, [(1, 2), (2, 3)]),
(5, [(1, 3), (2, 3)]),
(6, [(1, 2), (2, 4), (4, 5)]),
(7, [(1, 3), (2, 3), (4, 5), (4, 6)]),
(8, [(2, 4), (4, 6)]),
])
///|
/// 上游 `date_match` 的无分隔符分支用的 `maybe_date_no_separator`。
fn is_maybe_date_no_separator(token : Array[Char]) -> Bool {
let len = token.length()
if len < 4 || len > 8 {
return false
}
all_ascii_digit(token)
}
///|
/// 上游 `maybe_date_with_separator` 的手写等价实现。
/// 返回 `(第一段, 分隔符, 第二段, 第三段)`。
///
/// 上游正则 `^(\d{1,4})([\s/\\_.-])(\d{1,2})\2(\d{1,4})$` 是贪婪匹配,
/// 因此按"段长从大到小"试探,首个成功者即正则语义下的选择。
fn parse_maybe_date_with_separator(
token : Array[Char],
) -> (Int, Char, Int, Int)? {
let len = token.length()
let mut l1 = if len < 4 { len } else { 4 }
while l1 >= 1 {
if !all_ascii_digit(view_to_array(token[0:l1])) {
l1 -= 1
continue
}
let sep_index = l1
if sep_index >= len {
l1 -= 1
continue
}
let sep = token[sep_index]
if !is_date_separator(sep) {
l1 -= 1
continue
}
let mut l2 = if len - sep_index - 1 < 2 { len - sep_index - 1 } else { 2 }
while l2 >= 1 {
let d2_start = sep_index + 1
let d2_end = d2_start + l2
if d2_end > len {
l2 -= 1
continue
}
if !all_ascii_digit(view_to_array(token[d2_start:d2_end])) {
l2 -= 1
continue
}
let second_sep = d2_end
if second_sep >= len || token[second_sep] != sep {
l2 -= 1
continue
}
let d3_start = second_sep + 1
if d3_start >= len {
l2 -= 1
continue
}
let d3_len = len - d3_start
if d3_len < 1 ||
d3_len > 4 ||
!all_ascii_digit(view_to_array(token[d3_start:len])) {
l2 -= 1
continue
}
let d1 = digits_to_int(view_to_array(token[0:l1]))
let d2 = digits_to_int(view_to_array(token[d2_start:d2_end]))
let d3 = digits_to_int(view_to_array(token[d3_start:len]))
return Some((d1, sep, d2, d3))
}
l1 -= 1
}
None
}
///|
/// 上游 `[\s/\\_.-]` 的字符集。JS 的 `\s` 覆盖多种 Unicode 空白,这里用
/// MoonBit 的 `Char::is_whitespace` 对齐。
fn is_date_separator(c : Char) -> Bool {
c == '/' || c == '\\' || c == '_' || c == '.' || c == '-' || c.is_whitespace()
}
///|
/// 把纯数字字符数组转成整数(对应上游 `parseInt`)。
fn digits_to_int(chars : Array[Char]) -> Int {
let mut value = 0
for c in chars {
if !is_digit(c) {
// 上游 parseInt 会解析到第一个非数字字符为止;调用点保证全是数字
break
}
value = value * 10 + (c.to_int() - '0'.to_int())
}
value
}
///|
/// 上游 `map_ints_to_dm`:尝试把两个整数映射为 (day, month),两种顺序都试。
fn map_ints_to_dm(ints : (Int, Int)) -> (Int, Int)? {
let (a, b) = ints
// [ints, reversed(ints)]
if a >= 1 && a <= 31 && b >= 1 && b <= 12 {
return Some((a, b))
}
if b >= 1 && b <= 31 && a >= 1 && a <= 12 {
return Some((b, a))
}
None
}
///|
/// 上游 `map_ints_to_dmy`:返回 `(year, month, day)`。
fn map_ints_to_dmy(ints : (Int, Int, Int)) -> (Int, Int, Int)? {
let (n0, n1, n2) = ints
// 中间整数是月的候选:绝不能超过 31,也不能为 0
if n1 > 31 || n1 <= 0 {
return None
}
let mut over_12 = 0
let mut over_31 = 0
let mut under_1 = 0
for n in [n0, n1, n2] {
if (n > 99 && n < date_min_year) || n > date_max_year {
return None
}
if n > 31 {
over_31 += 1
}
if n > 12 {
over_12 += 1
}
if n <= 0 {
under_1 += 1
}
}
if over_31 >= 2 || over_12 == 3 || under_1 >= 2 {
return None
}
// 先找四位年:yyyy + daymonth,或 daymonth + yyyy
let year_splits : Array[(Int, (Int, Int))] = [(n2, (n0, n1)), (n0, (n1, n2))]
for split in year_splits {
let (y, rest) = split
if date_min_year <= y && y <= date_max_year {
// 候选里含四位年但剩余部分凑不出日/月 => 不是日期,直接否定
match map_ints_to_dm(rest) {
Some(dm) => return Some((y, dm.1, dm.0))
None => return None
}
}
}
// 没有四位年时,两位年最灵活
for split in year_splits {
let (y, rest) = split
match map_ints_to_dm(rest) {
Some(dm) => return Some((two_to_four_digit_year(y), dm.1, dm.0))
None => ()
}
}
None
}
///|
/// 上游 `two_to_four_digit_year`。
fn two_to_four_digit_year(year : Int) -> Int {
if year > 99 {
year
} else if year > 50 {
// 87 -> 1987
year + 1900
} else {
// 15 -> 2015
year + 2000
}
}
///|
/// 上游 `date_match`。
pub fn date_match(password : Array[Char], reference_year : Int) -> Array[Match] {
let matches : Array[Match] = []
let len = password.length()
// --- 无分隔符:长度 4..8 ---
if len >= 4 {
let mut i = 0
while i <= len - 4 {
let mut j = i + 3
while j <= i + 7 {
if j >= len {
j += 1
continue
}
let token = view_to_array(password[i:j + 1])
if is_maybe_date_no_separator(token) {
let candidates : Array[(Int, Int, Int)] = []
let splits = match date_splits.get(token.length()) {
Some(v) => v
None => []
}
for split in splits {
let (k, l) = split
let d1 = digits_to_int(view_to_array(token[0:k]))
let d2 = digits_to_int(view_to_array(token[k:l]))
let d3 = digits_to_int(view_to_array(token[l:token.length()]))
match map_ints_to_dmy((d1, d2, d3)) {
Some(dmy) => candidates.push(dmy)
None => ()
}
}
if candidates.length() > 0 {
// 同一子串可能有多种 dmy 解释,取"离参考年最近"的那个
let mut best = candidates[0]
let mut best_distance = abs_int(best.0 - reference_year)
for candidate in candidates[1:] {
let distance = abs_int(candidate.0 - reference_year)
if distance < best_distance {
best = candidate
best_distance = distance
}
}
matches.push(
Match::Date({
i,
j,
token: slice_to_string(password, i, j),
separator: "",
year: best.0,
month: best.1,
day: best.2,
guesses: 0.0,
guesses_log10: 0.0,
}),
)
}
}
j += 1
}
i += 1
}
}
// --- 带分隔符:长度 6..10 ---
if len >= 6 {
let mut i = 0
while i <= len - 6 {
let mut j = i + 5
while j <= i + 9 {
if j >= len {
j += 1
continue
}
let token = view_to_array(password[i:j + 1])
match parse_maybe_date_with_separator(token) {
Some((d1, sep, d2, d3)) =>
match map_ints_to_dmy((d1, d2, d3)) {
Some(dmy) =>
matches.push(
Match::Date({
i,
j,
token: slice_to_string(password, i, j),
separator: sep.to_string(),
year: dmy.0,
month: dmy.1,
day: dmy.2,
guesses: 0.0,
guesses_log10: 0.0,
}),
)
None => ()
}
None => ()
}
j += 1
}
i += 1
}
}
// 去掉"是别的日期匹配子串"的噪声项
let filtered : Array[Match] = []
let mut idx = 0
while idx < matches.length() {
let m = matches[idx]
let mut is_submatch = false
let mut other_idx = 0
while other_idx < matches.length() {
if other_idx != idx {
let other = matches[other_idx]
if other.start_index() <= m.start_index() &&
other.end_index() >= m.end_index() {
is_submatch = true
other_idx = matches.length()
break
}
}
other_idx += 1
}
if !is_submatch {
filtered.push(m)
}
idx += 1
}
sort_matches(filtered)
}