Source code
Revision control
Copy as Markdown
Other Tools
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
#![allow(non_snake_case)]
//! # Taskband Pin COM API
//!
//! Taskband Pin is a COM class implementing the undocumented IPinnedList3
//! interface that affords pinning and unpinning from the Windows taskbar for
//! unpackaged Win32 (non-MSIX) applications. Chromium source documents this API
//! as functional starting with Windows 10 RS5 and ending as of Windows 11 24H2.
//!
//! ## Pinning
//!
//! In Windows 10 and early versions of Windows 11 this API pins without
//! prompting the user.
//!
//! Later versions of Windows 11 prior to Windows 11 24H2 instead prompt the
//! user if they wish to pin with a toast notification.
//!
//! For Windows 11 24H2 and later this API reports success even though pinning
//! is not successful.
//!
//! ## Unpinning
//!
//! This API can be used to unpin an application past Windows 11 24H2.
//!
//! ## Requirements
//!
//! The IPinnedList3 API can pin shortcuts from any directory.
use nserror::{
nsresult, NS_ERROR_FILE_ACCESS_DENIED, NS_ERROR_FILE_NOT_FOUND, NS_ERROR_NOT_AVAILABLE,
};
use nsstring::{nsAString, nsString};
use windows::{
core::{interface, IUnknown, IUnknown_Vtbl, GUID, HRESULT, PCWSTR},
Win32::{
System::Com::{CoCreateInstance, CLSCTX_INPROC_SERVER},
UI::Shell::{Common::ITEMIDLIST, ILCreateFromPathW, ILFree},
},
};
use super::PinResult;
use crate::util::thread::MainThreadGuard;
pub(super) enum PinOp {
Pin,
UnPin,
}
pub(super) fn is_pinning_available(
// Taskband Pin COM class is registered ThreadingModel=Apartment, so
// instances are apartment-threaded and must live in an STA. There exists no
// marshaler for Taskband Pin therefore we must ensure we're on an STA
// thread before attempting to create it.
//
// We can ensure we're in an STA by running on the main thread; background
// threads rely on implicit MTA thus should not be used here.
_main_guard: MainThreadGuard,
) -> bool {
// SAFETY: Taskband Pin is a known, undocumented Windows COM API. The
// interface is therefore not stable but in practice has not been found to
// change, and historically has been extended instead of modified as
// evidenced by this being the third iteration. We redefine the interface
// below.
unsafe { CoCreateInstance::<_, IPinnedList3>(&CLSID_TASKBAND_PIN, None, CLSCTX_INPROC_SERVER) }
.is_ok()
}
/// Pins or unpins the provided shortcut to the taskbar using Taskband Pin's
/// IPinnedList3 implementation, optionally only verifies the COM object with
/// relevant interface is available.
pub(super) fn modify_taskbar(
pin_op: PinOp,
shortcut_path: &nsAString,
// Taskband Pin COM class is registered ThreadingModel=Apartment, so
// instances are apartment-threaded and must live in an STA. There exists no
// marshaler for Taskband Pin therefore we must ensure we're on an STA
// thread before attempting to create it.
//
// We can ensure we're in an STA by running on the main thread; background
// threads rely on implicit MTA thus should not be used here.
_main_guard: MainThreadGuard,
) -> Result<PinResult, nsresult> {
// Ensure path is a null-terminated string.
let shortcut_path: nsString = shortcut_path.into();
// SAFETY: Taskband Pin is a known, undocumented Windows COM API. The
// interface is therefore not stable but in practice has not been found to
// change, and historically has been extended instead of modified as
// evidenced by this being the third iteration. We redefine the interface
// below.
let pinned_list: IPinnedList3 =
unsafe { CoCreateInstance(&CLSID_TASKBAND_PIN, None, CLSCTX_INPROC_SERVER) }
.map_err(|_| NS_ERROR_NOT_AVAILABLE)?;
log::info!("COM pinning API is available.");
// SAFETY: nsString is null-terminated utf-16 thus valid to construct a
// PCWSTR from as an argument to ILCreateFromPathW.
let pidl = scopeguard::guard(
unsafe { ILCreateFromPathW(PCWSTR::from_raw(shortcut_path.as_ptr())) } as *const _,
// SAFETY: ITEMIDLIST was either created in the above guard, or null for
// invalid paths or null pointer as input. ILFree accepts null as valid.
|pidl| unsafe { ILFree(Some(pidl)) },
);
// ILCreateFromPathW returns null when the path is non-existent or when the
// parameter is null. This behavior is not documented, but was verified to
// occur.
if pidl.is_null() {
log::error!("Failed to create identifier list from shortcut path {shortcut_path}");
return Err(NS_ERROR_FILE_NOT_FOUND);
}
let (unpin_pidl, pin_pidl) = match pin_op {
PinOp::Pin => (std::ptr::null(), *pidl),
PinOp::UnPin => (*pidl, std::ptr::null()),
};
if xpcom::is_in_automation() {
// Return early in tests to avoid actually pinning the app.
return Ok(PinResult::Unknown);
}
// SAFETY: ITEMIDLIST arguments are defined above and either initialized or
// set to null (known valid for this API).
unsafe { pinned_list.Modify(unpin_pidl, pin_pidl, PinnedListModifyCallerEnum::MAX) }
.ok()
.map_err(|e| {
log::error!("Error modifying the taskbar: {e:?}");
NS_ERROR_FILE_ACCESS_DENIED
})?;
log::info!("Usage of Taskband Pin COM API ran to end.");
// IPinnedList3::Modify may have succeeded but depending on the version of
// Windows either successfully pinned, prompt the user whether to pin the
// app, or not pinned the app. We therefore do not know the result of our
// attempt to pin.
Ok(PinResult::Unknown)
}
// The types below, and the idea of using IPinnedList3::Modify, are thanks to
//
// Taskband Pin is registered in the Windows Registry at:
// HKEY_CLASSES_ROOT\CLSID\{90AA3A4E-1CBA-4233-B8BB-535773D48449}
const CLSID_TASKBAND_PIN: GUID = GUID::from_u128(0x90AA3A4E_1CBA_4233_B8BB_535773D48449);
// Note: This definition mirrors how the windows crate defines COM enums.
#[repr(transparent)]
struct PinnedListModifyCallerEnum(i32);
impl PinnedListModifyCallerEnum {
// This enum is likely only used for Windows telemetry, i32::MAX is chosen
// to avoid confusion with existing uses.
const MAX: Self = Self(i32::MAX);
}
// Enum to prevent usage of IPinnedList3 methods with incomplete parameter
// definitions.
enum IncompleteDefinition {}
#[interface("0dd79ae2-d156-45d4-9eeb-3b549769e940")]
unsafe trait IPinnedList3: IUnknown {
// Note: We're recreating a Windows internal COM interface; method
// declarations must not be reordered.
unsafe fn EnumObjects(&self, i: IncompleteDefinition) -> HRESULT;
unsafe fn GetPinnableInfo(&self, i: IncompleteDefinition) -> HRESULT;
unsafe fn IsPinnable(&self, i: IncompleteDefinition) -> HRESULT;
unsafe fn Resolve(&self, i: IncompleteDefinition) -> HRESULT;
unsafe fn LegacyModify(&self, i: IncompleteDefinition) -> HRESULT;
unsafe fn GetChangeCount(&self, i: IncompleteDefinition) -> HRESULT;
unsafe fn IsPinned(&self, i: IncompleteDefinition) -> HRESULT;
unsafe fn GetPinnedItem(&self, i: IncompleteDefinition) -> HRESULT;
unsafe fn GetAppIDForPinnedItem(&self, i: IncompleteDefinition) -> HRESULT;
unsafe fn ItemChangeNotify(&self, i: IncompleteDefinition) -> HRESULT;
unsafe fn UpdateForRemovedItemsAsNecessary(&self, i: IncompleteDefinition) -> HRESULT;
unsafe fn PinShellLink(&self, i: IncompleteDefinition) -> HRESULT;
unsafe fn GetPinnedItemForAppID(&self, i: IncompleteDefinition) -> HRESULT;
unsafe fn Modify(
&self,
unpin: *const ITEMIDLIST,
pin: *const ITEMIDLIST,
caller: PinnedListModifyCallerEnum,
) -> HRESULT;
}