pub const CALL: u16 = 0x1;
pub const GET: u16 = 0x2;
pub const PUT: u16 = 0x4;
pub const PUTREF: u16 = 0x8;
pub const DISPID_UNKNOWN: i32 = -1;
pub const TEXT: u16 = 8;
pub const FLAG: u16 = 11;
pub const VOID: u16 = 24;
pub const DISPID_TEXT: i32 = 1;
pub const DISPID_CAPTION: i32 = 2;
pub const DISPID_ENABLED: i32 = 3;
pub const DISPID_REFRESH: i32 = 4;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Member {
pub dispid: i32,
pub name: &'static str,
pub flags: u16,
pub vt: u16,
}
pub const MEMBERS: &[Member] = &[
Member {
dispid: DISPID_TEXT,
name: "Text",
flags: GET | PUT,
vt: TEXT,
},
Member {
dispid: DISPID_CAPTION,
name: "Caption",
flags: GET | PUT,
vt: TEXT,
},
Member {
dispid: DISPID_ENABLED,
name: "Enabled",
flags: GET | PUT,
vt: FLAG,
},
Member {
dispid: DISPID_REFRESH,
name: "Refresh",
flags: CALL,
vt: VOID,
},
];
pub const DISPID_CLICK: i32 = -600;
pub const DISPID_CHANGE: i32 = 1;
pub const EVENTS: &[Member] = &[
Member {
dispid: DISPID_CLICK,
name: "Click",
flags: CALL,
vt: VOID,
},
Member {
dispid: DISPID_CHANGE,
name: "Change",
flags: CALL,
vt: VOID,
},
];
pub fn member(name: &str) -> Option<&'static Member> {
MEMBERS.iter().find(|m| m.name.eq_ignore_ascii_case(name))
}
pub fn member_by_id(dispid: i32) -> Option<&'static Member> {
MEMBERS.iter().find(|m| m.dispid == dispid)
}
pub fn resolve(names: &[&str], out: &mut [i32]) -> bool {
let mut all_known = true;
for (index, name) in names.iter().enumerate() {
let dispid = match index {
0 => member(name).map(|m| m.dispid),
_ => None,
};
if let Some(slot) = out.get_mut(index) {
*slot = dispid.unwrap_or(DISPID_UNKNOWN);
}
all_known &= dispid.is_some();
}
all_known
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Action {
Get,
Put,
Call,
}
pub fn action(member: &Member, flags: u16) -> Option<Action> {
if flags & (PUT | PUTREF) != 0 && member.flags & PUT != 0 {
return Some(Action::Put);
}
if member.flags & CALL != 0 && flags & (CALL | GET) != 0 {
return Some(Action::Call);
}
if flags & GET != 0 && member.flags & GET != 0 {
return Some(Action::Get);
}
None
}
pub fn events_raised(previous: &str, current: &str, clicked: bool) -> Vec<i32> {
let mut raised = Vec::new();
if previous != current {
raised.push(DISPID_CHANGE);
}
if clicked {
raised.push(DISPID_CLICK);
}
raised
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Entry {
pub dispid: i32,
pub name: &'static str,
pub flags: u16,
pub vt: u16,
pub arguments: u32,
}
pub fn entries() -> Vec<Entry> {
let mut entries = Vec::new();
for member in MEMBERS {
if member.flags & GET != 0 {
entries.push(Entry {
dispid: member.dispid,
name: member.name,
flags: GET,
vt: member.vt,
arguments: 0,
});
}
if member.flags & PUT != 0 {
entries.push(Entry {
dispid: member.dispid,
name: member.name,
flags: PUT,
vt: member.vt,
arguments: 1,
});
}
if member.flags & CALL != 0 {
entries.push(Entry {
dispid: member.dispid,
name: member.name,
flags: CALL,
vt: member.vt,
arguments: 0,
});
}
}
entries
}
pub fn describe() -> String {
use core::fmt::Write as _;
let mut out = String::new();
for m in MEMBERS {
let kind = if m.flags & CALL != 0 {
"method"
} else if m.flags & PUT != 0 {
"property, read/write"
} else {
"property, read-only"
};
let _ = writeln!(out, " {:<10} {:>5} {kind}", m.name, m.dispid);
}
for e in EVENTS {
let _ = writeln!(out, " {:<10} {:>5} event", e.name, e.dispid);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_member_resolves_whatever_case_it_is_written_in() {
for m in MEMBERS {
for spelling in [
m.name.to_ascii_lowercase(),
m.name.to_ascii_uppercase(),
m.name.to_string(),
] {
assert_eq!(
member(&spelling).map(|found| found.dispid),
Some(m.dispid),
"{spelling} did not resolve"
);
}
assert_eq!(member_by_id(m.dispid), Some(m));
}
}
#[test]
fn the_dispids_are_the_ones_that_were_published() {
assert_eq!(DISPID_TEXT, 1);
assert_eq!(DISPID_CAPTION, 2);
assert_eq!(DISPID_ENABLED, 3);
assert_eq!(DISPID_REFRESH, 4);
assert_eq!(DISPID_CLICK, -600, "DISPID_CLICK from olectl.h");
assert_eq!(DISPID_CHANGE, 1);
for (index, m) in MEMBERS.iter().enumerate() {
assert!(m.dispid > 0, "{} must not use a reserved dispid", m.name);
assert!(
MEMBERS[..index]
.iter()
.all(|other| other.dispid != m.dispid),
"{} reuses a dispid",
m.name
);
}
}
#[test]
fn an_unknown_name_is_reported_and_still_written() {
let mut out = [0i32; 1];
assert!(!resolve(&["Nonsense"], &mut out));
assert_eq!(out, [DISPID_UNKNOWN]);
assert!(resolve(&["Caption"], &mut out));
assert_eq!(out, [DISPID_CAPTION]);
}
#[test]
fn a_named_argument_never_resolves_even_when_it_matches_a_member() {
let mut out = [0i32; 2];
assert!(!resolve(&["Refresh", "Caption"], &mut out));
assert_eq!(out, [DISPID_REFRESH, DISPID_UNKNOWN]);
}
#[test]
fn the_ambiguous_flags_every_script_host_sends_land_on_the_right_action() {
let text = member("Text").expect("Text");
let refresh = member("Refresh").expect("Refresh");
assert_eq!(action(text, CALL | GET), Some(Action::Get));
assert_eq!(action(refresh, CALL | GET), Some(Action::Call));
assert_eq!(action(refresh, CALL), Some(Action::Call));
assert_eq!(action(refresh, GET), Some(Action::Call));
}
#[test]
fn both_spellings_of_assignment_are_a_put() {
let caption = member("Caption").expect("Caption");
assert_eq!(action(caption, PUT), Some(Action::Put));
assert_eq!(action(caption, PUTREF), Some(Action::Put));
assert_eq!(action(caption, PUT | PUTREF), Some(Action::Put));
}
#[test]
fn assigning_to_something_that_is_not_a_property_is_refused() {
let refresh = member("Refresh").expect("Refresh");
assert_eq!(action(refresh, PUT), None);
assert_eq!(action(refresh, PUTREF), None);
assert_eq!(action(refresh, 0), None);
}
#[test]
fn a_script_assigning_to_text_does_not_raise_change_at_itself() {
assert!(events_raised("hei", "hei", false).is_empty());
assert_eq!(events_raised("", "h", false), vec![DISPID_CHANGE]);
}
#[test]
fn change_is_raised_before_the_click_that_follows_it() {
assert_eq!(
events_raised("", "hei", true),
vec![DISPID_CHANGE, DISPID_CLICK]
);
assert_eq!(events_raised("hei", "hei", true), vec![DISPID_CLICK]);
}
#[test]
fn a_read_write_property_becomes_two_entries_sharing_one_dispid() {
let entries = entries();
assert_eq!(entries.len(), 7);
for member in MEMBERS {
let mine: Vec<&Entry> = entries
.iter()
.filter(|entry| entry.dispid == member.dispid)
.collect();
for entry in &mine {
assert_eq!(entry.name, member.name);
assert_eq!(entry.vt, member.vt);
assert_eq!(entry.flags.count_ones(), 1, "one operation per entry");
}
}
}
#[test]
fn only_a_put_takes_an_argument() {
for entry in entries() {
assert_eq!(
entry.arguments,
u32::from(entry.flags == PUT),
"{}",
entry.name
);
}
}
#[test]
fn the_description_names_every_member_and_every_event() {
let text = describe();
for m in MEMBERS.iter().chain(EVENTS) {
assert!(text.contains(m.name), "{} is undocumented", m.name);
}
}
#[test]
#[cfg(windows)]
fn the_flags_are_the_ones_oaidl_defines() {
use windows::Win32::System::Com::{
DISPATCH_METHOD, DISPATCH_PROPERTYGET, DISPATCH_PROPERTYPUT, DISPATCH_PROPERTYPUTREF,
};
assert_eq!(CALL, DISPATCH_METHOD.0);
assert_eq!(GET, DISPATCH_PROPERTYGET.0);
assert_eq!(PUT, DISPATCH_PROPERTYPUT.0);
assert_eq!(PUTREF, DISPATCH_PROPERTYPUTREF.0);
assert_eq!(DISPID_UNKNOWN, windows::Win32::System::Ole::DISPID_UNKNOWN);
use windows::Win32::System::Variant::{VT_BOOL, VT_BSTR, VT_EMPTY, VT_VOID};
assert_eq!(TEXT, VT_BSTR.0);
assert_eq!(FLAG, VT_BOOL.0);
assert_eq!(VOID, VT_VOID.0);
assert_ne!(
VOID, VT_EMPTY.0,
"VT_EMPTY is a variant holding nothing, not a return type"
);
}
}