use core::ffi::c_void;
use core::ptr;
use super::ffi::{self, Handle};
const MAX_LEN: usize = 128;
#[must_use]
pub(super) fn is_valid(app_id: &str) -> bool {
!app_id.is_empty() && app_id.encode_utf16().count() <= MAX_LEN && !app_id.contains(' ')
}
pub(super) fn set(hwnd: Handle, app_id: &str) -> bool {
if app_id.is_empty() {
return false;
}
if !is_valid(app_id) {
crcbl_core::log::warn!(
"win32: app_id {app_id:?} is not an AppUserModelID (at most {MAX_LEN} characters, \
no spaces); the window is not labelled with it"
);
return false;
}
let Some(wide) = ffi::wide(app_id) else {
return false;
};
let value = PropVariant {
vt: VT_LPWSTR,
reserved: [0; 3],
value: PropValue {
pointer: wide.as_ptr(),
},
};
store_value(hwnd, &value, "set")
}
pub(super) fn clear(hwnd: Handle) {
let empty = PropVariant {
vt: VT_EMPTY,
reserved: [0; 3],
value: PropValue { padding: [0; 2] },
};
store_value(hwnd, &empty, "clear");
}
fn store_value(hwnd: Handle, value: &PropVariant, what: &str) -> bool {
let mut store: *mut c_void = ptr::null_mut();
let got = unsafe { SHGetPropertyStoreForWindow(hwnd, &IID_IPROPERTY_STORE, &raw mut store) };
if got < 0 || store.is_null() {
crcbl_core::log::warn!(
"win32: no property store for a window ({got:#010x}); could not {what} its app_id"
);
return false;
}
let stored = unsafe {
let vtbl = *store.cast::<*const PropertyStoreVtbl>();
let stored = ((*vtbl).set_value)(store, &PKEY_APP_USER_MODEL_ID, value);
((*vtbl).release)(store);
stored
};
if stored < 0 {
crcbl_core::log::warn!(
"win32: the window's property store refused to {what} its app_id ({stored:#010x})"
);
}
stored >= 0
}
#[repr(C)]
pub(super) struct Guid {
data1: u32,
data2: u16,
data3: u16,
data4: [u8; 8],
}
#[repr(C)]
pub(super) struct PropertyKey {
fmtid: Guid,
pid: u32,
}
const IID_IPROPERTY_STORE: Guid = Guid {
data1: 0x886d_8eeb,
data2: 0x8cf2,
data3: 0x4446,
data4: [0x8d, 0x02, 0xcd, 0xba, 0x1d, 0xbd, 0xcf, 0x99],
};
pub(super) const PKEY_APP_USER_MODEL_ID: PropertyKey = PropertyKey {
fmtid: Guid {
data1: 0x9f4c_2855,
data2: 0x9f79,
data3: 0x4b39,
data4: [0xa8, 0xd0, 0xe1, 0xd4, 0x2d, 0xe1, 0xd5, 0xf3],
},
pid: 5,
};
const VT_EMPTY: u16 = 0;
const VT_LPWSTR: u16 = 31;
#[repr(C)]
pub(super) struct PropVariant {
vt: u16,
reserved: [u16; 3],
value: PropValue,
}
#[repr(C)]
union PropValue {
pointer: *const u16,
padding: [usize; 2],
}
#[repr(C)]
struct PropertyStoreVtbl {
query_interface: usize,
add_ref: usize,
release: unsafe extern "system" fn(*mut c_void) -> u32,
get_count: usize,
get_at: usize,
get_value: usize,
set_value:
unsafe extern "system" fn(*mut c_void, *const PropertyKey, *const PropVariant) -> i32,
commit: usize,
}
#[link(name = "shell32")]
unsafe extern "system" {
fn SHGetPropertyStoreForWindow(hwnd: Handle, riid: *const Guid, ppv: *mut *mut c_void) -> i32;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_app_id_windows_accepts_is_short_and_has_no_spaces() {
assert!(is_valid("Kryptic.Crcbl.Sandbox"));
assert!(is_valid(&"a".repeat(MAX_LEN)));
assert!(!is_valid(&"a".repeat(MAX_LEN + 1)), "one past the limit");
assert!(!is_valid("Kryptic Crcbl"), "a space");
assert!(!is_valid(""), "nothing");
}
#[test]
fn the_property_layouts_are_the_sdks() {
use windows::Win32::Foundation::PROPERTYKEY;
use windows::Win32::System::Com::StructuredStorage::PROPVARIANT;
assert_eq!(size_of::<PropVariant>(), size_of::<PROPVARIANT>());
assert_eq!(align_of::<PropVariant>(), align_of::<PROPVARIANT>());
assert_eq!(size_of::<PropertyKey>(), size_of::<PROPERTYKEY>());
assert_eq!(core::mem::offset_of!(PropVariant, value), 8);
let key = windows::Win32::Storage::EnhancedStorage::PKEY_AppUserModel_ID;
assert_eq!(key.pid, PKEY_APP_USER_MODEL_ID.pid);
let ours = &PKEY_APP_USER_MODEL_ID.fmtid;
assert_eq!(
key.fmtid,
windows::core::GUID::from_values(ours.data1, ours.data2, ours.data3, ours.data4)
);
assert_eq!(
<windows::Win32::UI::Shell::PropertiesSystem::IPropertyStore as windows::core::Interface>::IID,
windows::core::GUID::from_values(
IID_IPROPERTY_STORE.data1,
IID_IPROPERTY_STORE.data2,
IID_IPROPERTY_STORE.data3,
IID_IPROPERTY_STORE.data4,
)
);
}
}