#![expect(
unsafe_code,
reason = "IOKit USB (IOUSBDeviceInterface) control-transfer FFI for UVC Processing-Unit controls"
)]
#![allow(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_sign_loss,
reason = "UVC payloads are bounded 16-bit values copied verbatim"
)]
use std::ffi::{CString, c_void};
use std::ptr;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Unit {
CameraTerminal,
Processing,
}
pub use crate::controls::{
AutoState, AutoToggle, CameraControl, CameraState, ControlError, ControlRange,
};
impl CameraControl {
fn unit(self) -> Unit {
match self {
Self::Zoom | Self::Focus | Self::Exposure => Unit::CameraTerminal,
_ => Unit::Processing,
}
}
#[allow(
clippy::match_same_arms,
reason = "Focus (CT) and Tint (PU) share 0x06 by coincidence — they address different units"
)]
fn selector(self) -> u16 {
match self {
Self::Zoom => 0x0B, Self::Focus => 0x06, Self::Exposure => 0x04, Self::Brightness => 0x02, Self::Contrast => 0x03, Self::Saturation => 0x07, Self::Sharpness => 0x08, Self::WhiteBalance => 0x0A, Self::Tint => 0x06, }
}
fn len(self) -> usize {
match self {
Self::Exposure => 4,
_ => 2,
}
}
fn signed(self) -> bool {
matches!(self, Self::Brightness | Self::Tint)
}
}
impl AutoToggle {
fn unit(self) -> Unit {
match self {
Self::Focus | Self::Exposure => Unit::CameraTerminal,
Self::WhiteBalance => Unit::Processing,
}
}
fn selector(self) -> u16 {
match self {
Self::Focus => 0x08, Self::Exposure => 0x02, Self::WhiteBalance => 0x0B, }
}
}
const UVC_SET_CUR: u8 = 0x01;
const UVC_GET_CUR: u8 = 0x81;
const UVC_GET_MIN: u8 = 0x82;
const UVC_GET_MAX: u8 = 0x83;
const UVC_GET_DEF: u8 = 0x87;
const RT_GET: u8 = 0xA1; const RT_SET: u8 = 0x21;
const CC_VIDEO: u8 = 0x0E;
const SC_VIDEOCONTROL: u8 = 0x01;
const DESC_INTERFACE: u8 = 0x04;
const DESC_CS_INTERFACE: u8 = 0x24;
const VC_INPUT_TERMINAL: u8 = 0x02;
const VC_PROCESSING_UNIT: u8 = 0x05;
const ITT_CAMERA: u16 = 0x0201;
const AE_MANUAL: u8 = 0x01;
const AE_AUTO_MODES: [u8; 3] = [0x02, 0x08, 0x04];
const KIO_RETURN_SUCCESS: i32 = 0;
fn quiesce() -> std::sync::MutexGuard<'static, ()> {
crate::USB_QUIESCE
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
pub fn control_range(
unique_id: &str,
control: CameraControl,
) -> Result<ControlRange, ControlError> {
let _quiesce = quiesce();
let dev = UsbDevice::open_for(unique_id)?;
let min = dev.get(control, UVC_GET_MIN)?;
let max = dev.get(control, UVC_GET_MAX)?;
let default = dev.get(control, UVC_GET_DEF)?;
let current = dev.get(control, UVC_GET_CUR).unwrap_or(default);
Ok(ControlRange {
min,
max,
default,
current,
})
}
pub fn control_ranges(unique_id: &str) -> Result<Vec<(CameraControl, ControlRange)>, ControlError> {
Ok(read_camera_state(unique_id)?.controls)
}
pub fn read_camera_state(unique_id: &str) -> Result<CameraState, ControlError> {
let _quiesce = quiesce();
let dev = UsbDevice::open_for(unique_id)?;
let mut state = CameraState::default();
for control in CameraControl::ALL {
if let (Ok(min), Ok(max), Ok(default)) = (
dev.get(control, UVC_GET_MIN),
dev.get(control, UVC_GET_MAX),
dev.get(control, UVC_GET_DEF),
) {
let current = dev.get(control, UVC_GET_CUR).unwrap_or(default);
state.controls.push((
control,
ControlRange {
min,
max,
default,
current,
},
));
}
}
for toggle in AutoToggle::ALL {
if let (Ok(current), Ok(default)) = (
dev.get_auto(toggle, UVC_GET_CUR),
dev.get_auto(toggle, UVC_GET_DEF),
) {
state.autos.push((toggle, AutoState { current, default }));
}
}
Ok(state)
}
pub fn set_control(
unique_id: &str,
control: CameraControl,
value: i32,
) -> Result<(), ControlError> {
let _quiesce = quiesce();
let dev = UsbDevice::open_for(unique_id)?;
dev.set(control, value)
}
pub fn set_auto(unique_id: &str, toggle: AutoToggle, on: bool) -> Result<(), ControlError> {
let _quiesce = quiesce();
let dev = UsbDevice::open_for(unique_id)?;
dev.set_auto(toggle, on)
}
pub fn apply_settings(
unique_id: &str,
autos: &[(AutoToggle, bool)],
values: &[(CameraControl, i32)],
) -> Result<(), ControlError> {
let _quiesce = quiesce();
let dev = UsbDevice::open_for(unique_id)?;
let mut first_err = None;
for (toggle, on) in autos {
if let Err(e) = dev.set_auto(*toggle, *on) {
first_err.get_or_insert(e);
}
}
for (control, value) in values {
if let Err(e) = dev.set(*control, *value) {
first_err.get_or_insert(e);
}
}
first_err.map_or(Ok(()), Err)
}
pub(crate) fn location_hint(unique_id: &str) -> Option<u32> {
let hex = unique_id.strip_prefix("0x").unwrap_or(unique_id);
let location = hex.get(..hex.len().checked_sub(8)?)?;
if location.is_empty() {
return None;
}
u32::from_str_radix(location, 16).ok()
}
pub(crate) fn usb_serials_by_location() -> std::collections::HashMap<u32, String> {
use std::collections::HashMap;
let mut out = HashMap::new();
let Ok(class) = CString::new("IOUSBDevice") else {
return out;
};
unsafe {
let matching = IOServiceMatching(class.as_ptr());
if matching.is_null() {
return out;
}
let mut iter: IoIterator = 0;
if IOServiceGetMatchingServices(kIOMainPortDefault, matching, &raw mut iter)
!= KIO_RETURN_SUCCESS
{
return out;
}
let key = cf_string("USB Serial Number");
loop {
let service = IOIteratorNext(iter);
if service == 0 {
break;
}
if let Some((location, serial)) = registry_location_and_serial(service, key) {
out.entry(location).or_insert(serial);
}
IOObjectRelease(service);
}
if !key.is_null() {
CFRelease(key);
}
IOObjectRelease(iter);
}
out
}
unsafe fn registry_location_and_serial(
service: IoService,
serial_key: *const c_void,
) -> Option<(u32, String)> {
unsafe {
let location = device_location_id(service).or_else(|| {
let key = cf_string("locationID");
let loc = cf_number_u32(IORegistryEntryCreateCFProperty(
service,
key,
ptr::null(),
0,
));
if !key.is_null() {
CFRelease(key);
}
loc
})?;
let serial_ref = IORegistryEntryCreateCFProperty(service, serial_key, ptr::null(), 0);
let serial = cf_string_value(serial_ref)?;
if serial.is_empty() {
return None;
}
Some((location, serial))
}
}
unsafe fn device_location_id(service: IoService) -> Option<u32> {
unsafe {
let user_client = make_uuid(&KIO_USB_DEVICE_USER_CLIENT_TYPE_ID);
let plugin_type = make_uuid(&KIO_CF_PLUGIN_INTERFACE_ID);
let mut plugin: *mut *mut PlugInInterface = ptr::null_mut();
let mut score: i32 = 0;
let rc = IOCreatePlugInInterfaceForService(
service,
user_client,
plugin_type,
&raw mut plugin,
&raw mut score,
);
CFRelease(user_client);
CFRelease(plugin_type);
if rc != KIO_RETURN_SUCCESS || plugin.is_null() {
return None;
}
let dev_uuid_ref = make_uuid(&KIO_USB_DEVICE_INTERFACE_ID);
let dev_uuid = CFUUIDGetUUIDBytes(dev_uuid_ref);
CFRelease(dev_uuid_ref);
let mut dev_ptr: *mut c_void = ptr::null_mut();
let qrc = ((**plugin).query_interface)(plugin.cast::<c_void>(), dev_uuid, &raw mut dev_ptr);
IODestroyPlugInInterface(plugin);
if qrc != 0 || dev_ptr.is_null() {
return None;
}
let dev = dev_ptr.cast::<*mut UsbDeviceInterface>();
let mut location: u32 = 0;
let ok = ((**dev).get_location_id)(dev.cast::<c_void>(), &raw mut location)
== KIO_RETURN_SUCCESS;
((**dev).release)(dev.cast::<c_void>());
ok.then_some(location)
}
}
fn cf_string(s: &str) -> *const c_void {
let Ok(c) = CString::new(s) else {
return ptr::null();
};
unsafe { CFStringCreateWithCString(ptr::null(), c.as_ptr(), K_CF_STRING_ENCODING_UTF8) }
}
unsafe fn cf_string_value(cf: *const c_void) -> Option<String> {
if cf.is_null() {
return None;
}
unsafe {
if CFGetTypeID(cf) != CFStringGetTypeID() {
CFRelease(cf);
return None;
}
let len = CFStringGetLength(cf);
let cap = usize::try_from(len).ok()?.checked_mul(4)?.checked_add(1)?;
let mut buf = vec![0u8; cap];
let ok = CFStringGetCString(
cf,
buf.as_mut_ptr().cast(),
isize::try_from(cap).ok()?,
K_CF_STRING_ENCODING_UTF8,
) != 0;
CFRelease(cf);
if !ok {
return None;
}
let nul = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
String::from_utf8(buf[..nul].to_vec()).ok()
}
}
unsafe fn cf_number_u32(cf: *const c_void) -> Option<u32> {
if cf.is_null() {
return None;
}
unsafe {
if CFGetTypeID(cf) != CFNumberGetTypeID() {
CFRelease(cf);
return None;
}
let mut value: u32 = 0;
let ok = CFNumberGetValue(cf, K_CF_NUMBER_SINT32_TYPE, (&raw mut value).cast()) != 0;
CFRelease(cf);
ok.then_some(value)
}
}
type IoReturn = i32;
type IoService = u32;
type IoIterator = u32;
type CfUuidRef = *const c_void;
#[repr(C)]
#[derive(Clone, Copy)]
struct CfUuidBytes {
bytes: [u8; 16],
}
#[repr(C)]
struct IoUsbDevRequest {
bm_request_type: u8,
b_request: u8,
w_value: u16,
w_index: u16,
w_length: u16,
p_data: *mut c_void,
w_len_done: u32,
}
#[repr(C)]
struct PlugInInterface {
_reserved: *mut c_void,
query_interface: extern "C" fn(*mut c_void, CfUuidBytes, *mut *mut c_void) -> i32,
add_ref: extern "C" fn(*mut c_void) -> u32,
release: extern "C" fn(*mut c_void) -> u32,
}
#[repr(C)]
struct UsbDeviceInterface {
_reserved: *mut c_void,
query_interface: extern "C" fn(*mut c_void, CfUuidBytes, *mut *mut c_void) -> i32,
add_ref: extern "C" fn(*mut c_void) -> u32,
release: extern "C" fn(*mut c_void) -> u32,
create_device_async_event_source: *const c_void,
get_device_async_event_source: *const c_void,
create_device_async_port: *const c_void,
get_device_async_port: *const c_void,
usb_device_open: extern "C" fn(*mut c_void) -> IoReturn,
usb_device_close: extern "C" fn(*mut c_void) -> IoReturn,
get_device_class: *const c_void,
get_device_sub_class: *const c_void,
get_device_protocol: *const c_void,
get_device_vendor: extern "C" fn(*mut c_void, *mut u16) -> IoReturn,
get_device_product: extern "C" fn(*mut c_void, *mut u16) -> IoReturn,
get_device_release_number: *const c_void,
get_device_address: *const c_void,
get_device_bus_power_available: *const c_void,
get_device_speed: *const c_void,
get_number_of_configurations: extern "C" fn(*mut c_void, *mut u8) -> IoReturn,
get_location_id: extern "C" fn(*mut c_void, *mut u32) -> IoReturn,
get_configuration_descriptor_ptr: extern "C" fn(*mut c_void, u8, *mut *const u8) -> IoReturn,
get_configuration: *const c_void,
set_configuration: *const c_void,
get_bus_frame_number: *const c_void,
reset_device: *const c_void,
device_request: extern "C" fn(*mut c_void, *mut IoUsbDevRequest) -> IoReturn,
device_request_async: *const c_void,
create_interface_iterator: *const c_void,
usb_device_open_seize: extern "C" fn(*mut c_void) -> IoReturn,
}
#[link(name = "IOKit", kind = "framework")]
unsafe extern "C" {
static kIOMainPortDefault: u32;
fn IOServiceMatching(name: *const i8) -> *mut c_void;
fn IOServiceGetMatchingServices(
main_port: u32,
matching: *mut c_void,
existing: *mut IoIterator,
) -> IoReturn;
fn IOIteratorNext(iterator: IoIterator) -> IoService;
fn IOObjectRelease(object: u32) -> IoReturn;
fn IOCreatePlugInInterfaceForService(
service: IoService,
plugin_type: CfUuidRef,
interface_type: CfUuidRef,
plug_in: *mut *mut *mut PlugInInterface,
score: *mut i32,
) -> IoReturn;
fn IODestroyPlugInInterface(interface: *mut *mut PlugInInterface) -> IoReturn;
fn IORegistryEntryCreateCFProperty(
entry: IoService,
key: *const c_void,
allocator: *const c_void,
options: u32,
) -> *const c_void;
}
#[link(name = "CoreFoundation", kind = "framework")]
unsafe extern "C" {
fn CFUUIDCreateFromUUIDBytes(allocator: *const c_void, bytes: CfUuidBytes) -> CfUuidRef;
fn CFUUIDGetUUIDBytes(uuid: CfUuidRef) -> CfUuidBytes;
fn CFRelease(cf: *const c_void);
fn CFStringCreateWithCString(
allocator: *const c_void,
c_str: *const i8,
encoding: u32,
) -> *const c_void;
fn CFStringGetTypeID() -> usize;
fn CFNumberGetTypeID() -> usize;
fn CFGetTypeID(cf: *const c_void) -> usize;
fn CFStringGetLength(s: *const c_void) -> isize;
fn CFStringGetCString(s: *const c_void, buf: *mut i8, buffer_size: isize, encoding: u32) -> u8;
fn CFNumberGetValue(number: *const c_void, the_type: i32, value_ptr: *mut c_void) -> u8;
}
const K_CF_STRING_ENCODING_UTF8: u32 = 0x0800_0100;
const K_CF_NUMBER_SINT32_TYPE: i32 = 3;
const KIO_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 KIO_CF_PLUGIN_INTERFACE_ID: [u8; 16] = [
0xc2, 0x44, 0xe8, 0x58, 0x10, 0x9c, 0x11, 0xd4, 0x91, 0xd4, 0x00, 0x50, 0xe4, 0xc6, 0x42, 0x6f,
];
const KIO_USB_DEVICE_INTERFACE_ID: [u8; 16] = [
0x15, 0x2f, 0xc4, 0x96, 0x48, 0x91, 0x11, 0xd5, 0x9d, 0x52, 0x00, 0x0a, 0x27, 0x80, 0x1e, 0x86,
];
struct UsbDevice {
dev: *mut *mut UsbDeviceInterface,
vc_interface: u8,
unit_id: u8,
terminal_id: Option<u8>,
}
impl UsbDevice {
fn open_for(unique_id: &str) -> Result<Self, ControlError> {
let want_vid = crate::LOGITECH_VID;
let want_location = location_hint(unique_id);
unsafe {
let class = CString::new("IOUSBDevice").map_err(|e| ControlError::Io(e.to_string()))?;
let matching = IOServiceMatching(class.as_ptr());
if matching.is_null() {
return Err(ControlError::Io("IOServiceMatching".into()));
}
let mut iter: IoIterator = 0;
if IOServiceGetMatchingServices(kIOMainPortDefault, matching, &raw mut iter)
!= KIO_RETURN_SUCCESS
{
return Err(ControlError::Io("IOServiceGetMatchingServices".into()));
}
let mut chosen: Option<Opened> = None;
let mut vendor_candidates = 0usize;
loop {
let service = IOIteratorNext(iter);
if service == 0 {
break;
}
match Self::try_open(service, want_vid, want_location) {
Some(found) => {
let exact =
want_location.is_some_and(|l| found.matched_location == Some(l));
IOObjectRelease(service);
if exact {
if let Some(prev) = chosen.take() {
prev.into_device().close();
}
chosen = Some(found);
break;
} else if want_location.is_none() {
vendor_candidates += 1;
if chosen.is_none() {
chosen = Some(found);
} else {
found.into_device().close();
}
} else {
found.into_device().close();
}
}
None => {
IOObjectRelease(service);
}
}
}
IOObjectRelease(iter);
if want_location.is_none() && vendor_candidates > 1 {
if let Some(dev) = chosen.take() {
dev.into_device().close();
}
return Err(ControlError::Ambiguous);
}
chosen
.map(Opened::into_device)
.ok_or(ControlError::NotFound)
}
}
fn entity(&self, unit: Unit) -> Result<u8, ControlError> {
match unit {
Unit::Processing => Ok(self.unit_id),
Unit::CameraTerminal => self.terminal_id.ok_or(ControlError::Unsupported),
}
}
fn get(&self, control: CameraControl, req: u8) -> Result<i32, ControlError> {
let entity = self.entity(control.unit())?;
let mut buf = [0u8; 4];
let len = control.len();
self.transfer(RT_GET, req, control.selector(), entity, &mut buf[..len])?;
Ok(match (len, control.signed()) {
(4, _) => i32::try_from(u32::from_le_bytes(buf)).unwrap_or(i32::MAX),
(_, true) => i32::from(i16::from_le_bytes([buf[0], buf[1]])),
(_, false) => i32::from(u16::from_le_bytes([buf[0], buf[1]])),
})
}
fn set(&self, control: CameraControl, value: i32) -> Result<(), ControlError> {
let entity = self.entity(control.unit())?;
let mut buf = (value as u32).to_le_bytes();
let len = control.len();
self.transfer(
RT_SET,
UVC_SET_CUR,
control.selector(),
entity,
&mut buf[..len],
)
}
fn get_auto(&self, toggle: AutoToggle, req: u8) -> Result<bool, ControlError> {
let entity = self.entity(toggle.unit())?;
let mut buf = [0u8; 1];
self.transfer(RT_GET, req, toggle.selector(), entity, &mut buf)?;
Ok(match toggle {
AutoToggle::Exposure => buf[0] != AE_MANUAL,
_ => buf[0] != 0,
})
}
fn set_auto(&self, toggle: AutoToggle, on: bool) -> Result<(), ControlError> {
let entity = self.entity(toggle.unit())?;
let selector = toggle.selector();
let candidates: &[u8] = match (toggle, on) {
(AutoToggle::Exposure, true) => &AE_AUTO_MODES,
(AutoToggle::Exposure, false) => &[AE_MANUAL],
(_, true) => &[1],
(_, false) => &[0],
};
let mut last = ControlError::Unsupported;
for &mode in candidates {
match self.transfer(RT_SET, UVC_SET_CUR, selector, entity, &mut [mode]) {
Ok(()) => return Ok(()),
Err(e) => last = e,
}
}
Err(last)
}
fn transfer(
&self,
request_type: u8,
request: u8,
selector: u16,
entity: u8,
data: &mut [u8],
) -> Result<(), ControlError> {
let mut req = IoUsbDevRequest {
bm_request_type: request_type,
b_request: request,
w_value: selector << 8,
w_index: (u16::from(entity) << 8) | u16::from(self.vc_interface),
w_length: data.len() as u16,
p_data: data.as_mut_ptr().cast::<c_void>(),
w_len_done: 0,
};
let rc = unsafe { ((**self.dev).device_request)(self.dev.cast::<c_void>(), &raw mut req) };
if rc != KIO_RETURN_SUCCESS {
return Err(ControlError::Unsupported);
}
Ok(())
}
fn close(self) {
drop(self);
}
}
impl Drop for UsbDevice {
fn drop(&mut self) {
unsafe {
let _ = ((**self.dev).usb_device_close)(self.dev.cast::<c_void>());
((**self.dev).release)(self.dev.cast::<c_void>());
}
}
}
struct Opened {
device: UsbDevice,
matched_location: Option<u32>,
}
impl Opened {
fn into_device(self) -> UsbDevice {
self.device
}
}
impl UsbDevice {
unsafe fn try_open(
service: IoService,
want_vid: u16,
_want_location: Option<u32>,
) -> Option<Opened> {
unsafe {
let user_client = make_uuid(&KIO_USB_DEVICE_USER_CLIENT_TYPE_ID);
let plugin_type = make_uuid(&KIO_CF_PLUGIN_INTERFACE_ID);
let mut plugin: *mut *mut PlugInInterface = ptr::null_mut();
let mut score: i32 = 0;
let rc = IOCreatePlugInInterfaceForService(
service,
user_client,
plugin_type,
&raw mut plugin,
&raw mut score,
);
CFRelease(user_client);
CFRelease(plugin_type);
if rc != KIO_RETURN_SUCCESS || plugin.is_null() {
return None;
}
let dev_uuid_ref = make_uuid(&KIO_USB_DEVICE_INTERFACE_ID);
let dev_uuid = CFUUIDGetUUIDBytes(dev_uuid_ref);
CFRelease(dev_uuid_ref);
let mut dev_ptr: *mut c_void = ptr::null_mut();
let qrc =
((**plugin).query_interface)(plugin.cast::<c_void>(), dev_uuid, &raw mut dev_ptr);
IODestroyPlugInInterface(plugin);
if qrc != 0 || dev_ptr.is_null() {
return None;
}
let dev = dev_ptr.cast::<*mut UsbDeviceInterface>();
let mut vid: u16 = 0;
((**dev).get_device_vendor)(dev.cast::<c_void>(), &raw mut vid);
if vid != want_vid {
((**dev).release)(dev.cast::<c_void>());
return None;
}
let mut location: u32 = 0;
let loc_ok = ((**dev).get_location_id)(dev.cast::<c_void>(), &raw mut location)
== KIO_RETURN_SUCCESS;
if ((**dev).usb_device_open_seize)(dev.cast::<c_void>()) != KIO_RETURN_SUCCESS {
((**dev).release)(dev.cast::<c_void>());
return None;
}
let Some(topology) = video_control_topology(dev) else {
let _ = ((**dev).usb_device_close)(dev.cast::<c_void>());
((**dev).release)(dev.cast::<c_void>());
return None;
};
Some(Opened {
device: UsbDevice {
dev,
vc_interface: topology.vc_interface,
unit_id: topology.processing_unit,
terminal_id: topology.camera_terminal,
},
matched_location: loc_ok.then_some(location),
})
}
}
}
struct VcTopology {
vc_interface: u8,
processing_unit: u8,
camera_terminal: Option<u8>,
}
unsafe fn video_control_topology(dev: *mut *mut UsbDeviceInterface) -> Option<VcTopology> {
unsafe {
let mut num_configs: u8 = 0;
((**dev).get_number_of_configurations)(dev.cast::<c_void>(), &raw mut num_configs);
for cfg in 0..num_configs {
let mut desc: *const u8 = ptr::null();
if ((**dev).get_configuration_descriptor_ptr)(dev.cast::<c_void>(), cfg, &raw mut desc)
!= KIO_RETURN_SUCCESS
|| desc.is_null()
{
continue;
}
let total = u16::from(*desc.add(2)) | (u16::from(*desc.add(3)) << 8);
if let Some(found) = scan_descriptors(desc, total as usize) {
return Some(found);
}
}
None
}
}
unsafe fn scan_descriptors(base: *const u8, total: usize) -> Option<VcTopology> {
unsafe {
let mut off = 0usize;
let mut vc_interface: Option<u8> = None;
let mut camera_terminal: Option<u8> = None;
while off + 2 <= total {
let len = *base.add(off) as usize;
if len < 2 || off + len > total {
break;
}
let dtype = *base.add(off + 1);
if dtype == DESC_INTERFACE && len >= 9 {
let class = *base.add(off + 5);
let sub = *base.add(off + 6);
vc_interface = (class == CC_VIDEO && sub == SC_VIDEOCONTROL)
.then(|| *base.add(off + 2))
.or(vc_interface);
} else if dtype == DESC_CS_INTERFACE && len >= 4 && vc_interface.is_some() {
let subtype = *base.add(off + 2);
if subtype == VC_INPUT_TERMINAL && len >= 8 {
let ttype =
u16::from(*base.add(off + 4)) | (u16::from(*base.add(off + 5)) << 8);
if ttype == ITT_CAMERA && camera_terminal.is_none() {
camera_terminal = Some(*base.add(off + 3));
}
} else if subtype == VC_PROCESSING_UNIT {
return vc_interface.map(|vc| VcTopology {
vc_interface: vc,
processing_unit: *base.add(off + 3),
camera_terminal,
});
}
}
off += len;
}
None
}
}
unsafe fn make_uuid(bytes: &[u8; 16]) -> CfUuidRef {
unsafe { CFUUIDCreateFromUUIDBytes(ptr::null(), CfUuidBytes { bytes: *bytes }) }
}
#[cfg(test)]
mod tests {
use super::location_hint;
#[test]
fn unpadded_location_parses() {
assert_eq!(location_hint("0x1123000046d0893"), Some(0x0112_3000));
}
#[test]
fn padded_location_parses() {
assert_eq!(location_hint("0x14110000046d082d"), Some(0x1411_0000));
}
#[test]
fn too_short_ids_yield_no_hint() {
assert_eq!(location_hint("0x46d0893"), None);
assert_eq!(location_hint("46d0893"), None);
assert_eq!(location_hint(""), None);
}
}