pub const CLSID_TEXT: &str = "{7F1B483A-5853-4348-9081-D5BD502B51E8}";
pub const PROG_ID: &str = "Denise.Panel.1";
pub const VERSION_INDEPENDENT_PROG_ID: &str = "Denise.Panel";
pub const FRIENDLY_NAME: &str = "Denise Panel Control";
pub const VERSION: &str = "1.0";
pub const MISC_STATUS: u32 = 1 | 16 | 128 | 256 | 131_072;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Entry {
pub key: String,
pub name: String,
pub value: String,
}
impl Entry {
fn new(key: impl Into<String>, name: impl Into<String>, value: impl Into<String>) -> Self {
Self {
key: key.into(),
name: name.into(),
value: value.into(),
}
}
}
pub fn entries(server_path: &str) -> Vec<Entry> {
let clsid = format!("CLSID\\{CLSID_TEXT}");
vec![
Entry::new(VERSION_INDEPENDENT_PROG_ID, "", FRIENDLY_NAME),
Entry::new(
format!("{VERSION_INDEPENDENT_PROG_ID}\\CLSID"),
"",
CLSID_TEXT,
),
Entry::new(
format!("{VERSION_INDEPENDENT_PROG_ID}\\CurVer"),
"",
PROG_ID,
),
Entry::new(PROG_ID, "", FRIENDLY_NAME),
Entry::new(format!("{PROG_ID}\\CLSID"), "", CLSID_TEXT),
Entry::new(&clsid, "", FRIENDLY_NAME),
Entry::new(format!("{clsid}\\InprocServer32"), "", server_path),
Entry::new(
format!("{clsid}\\InprocServer32"),
"ThreadingModel",
"Apartment",
),
Entry::new(format!("{clsid}\\ProgID"), "", PROG_ID),
Entry::new(
format!("{clsid}\\VersionIndependentProgID"),
"",
VERSION_INDEPENDENT_PROG_ID,
),
Entry::new(format!("{clsid}\\Control"), "", ""),
Entry::new(format!("{clsid}\\Programmable"), "", ""),
Entry::new(format!("{clsid}\\Version"), "", VERSION),
Entry::new(format!("{clsid}\\MiscStatus"), "", "0"),
Entry::new(
format!("{clsid}\\MiscStatus\\1"),
"",
MISC_STATUS.to_string(),
),
Entry::new(
format!("{clsid}\\ToolboxBitmap32"),
"",
format!("{server_path}, 1"),
),
]
}
pub fn keys_to_remove() -> Vec<String> {
let clsid = format!("CLSID\\{CLSID_TEXT}");
vec![
format!("{clsid}\\MiscStatus\\1"),
format!("{clsid}\\MiscStatus"),
format!("{clsid}\\ToolboxBitmap32"),
format!("{clsid}\\Version"),
format!("{clsid}\\Programmable"),
format!("{clsid}\\Control"),
format!("{clsid}\\VersionIndependentProgID"),
format!("{clsid}\\ProgID"),
format!("{clsid}\\InprocServer32"),
clsid,
format!("{PROG_ID}\\CLSID"),
PROG_ID.to_string(),
format!("{VERSION_INDEPENDENT_PROG_ID}\\CurVer"),
format!("{VERSION_INDEPENDENT_PROG_ID}\\CLSID"),
VERSION_INDEPENDENT_PROG_ID.to_string(),
]
}
#[cfg(test)]
mod tests {
use super::*;
const PATH: &str = "C:\\Program Files\\Denise\\denise_activex.dll";
fn value_of(entries: &[Entry], key: &str, name: &str) -> Option<String> {
entries
.iter()
.find(|e| e.key == key && e.name == name)
.map(|e| e.value.clone())
}
#[test]
fn the_class_is_registered_where_a_host_looks_for_it() {
let entries = entries(PATH);
let clsid = format!("CLSID\\{CLSID_TEXT}");
assert_eq!(
value_of(&entries, &format!("{clsid}\\InprocServer32"), ""),
Some(PATH.to_string()),
"without the server path there is nothing to load"
);
assert_eq!(
value_of(
&entries,
&format!("{clsid}\\InprocServer32"),
"ThreadingModel"
),
Some("Apartment".to_string()),
"the window procedure is thread-affine; anything else is a race"
);
assert!(
entries.iter().any(|e| e.key == format!("{clsid}\\Control")),
"without the Control key a host will not offer it in a toolbox"
);
assert_eq!(
value_of(&entries, &format!("{clsid}\\ProgID"), ""),
Some(PROG_ID.to_string())
);
}
#[test]
fn the_prog_ids_and_the_clsid_agree_in_both_directions() {
let entries = entries(PATH);
assert_eq!(
value_of(&entries, &format!("{PROG_ID}\\CLSID"), ""),
Some(CLSID_TEXT.to_string())
);
assert_eq!(
value_of(
&entries,
&format!("{VERSION_INDEPENDENT_PROG_ID}\\CLSID"),
""
),
Some(CLSID_TEXT.to_string())
);
assert_eq!(
value_of(
&entries,
&format!("{VERSION_INDEPENDENT_PROG_ID}\\CurVer"),
""
),
Some(PROG_ID.to_string())
);
assert!(
PROG_ID.starts_with(VERSION_INDEPENDENT_PROG_ID),
"the versioned ProgID must extend the version-independent one"
);
}
#[test]
fn the_misc_status_flags_are_the_ones_a_windowed_control_needs() {
assert_eq!(MISC_STATUS & 131_072, 131_072, "OLEMISC_SETCLIENTSITEFIRST");
assert_eq!(MISC_STATUS & 256, 256, "OLEMISC_ACTIVATEWHENVISIBLE");
assert_eq!(MISC_STATUS & 128, 128, "OLEMISC_INSIDEOUT");
assert_eq!(MISC_STATUS & 1, 1, "OLEMISC_RECOMPOSEONRESIZE");
assert_eq!(MISC_STATUS, 131_473);
let entries = entries(PATH);
assert_eq!(
value_of(&entries, &format!("CLSID\\{CLSID_TEXT}\\MiscStatus\\1"), ""),
Some(MISC_STATUS.to_string())
);
}
#[test]
fn unregistration_removes_children_before_their_parents() {
let keys = keys_to_remove();
for (index, key) in keys.iter().enumerate() {
for (other_index, other) in keys.iter().enumerate() {
if other != key && other.starts_with(&format!("{key}\\")) {
assert!(
other_index < index,
"{other} is inside {key} and must be removed first"
);
}
}
}
}
#[test]
fn every_key_written_is_a_key_removed() {
let removed = keys_to_remove();
for entry in entries(PATH) {
assert!(
removed.contains(&entry.key),
"{} is registered and never unregistered",
entry.key
);
}
}
#[test]
fn the_clsid_is_a_well_formed_braced_guid() {
assert_eq!(CLSID_TEXT.len(), 38);
assert!(CLSID_TEXT.starts_with('{') && CLSID_TEXT.ends_with('}'));
let inner = &CLSID_TEXT[1..CLSID_TEXT.len() - 1];
let groups: Vec<&str> = inner.split('-').collect();
assert_eq!(
groups.iter().map(|g| g.len()).collect::<Vec<_>>(),
vec![8, 4, 4, 4, 12]
);
assert!(inner.chars().all(|c| c.is_ascii_hexdigit() || c == '-'));
assert!(
inner.chars().all(|c| !c.is_ascii_lowercase()),
"the registry compares these textually in places; keep one casing"
);
}
}