use winreg_core::key::Key;
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
pub enum Wildcard {
Subkey,
ControlSet,
User,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
pub struct Binding {
pub kind: Wildcard,
pub value: String,
}
impl Binding {
pub fn new(kind: Wildcard, value: impl Into<String>) -> Self {
Self {
kind,
value: value.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Segment {
Literal(String),
Variable(Wildcard, String),
}
pub(crate) const MAX_GLOB_DEPTH: usize = 64;
pub(crate) const MAX_GLOB_MATCHES: usize = 4096;
#[derive(Debug, Clone)]
pub struct ControlSetResolver {
pub sets: Vec<String>,
}
#[must_use]
pub fn resolve_control_sets(root: &Key<'_>) -> ControlSetResolver {
let n = current_control_set_number(root).unwrap_or(1);
ControlSetResolver {
sets: vec![format!("ControlSet{n:03}")],
}
}
fn current_control_set_number(root: &Key<'_>) -> Option<u32> {
let select = root.subkey("Select").ok()??;
let current = select.value("Current").ok()??;
let n = current.as_u32().ok()?;
if n == 0 {
None
} else {
Some(n)
}
}
pub fn expand(
root: &Key<'_>,
segments: &[Segment],
controlset: Option<&ControlSetResolver>,
emit: &mut dyn FnMut(&[Binding], &str, &Key<'_>),
) {
let mut bindings: Vec<Binding> = Vec::new();
let mut matched = 0usize;
walk(
root,
segments,
controlset,
"",
0,
&mut matched,
&mut bindings,
emit,
);
}
#[allow(clippy::too_many_arguments)]
fn walk(
key: &Key<'_>,
segments: &[Segment],
controlset: Option<&ControlSetResolver>,
prefix: &str,
depth: usize,
matched: &mut usize,
bindings: &mut Vec<Binding>,
emit: &mut dyn FnMut(&[Binding], &str, &Key<'_>),
) {
if *matched >= MAX_GLOB_MATCHES || depth > MAX_GLOB_DEPTH {
return;
}
let Some((head, rest)) = segments.split_first() else {
*matched += 1;
emit(bindings, prefix, key);
return;
};
match head {
Segment::Literal(name) => {
let Ok(children) = key.subkeys() else { return };
for child in children {
if child.name().eq_ignore_ascii_case(name) {
let child_prefix = join_path(prefix, &child.name());
walk(
&child,
rest,
controlset,
&child_prefix,
depth + 1,
matched,
bindings,
emit,
);
break;
}
}
}
Segment::Variable(Wildcard::ControlSet, _) => {
let fallback = ControlSetResolver {
sets: vec!["ControlSet001".to_string()],
};
let resolver = controlset.unwrap_or(&fallback);
let Ok(children) = key.subkeys() else { return };
for set_name in &resolver.sets {
if *matched >= MAX_GLOB_MATCHES {
return;
}
for child in &children {
if child.name().eq_ignore_ascii_case(set_name) {
let child_prefix = join_path(prefix, &child.name());
bindings.push(Binding::new(Wildcard::ControlSet, child.name()));
walk(
child,
rest,
controlset,
&child_prefix,
depth + 1,
matched,
bindings,
emit,
);
bindings.pop();
break;
}
}
}
}
Segment::Variable(Wildcard::Subkey, pattern) => {
if pattern.contains("**") {
walk(
key, rest, controlset, prefix, depth, matched, bindings, emit,
);
let Ok(children) = key.subkeys() else { return };
for child in children {
if *matched >= MAX_GLOB_MATCHES {
return;
}
let child_prefix = join_path(prefix, &child.name());
bindings.push(Binding::new(Wildcard::Subkey, child.name()));
walk(
&child,
segments,
controlset,
&child_prefix,
depth + 1,
matched,
bindings,
emit,
);
bindings.pop();
}
} else {
let Ok(children) = key.subkeys() else { return };
for child in children {
if *matched >= MAX_GLOB_MATCHES {
return;
}
if segment_matches(pattern, &child.name()) {
let child_prefix = join_path(prefix, &child.name());
bindings.push(Binding::new(Wildcard::Subkey, child.name()));
walk(
&child,
rest,
controlset,
&child_prefix,
depth + 1,
matched,
bindings,
emit,
);
bindings.pop();
}
}
}
}
Segment::Variable(Wildcard::User, _) => {} }
}
fn join_path(prefix: &str, name: &str) -> String {
if prefix.is_empty() {
name.to_string()
} else {
format!("{prefix}\\{name}")
}
}
fn segment_matches(pattern: &str, name: &str) -> bool {
let pat: Vec<char> = pattern.to_ascii_lowercase().chars().collect();
let txt: Vec<char> = name.to_ascii_lowercase().chars().collect();
glob_match(&pat, &txt)
}
fn glob_match(pat: &[char], txt: &[char]) -> bool {
let (mut p, mut t) = (0usize, 0usize);
let (mut star, mut mark) = (None, 0usize);
while t < txt.len() {
if p < pat.len() && pat[p] == '*' {
star = Some(p);
mark = t;
p += 1;
} else if p < pat.len() && pat[p] == txt[t] {
p += 1;
t += 1;
} else if let Some(sp) = star {
p = sp + 1;
mark += 1;
t = mark;
} else {
return false;
}
}
while p < pat.len() && pat[p] == '*' {
p += 1;
}
p == pat.len()
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn segment_match_handles_midsegment_wildcard() {
assert!(segment_matches("*ControlSet*", "ControlSet001"));
assert!(segment_matches("*", "anything"));
assert!(segment_matches("ABC*", "abcdef"));
assert!(!segment_matches("ABC*", "xyz"));
assert!(!segment_matches("Foo", "Bar"));
}
#[test]
fn binding_new_constructs() {
let b = Binding::new(Wildcard::ControlSet, "ControlSet002");
assert_eq!(b.kind, Wildcard::ControlSet);
assert_eq!(b.value, "ControlSet002");
}
}