use std::collections::BTreeMap;
use std::path::PathBuf;
use super::{Error, Layer, Mode, Notify, Origin, Read, Source};
const POLICY_KEY: &str = r"SOFTWARE\Policies\Excelano\Slipcase\Open";
const APPLICATION_KEY: &str = r"SOFTWARE\Excelano\Slipcase\Open";
const KEY_NOT_THERE: i32 = 0x8007_0002_u32.cast_signed();
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Hive {
Machine,
User,
}
impl Hive {
fn short(self) -> &'static str {
match self {
Self::Machine => "HKLM",
Self::User => "HKCU",
}
}
}
#[derive(Debug, Default, Clone)]
pub struct Registry {
keys: BTreeMap<Origin, (Hive, &'static str)>,
}
impl Registry {
#[must_use]
pub fn none() -> Self {
Self::default()
}
#[must_use]
pub fn for_this_platform() -> Self {
let mut keys = BTreeMap::new();
keys.insert(Origin::MachinePolicy, (Hive::Machine, POLICY_KEY));
keys.insert(Origin::UserPolicy, (Hive::User, POLICY_KEY));
keys.insert(Origin::Configuration, (Hive::User, APPLICATION_KEY));
Self { keys }
}
#[must_use]
pub fn locations(&self) -> Vec<(Origin, String)> {
self.keys
.iter()
.rev()
.map(|(origin, (hive, key))| (*origin, format!(r"{}\{}", hive.short(), key)))
.collect()
}
}
impl Source for Registry {
fn layer(&self, origin: Origin) -> Read {
let Some((hive, subkey)) = self.keys.get(&origin) else {
return Ok(None);
};
read(*hive, subkey)
}
}
fn without_the_terminator(mut values: Vec<String>) -> Vec<String> {
while values.last().is_some_and(String::is_empty) {
values.pop();
}
values
}
fn named(hive: Hive, subkey: &str, value: &str) -> PathBuf {
PathBuf::from(format!(r"{}\{}\{}", hive.short(), subkey, value))
}
fn read(hive: Hive, subkey: &str) -> Read {
use windows_registry::{CURRENT_USER, LOCAL_MACHINE};
let root = match hive {
Hive::Machine => LOCAL_MACHINE,
Hive::User => CURRENT_USER,
};
let key = match root.open(subkey) {
Ok(key) => key,
Err(e) if e.code().0 == KEY_NOT_THERE => return Ok(None),
Err(e) => {
return Err(Error::Unreadable {
path: PathBuf::from(format!(r"{}\{}", hive.short(), subkey)),
cause: std::io::Error::other(e.message()),
})
}
};
let bad = |value: &str, cause: String| Error::Malformed {
path: named(hive, subkey, value),
cause,
};
let has = |name: &str| key.get_type(name).is_ok();
let list = |name: &str| -> std::result::Result<Option<Vec<String>>, Error> {
if !has(name) {
return Ok(None);
}
key.get_multi_string(name)
.map(|v| Some(without_the_terminator(v)))
.map_err(|_| {
bad(
name,
"must be a REG_MULTI_SZ of extensions, one to a line".to_string(),
)
})
};
let flag = |name: &str| -> std::result::Result<Option<bool>, Error> {
if !has(name) {
return Ok(None);
}
key.get_u32(name)
.map(|v| Some(v != 0))
.map_err(|_| bad(name, "must be a REG_DWORD of 0 or 1".to_string()))
};
let word = |name: &str| -> std::result::Result<Option<String>, Error> {
if !has(name) {
return Ok(None);
}
key.get_string(name)
.map(Some)
.map_err(|_| bad(name, "must be a REG_SZ".to_string()))
};
let mode = match word("mode")?.as_deref() {
None => None,
Some("replace") => Some(Mode::Replace),
Some("append") => Some(Mode::Append),
Some(other) => {
return Err(bad(
"mode",
format!("must be \"replace\" or \"append\", not \"{other}\""),
))
}
};
let notify = match word("notify")?.as_deref() {
None => None,
Some("everything") => Some(Notify::Everything),
Some("important") => Some(Notify::Important),
Some(other) => {
return Err(bad(
"notify",
format!("must be \"everything\" or \"important\", not \"{other}\""),
))
}
};
Ok(Some(Layer {
allowed: list("allowed")?,
mode,
denied: list("denied")?,
user_may_extend: flag("user_may_extend")?,
confirm_each_write_back: flag("confirm_each_write_back")?,
notify,
}))
}
#[cfg(test)]
mod tests {
use super::{without_the_terminator, Hive, Registry, APPLICATION_KEY, POLICY_KEY};
use crate::policy::{Origin, Source};
#[test]
fn the_three_layers_are_the_ones_concept_ten_names() {
let r = Registry::for_this_platform();
let found = r.locations();
assert_eq!(found.len(), 3);
assert_eq!(
found[0],
(Origin::MachinePolicy, format!(r"HKLM\{POLICY_KEY}"))
);
assert_eq!(
found[1],
(Origin::UserPolicy, format!(r"HKCU\{POLICY_KEY}"))
);
assert_eq!(
found[2],
(Origin::Configuration, format!(r"HKCU\{APPLICATION_KEY}"))
);
}
#[test]
fn the_layers_are_listed_with_the_one_that_wins_at_the_top() {
let found = Registry::for_this_platform().locations();
let origins: Vec<Origin> = found.iter().map(|(o, _)| *o).collect();
let mut sorted = origins.clone();
sorted.sort_by(|a, b| b.cmp(a));
assert_eq!(origins, sorted);
}
#[test]
fn a_source_asked_for_a_layer_it_has_no_key_for_says_nothing() {
let r = Registry::none();
assert!(r.layer(Origin::MachinePolicy).unwrap().is_none());
assert!(r.layer(Origin::BuiltIn).unwrap().is_none());
}
#[test]
fn the_empty_string_that_ends_a_multi_sz_is_not_an_extension() {
assert_eq!(
without_the_terminator(vec!["txt".into(), "dwg".into(), String::new()]),
vec!["txt".to_string(), "dwg".to_string()]
);
assert_eq!(
without_the_terminator(vec!["txt".into(), String::new(), String::new()]),
vec!["txt".to_string()]
);
assert!(without_the_terminator(vec![String::new()]).is_empty());
assert_eq!(
without_the_terminator(vec!["txt".into()]),
vec!["txt".to_string()]
);
}
#[test]
fn an_empty_entry_in_the_middle_is_left_for_the_resolution_to_object_to() {
assert_eq!(
without_the_terminator(vec!["txt".into(), String::new(), "dwg".into()]),
vec!["txt".to_string(), String::new(), "dwg".to_string()]
);
}
const EVERY_VALUE: [&str; 6] = [
"allowed",
"mode",
"denied",
"user_may_extend",
"confirm_each_write_back",
"notify",
];
#[test]
fn the_admx_writes_the_keys_and_values_this_module_reads() {
let admx = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("packaging/windows/policy/SlipcaseOpen.admx");
let text =
std::fs::read_to_string(&admx).unwrap_or_else(|e| panic!("{}: {e}", admx.display()));
let attribute = |name: &str| -> Vec<String> {
let needle = format!("{name}=\"");
text.match_indices(&needle)
.filter_map(|(at, _)| {
let rest = &text[at + needle.len()..];
rest.find('"').map(|end| rest[..end].to_string())
})
.collect()
};
let keys = attribute("key");
assert!(!keys.is_empty(), "the ADMX names no keys at all");
for key in &keys {
assert_eq!(key, POLICY_KEY, "the ADMX writes a key nothing reads");
}
let mut written: Vec<String> = attribute("valueName");
written.sort();
written.dedup();
let mut read: Vec<String> = EVERY_VALUE.iter().map(|s| (*s).to_string()).collect();
read.sort();
assert_eq!(written, read);
}
#[test]
fn a_hive_is_named_the_way_an_administrator_would_type_it() {
assert_eq!(Hive::Machine.short(), "HKLM");
assert_eq!(Hive::User.short(), "HKCU");
}
}