#![expect(
unsafe_code,
reason = "IOKit USB plug-in (IOUSBDeviceInterface182) FFI for UVC control transfers"
)]
use std::ffi::c_void;
use std::num::NonZeroU32;
use std::ptr;
use std::ptr::NonNull;
use objc2_core_foundation::{CFDictionary, CFRetained, CFString, CFType, CFUUID};
use objc2_io_kit::{
IOCFPlugInInterface, IOCreatePlugInInterfaceForService, IODestroyPlugInInterface,
IOIteratorNext, IOObjectRelease, IORegistryEntryCreateCFProperty, IOServiceGetMatchingServices,
IOServiceMatching, IOUSBConfigurationDescriptor, IOUSBConfigurationDescriptorPtr,
IOUSBDevRequest, IOUSBDeviceInterface182, io_iterator_t, io_object_t, kIOMainPortDefault,
};
const SUCCESS: i32 = 0;
pub(super) struct IoObject(NonZeroU32);
impl IoObject {
fn adopt(raw: io_object_t) -> Option<Self> {
NonZeroU32::new(raw).map(Self)
}
fn raw(&self) -> io_object_t {
self.0.get()
}
}
impl Drop for IoObject {
fn drop(&mut self) {
let _ = IOObjectRelease(self.0.get());
}
}
pub(super) struct IoServices(Option<IoObject>);
impl Iterator for IoServices {
type Item = IoObject;
fn next(&mut self) -> Option<IoObject> {
IoObject::adopt(IOIteratorNext(self.0.as_ref()?.raw()))
}
}
pub(super) fn usb_devices() -> Result<IoServices, &'static str> {
let matching =
unsafe { IOServiceMatching(c"IOUSBDevice".as_ptr()) }.ok_or("IOServiceMatching")?;
let matching = CFRetained::<CFDictionary>::from(&*matching);
let mut iterator: io_iterator_t = 0;
let rc = unsafe {
IOServiceGetMatchingServices(kIOMainPortDefault, Some(matching), &raw mut iterator)
};
if rc != SUCCESS {
return Err("IOServiceGetMatchingServices");
}
Ok(IoServices(IoObject::adopt(iterator)))
}
pub(super) fn registry_property(entry: &IoObject, key: &CFString) -> Option<CFRetained<CFType>> {
unsafe { IORegistryEntryCreateCFProperty(entry.raw(), Some(key), None, 0) }
}
const USB_DEVICE_USER_CLIENT_TYPE_ID: [u8; 16] = [
0x9d, 0xc7, 0xb7, 0x80, 0x9e, 0xc0, 0x11, 0xd4, 0xa5, 0x4f, 0x00, 0x0a, 0x27, 0x05, 0x28, 0x61,
];
const CF_PLUGIN_INTERFACE_ID: [u8; 16] = [
0xc2, 0x44, 0xe8, 0x58, 0x10, 0x9c, 0x11, 0xd4, 0x91, 0xd4, 0x00, 0x50, 0xe4, 0xc6, 0x42, 0x6f,
];
const USB_DEVICE_INTERFACE_ID_182: [u8; 16] = [
0x15, 0x2f, 0xc4, 0x96, 0x48, 0x91, 0x11, 0xd5, 0x9d, 0x52, 0x00, 0x0a, 0x27, 0x80, 0x1e, 0x86,
];
fn uuid(b: [u8; 16]) -> Option<CFRetained<CFUUID>> {
CFUUID::constant_uuid_with_bytes(
None, b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7], b[8], b[9], b[10], b[11], b[12],
b[13], b[14], b[15],
)
}
pub(super) struct UsbInterface(NonNull<*mut IOUSBDeviceInterface182>);
impl UsbInterface {
pub(super) fn open(service: &IoObject) -> Option<Self> {
let user_client = uuid(USB_DEVICE_USER_CLIENT_TYPE_ID)?;
let plugin_type = uuid(CF_PLUGIN_INTERFACE_ID)?;
let device_type = uuid(USB_DEVICE_INTERFACE_ID_182)?;
let mut plugin: *mut *mut IOCFPlugInInterface = ptr::null_mut();
let mut score = 0i32;
let rc = unsafe {
IOCreatePlugInInterfaceForService(
service.raw(),
Some(&user_client),
Some(&plugin_type),
&raw mut plugin,
&raw mut score,
)
};
if rc != SUCCESS {
return None;
}
let plugin = NonNull::new(plugin)?;
let device = unsafe {
let mut device: *mut c_void = ptr::null_mut();
let queried = (**plugin.as_ptr()).QueryInterface.is_some_and(|query| {
query(
plugin.as_ptr().cast(),
device_type.uuid_bytes(),
&raw mut device,
) == SUCCESS
});
let _ = IODestroyPlugInInterface(plugin.as_ptr());
if !queried {
return None;
}
device
};
NonNull::new(device.cast::<*mut IOUSBDeviceInterface182>()).map(Self)
}
fn vtable(&self) -> &IOUSBDeviceInterface182 {
unsafe { &**self.0.as_ptr() }
}
fn this(&self) -> *mut c_void {
self.0.as_ptr().cast()
}
pub(super) fn vendor_id(&self) -> Option<u16> {
let get = self.vtable().GetDeviceVendor?;
let mut vendor = 0u16;
(unsafe { get(self.this(), &raw mut vendor) } == SUCCESS).then_some(vendor)
}
pub(super) fn location_id(&self) -> Option<u32> {
let get = self.vtable().GetLocationID?;
let mut location = 0u32;
(unsafe { get(self.this(), &raw mut location) } == SUCCESS).then_some(location)
}
pub(super) fn seize(self) -> Option<SeizedDevice> {
let open = self.vtable().USBDeviceOpenSeize?;
if unsafe { open(self.this()) } != SUCCESS {
return None;
}
Some(SeizedDevice(self))
}
}
impl Drop for UsbInterface {
fn drop(&mut self) {
let Some(release) = self.vtable().Release else {
return;
};
unsafe { release(self.this()) };
}
}
pub(super) struct SeizedDevice(UsbInterface);
impl SeizedDevice {
#[must_use]
pub(super) fn control_request(&self, request: &mut IOUSBDevRequest) -> bool {
let Some(device_request) = self.0.vtable().DeviceRequest else {
return false;
};
unsafe { device_request(self.0.this(), &raw mut *request) == SUCCESS }
}
pub(super) fn configuration_count(&self) -> Option<u8> {
let get = self.0.vtable().GetNumberOfConfigurations?;
let mut count = 0u8;
(unsafe { get(self.0.this(), &raw mut count) } == SUCCESS).then_some(count)
}
pub(super) fn configuration_descriptor(&self, index: u8) -> Option<&[u8]> {
let get = self.0.vtable().GetConfigurationDescriptorPtr?;
let mut header: IOUSBConfigurationDescriptorPtr = ptr::null_mut();
unsafe {
if get(self.0.this(), index, &raw mut header) != SUCCESS {
return None;
}
let header = NonNull::new(header)?;
let total = usize::from((*header.as_ptr()).wTotalLength);
(total >= size_of::<IOUSBConfigurationDescriptor>())
.then(|| std::slice::from_raw_parts(header.as_ptr().cast::<u8>(), total))
}
}
}
impl Drop for SeizedDevice {
fn drop(&mut self) {
let Some(close) = self.0.vtable().USBDeviceClose else {
return;
};
unsafe { close(self.0.this()) };
}
}