use crate::{ShellError, WebUrl};
use super::ffi::{self, Handle, value};
const LAST_ERROR_CODE: usize = 32;
pub(super) fn open(owner: Handle, url: &WebUrl) -> Result<(), ShellError> {
let verb = ffi::wide("open").ok_or_else(|| interior_nul("the verb"))?;
let file = ffi::wide(url.as_str()).ok_or_else(|| interior_nul("the URL"))?;
let _apartment = Apartment::enter()?;
let answer = unsafe {
ffi::ShellExecuteW(
owner,
verb.as_ptr(),
file.as_ptr(),
core::ptr::null(),
core::ptr::null(),
value::SW_SHOW_NORMAL,
)
};
let code = answer as usize;
if code > LAST_ERROR_CODE {
Ok(())
} else {
Err(ShellError::Backend(format!(
"ShellExecuteW could not open {url}: {} ({code})",
refusal_name(code)
)))
}
}
fn interior_nul(what: &str) -> ShellError {
ShellError::Backend(format!(
"{what} holds a NUL, which ShellExecuteW cannot take"
))
}
fn refusal_name(code: usize) -> &'static str {
match code {
0 => "out of memory or resources",
2 => "ERROR_FILE_NOT_FOUND",
3 => "ERROR_PATH_NOT_FOUND",
5 => "SE_ERR_ACCESSDENIED",
8 => "SE_ERR_OOM",
11 => "ERROR_BAD_FORMAT",
26 => "SE_ERR_SHARE",
27 => "SE_ERR_ASSOCINCOMPLETE",
28 => "SE_ERR_DDETIMEOUT",
29 => "SE_ERR_DDEFAIL",
30 => "SE_ERR_DDEBUSY",
31 => "SE_ERR_NOASSOC, no program is registered for the scheme",
32 => "SE_ERR_DLLNOTFOUND",
_ => "an undocumented code",
}
}
struct Apartment {
owes: bool,
}
impl Apartment {
fn enter() -> Result<Self, ShellError> {
let result = unsafe {
ffi::CoInitializeEx(
core::ptr::null(),
value::COINIT_APARTMENT_THREADED | value::COINIT_DISABLE_OLE1DDE,
)
};
match result {
value::S_OK | value::S_FALSE => Ok(Self { owes: true }),
value::RPC_E_CHANGED_MODE => Ok(Self { owes: false }),
refused => Err(ShellError::Backend(format!(
"CoInitializeEx refused the thread before ShellExecuteW: {:#010x}",
refused.cast_unsigned()
))),
}
}
}
impl Drop for Apartment {
fn drop(&mut self) {
if self.owes {
unsafe { ffi::CoUninitialize() };
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const COINIT_MULTITHREADED: u32 = 0x0;
#[test]
fn the_apartment_is_left_as_it_was_found() {
std::thread::spawn(|| {
drop(Apartment::enter().expect("a fresh thread enters"));
let multithreaded =
unsafe { ffi::CoInitializeEx(core::ptr::null(), COINIT_MULTITHREADED) };
assert_eq!(multithreaded, value::S_OK, "nothing was left behind");
let inside = Apartment::enter().expect("the host's apartment is used");
assert!(!inside.owes);
drop(inside);
unsafe { ffi::CoUninitialize() };
})
.join()
.expect("the test thread");
}
#[test]
fn a_hosts_apartment_survives_the_call() {
std::thread::spawn(|| {
let host =
unsafe { ffi::CoInitializeEx(core::ptr::null(), value::COINIT_APARTMENT_THREADED) };
assert_eq!(host, value::S_OK);
let inside = Apartment::enter().expect("the host's apartment is joined");
assert!(inside.owes, "S_FALSE is counted and owes its uninit");
drop(inside);
let multithreaded =
unsafe { ffi::CoInitializeEx(core::ptr::null(), COINIT_MULTITHREADED) };
assert_eq!(multithreaded, value::RPC_E_CHANGED_MODE);
unsafe { ffi::CoUninitialize() };
})
.join()
.expect("the test thread");
}
#[test]
fn an_error_code_is_named() {
assert_eq!(
refusal_name(31),
"SE_ERR_NOASSOC, no program is registered for the scheme"
);
assert_eq!(refusal_name(2), "ERROR_FILE_NOT_FOUND");
assert_eq!(refusal_name(17), "an undocumented code");
}
}