wdi_rs/
wdi.rs

1// Copyright (C) 2025 Piers Finlayson <piers@piers.rocks>
2//
3// MIT License
4
5//! Exposes a safe Rust API around libwdi's APIs
6
7use crate::ffi::{WdiDeviceInfo, WdiLogLevel, WdiOptionsCreateList, WdiOptionsPrepareDriver, WdiOptionsInstallDriver};
8use crate::ffi::{wdi_create_list, wdi_destroy_list, wdi_prepare_driver, wdi_install_driver, wdi_set_log_level};
9use std::ffi::{CStr, CString};
10use std::fmt;
11use std::os::raw::c_int;
12use std::ptr;
13
14/// Log level for libwdi logging.  Note that libwdi is quite chatty, so the levels are shifted
15/// down by one when mapping the standard Rust log levels.
16pub enum LogLevel {
17    Debug,
18    Info,
19    Warning,
20    Error,
21    None,
22}
23
24impl From<log::LevelFilter> for LogLevel {
25    fn from(level: log::LevelFilter) -> Self {
26        // Shift off by 1 - libwdi is a bit chatty
27        match level {
28            log::LevelFilter::Trace => LogLevel::Debug,
29            log::LevelFilter::Debug => LogLevel::Info,
30            log::LevelFilter::Info => LogLevel::Warning,
31            log::LevelFilter::Warn => LogLevel::Error,
32            log::LevelFilter::Error => LogLevel::Error,
33            log::LevelFilter::Off => LogLevel::None,
34        }
35    }
36}
37
38impl From<log::Level> for LogLevel {
39    fn from(level: log::Level) -> Self {
40        // Shift off by 1 - libwdi is a bit chatty
41        match level {
42            log::Level::Trace => LogLevel::Debug,
43            log::Level::Debug => LogLevel::Info,
44            log::Level::Info => LogLevel::Warning,
45            log::Level::Warn => LogLevel::Error,
46            log::Level::Error => LogLevel::Error,
47        }
48    }
49}
50
51impl From<LogLevel> for c_int {
52    fn from(level: LogLevel) -> Self {
53        match level {
54            LogLevel::Debug => 0,
55            LogLevel::Info => 1,
56            LogLevel::Warning => 2,
57            LogLevel::Error => 3,
58            LogLevel::None => 4,
59        }
60    }
61}
62
63impl From<WdiLogLevel> for LogLevel {
64    fn from(level: WdiLogLevel) -> Self {
65        match level {
66            WdiLogLevel::Debug => LogLevel::Debug,
67            WdiLogLevel::Info => LogLevel::Info,
68            WdiLogLevel::Warning => LogLevel::Warning,
69            WdiLogLevel::Error => LogLevel::Error,
70            WdiLogLevel::None => LogLevel::None,
71        }
72    }
73}
74
75/// Error codes returned by libwdi
76#[derive(Debug)]
77pub enum Error {
78    Io,
79    InvalidParam,
80    Access,
81    NoDevice,
82    NotFound,
83    Busy,
84    Timeout,
85    Overflow,
86    PendingInstallation,
87    Interrupted,
88    Resource,
89    NotSupported,
90    Exists,
91    UserCancel,
92    NeedsAdmin,
93    Wow64,
94    InfSyntax,
95    CatMissing,
96    Unsigned,
97    Other,
98    Unknown(c_int),
99}
100
101impl Error {
102    fn from_code(code: c_int) -> Result<(), Self> {
103        match code {
104            0 => Ok(()),
105            -1 => Err(Error::Io),
106            -2 => Err(Error::InvalidParam),
107            -3 => Err(Error::Access),
108            -4 => Err(Error::NoDevice),
109            -5 => Err(Error::NotFound),
110            -6 => Err(Error::Busy),
111            -7 => Err(Error::Timeout),
112            -8 => Err(Error::Overflow),
113            -9 => Err(Error::PendingInstallation),
114            -10 => Err(Error::Interrupted),
115            -11 => Err(Error::Resource),
116            -12 => Err(Error::NotSupported),
117            -13 => Err(Error::Exists),
118            -14 => Err(Error::UserCancel),
119            -15 => Err(Error::NeedsAdmin),
120            -16 => Err(Error::Wow64),
121            -17 => Err(Error::InfSyntax),
122            -18 => Err(Error::CatMissing),
123            -19 => Err(Error::Unsigned),
124            -99 => Err(Error::Other),
125            code => Err(Error::Unknown(code)),
126        }
127    }
128}
129
130impl fmt::Display for Error {
131    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
132        write!(f, "{:?}", self)
133    }
134}
135
136impl std::error::Error for Error {}
137
138/// Driver types supported by libwdi
139#[derive(Debug, Clone, Copy)]
140pub enum DriverType {
141    WinUsb,
142    LibUsb0,
143    LibUsbK,
144    Cdc,
145    User,
146}
147
148impl DriverType {
149    fn to_c_int(self) -> c_int {
150        match self {
151            DriverType::WinUsb => 0,
152            DriverType::LibUsb0 => 1,
153            DriverType::LibUsbK => 2,
154            DriverType::Cdc => 3,
155            DriverType::User => 4,
156        }
157    }
158}
159
160/// Represents a connected device.  The fields correspond to those returned by libwdi
161#[derive(Debug, Clone)]
162pub struct Device {
163    pub vid: u16,
164    pub pid: u16,
165    pub is_composite: bool,
166    pub mi: u8,
167    pub desc: Option<String>,
168    pub driver: Option<String>,
169    pub device_id: Option<String>,
170    pub hardware_id: Option<String>,
171    pub compatible_id: Option<String>,
172    pub upper_filter: Option<String>,
173    pub driver_version: u64,
174}
175
176impl Device {
177    unsafe fn from_raw(raw: *const WdiDeviceInfo) -> Self {
178        let raw = unsafe { &*raw };
179        Device {
180            vid: raw.vid,
181            pid: raw.pid,
182            is_composite: raw.is_composite != 0,
183            mi: raw.mi,
184            desc: unsafe{ ptr_to_string(raw.desc) },
185            driver: unsafe{ ptr_to_string(raw.driver) },
186            device_id: unsafe{ ptr_to_string(raw.device_id) },
187            hardware_id: unsafe{ ptr_to_string(raw.hardware_id) },
188            compatible_id: unsafe{ ptr_to_string(raw.compatible_id) },
189            upper_filter: unsafe{ ptr_to_string(raw.upper_filter) },
190            driver_version: raw.driver_version,
191        }
192    }
193}
194
195impl std::fmt::Display for Device {
196    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
197        write!(
198            f,
199            "{:04X}:{:04X} {}",
200            self.vid,
201            self.pid,
202            self.desc.as_deref().unwrap_or("(no description)")
203        )
204    }
205}
206
207unsafe fn ptr_to_string(ptr: *mut i8) -> Option<String> {
208    if ptr.is_null() {
209        None
210    } else {
211        unsafe { CStr::from_ptr(ptr).to_str().ok().map(|s| s.to_owned()) }
212    }
213}
214
215/// Represents a list of connected devices
216/// 
217/// Use the [`iter`](DeviceList::iter) method to iterate over the devices
218#[derive(Debug)]
219pub struct DeviceList {
220    head: *mut WdiDeviceInfo,
221}
222
223impl DeviceList {
224    /// Returns an iterator over the devices in the list
225    pub fn iter(&self) -> DeviceIter {
226        DeviceIter {
227            current: self.head,
228        }
229    }
230
231    /// Gets the number of devices in the list
232    pub fn len(&self) -> usize {
233        self.iter().count()
234    }
235
236    /// Checks if the device list is empty
237    pub fn is_empty(&self) -> bool {
238        self.head.is_null()
239    }
240
241    /// Gets the device at the specified index, if it exists
242    pub fn get(&self, index: usize) -> Option<Device> {
243        self.iter().nth(index)
244    }
245
246    /// Filters the device list by VID and PID, returning a vector of matching [`Device`]s
247    pub fn from_vid_pid(&self, vid: u16, pid: u16) -> Vec<Device> {
248        self.iter()
249            .filter(|d| d.vid == vid && d.pid == pid)
250            .collect()
251    }
252}
253
254impl Drop for DeviceList {
255    fn drop(&mut self) {
256        if !self.head.is_null() {
257            unsafe {
258                wdi_destroy_list(self.head);
259            }
260        }
261    }
262}
263
264/// Iterator over the devices in a [`DeviceList`]
265pub struct DeviceIter {
266    current: *mut WdiDeviceInfo,
267}
268
269impl Iterator for DeviceIter {
270    type Item = Device;
271
272    fn next(&mut self) -> Option<Self::Item> {
273        if self.current.is_null() {
274            None
275        } else {
276            unsafe {
277                let device = Device::from_raw(self.current);
278                self.current = (*self.current).next;
279                Some(device)
280            }
281        }
282    }
283}
284
285/// Options for creating a device list, as exposed by libwdi
286#[derive(Debug, Clone)]
287pub struct CreateListOptions {
288    pub list_all: bool,
289    pub list_hubs: bool,
290    pub trim_whitespaces: bool,
291}
292
293impl Default for CreateListOptions {
294    fn default() -> Self {
295        CreateListOptions {
296            list_all: false,
297            list_hubs: false,
298            trim_whitespaces: true,
299        }
300    }
301}
302
303/// Enumerates connected devices and returns a [`DeviceList`]
304/// 
305/// # Arguments
306/// * `options` - The options to use when creating the device list.
307pub fn create_list(options: CreateListOptions) -> Result<DeviceList, Error> {
308    let mut opts = WdiOptionsCreateList {
309        list_all: options.list_all as c_int,
310        list_hubs: options.list_hubs as c_int,
311        trim_whitespaces: options.trim_whitespaces as c_int,
312    };
313
314    let mut list: *mut WdiDeviceInfo = ptr::null_mut();
315    
316    unsafe {
317        let result = wdi_create_list(&mut list, &mut opts);
318        Error::from_code(result)?;
319    }
320
321    Ok(DeviceList { head: list })
322}
323
324/// Options for preparing a driver, as exposed by libwdi
325/// 
326/// You can use `default()` to construct.
327#[derive(Debug, Clone)]
328pub struct PrepareDriverOptions {
329    pub driver_type: DriverType,
330    pub vendor_name: Option<String>,
331    pub device_guid: Option<String>,
332    pub disable_cat: bool,
333    pub disable_signing: bool,
334    pub cert_subject: Option<String>,
335    pub use_wcid_driver: bool,
336
337    /// Indicates whether to use an external INF file.  Note that `wdi-rs` overrides this
338    /// when using a custom INF file via the higher level APIs.
339    pub external_inf: bool,
340}
341
342impl Default for PrepareDriverOptions {
343    fn default() -> Self {
344        PrepareDriverOptions {
345            driver_type: DriverType::WinUsb,
346            vendor_name: None,
347            device_guid: None,
348            disable_cat: false,
349            disable_signing: false,
350            cert_subject: None,
351            use_wcid_driver: false,
352            external_inf: false,
353        }
354    }
355}
356
357/// Prepares a driver for installation using libwdi
358/// 
359/// # Arguments
360/// * `device` - The device for which to prepare the driver.
361/// * `path` - The path where the driver files will be created.
362/// * `inf_name` - The name of the INF file to create, or use, if using an existing one.
363/// * `options` - The options to use when preparing the driver.
364/// 
365/// # Errors
366/// * Returns an `Error` if the preparation fails.
367pub fn prepare_driver(
368    device: &Device,
369    path: &str,
370    inf_name: &str,
371    options: &PrepareDriverOptions,
372) -> Result<(), Error> {
373    let path_c = CString::new(path).map_err(|_| Error::InvalidParam)?;
374    let inf_name_c = CString::new(inf_name).map_err(|_| Error::InvalidParam)?;
375    
376    // Convert device strings to CString - keep them alive for the C call
377    let desc_c = device.desc.as_ref()
378        .and_then(|s| CString::new(s.as_str()).ok());
379    let driver_c = device.driver.as_ref()
380        .and_then(|s| CString::new(s.as_str()).ok());
381    let device_id_c = device.device_id.as_ref()
382        .and_then(|s| CString::new(s.as_str()).ok());
383    let hardware_id_c = device.hardware_id.as_ref()
384        .and_then(|s| CString::new(s.as_str()).ok());
385    let compatible_id_c = device.compatible_id.as_ref()
386        .and_then(|s| CString::new(s.as_str()).ok());
387    let upper_filter_c = device.upper_filter.as_ref()
388        .and_then(|s| CString::new(s.as_str()).ok());
389    
390    let vendor_name_c = options.vendor_name.as_ref()
391        .and_then(|s| CString::new(s.as_str()).ok());
392    let device_guid_c = options.device_guid.as_ref()
393        .and_then(|s| CString::new(s.as_str()).ok());
394    let cert_subject_c = options.cert_subject.as_ref()
395        .and_then(|s| CString::new(s.as_str()).ok());
396
397    let mut device_info = WdiDeviceInfo {
398        next: ptr::null_mut(),
399        vid: device.vid,
400        pid: device.pid,
401        is_composite: device.is_composite as c_int,
402        mi: device.mi,
403        desc: desc_c.as_ref().map_or(ptr::null_mut(), |c| c.as_ptr() as *mut i8),
404        driver: driver_c.as_ref().map_or(ptr::null_mut(), |c| c.as_ptr() as *mut i8),
405        device_id: device_id_c.as_ref().map_or(ptr::null_mut(), |c| c.as_ptr() as *mut i8),
406        hardware_id: hardware_id_c.as_ref().map_or(ptr::null_mut(), |c| c.as_ptr() as *mut i8),
407        compatible_id: compatible_id_c.as_ref().map_or(ptr::null_mut(), |c| c.as_ptr() as *mut i8),
408        upper_filter: upper_filter_c.as_ref().map_or(ptr::null_mut(), |c| c.as_ptr() as *mut i8),
409        driver_version: device.driver_version,
410    };
411
412    let mut opts = WdiOptionsPrepareDriver {
413        driver_type: options.driver_type.to_c_int(),
414        vendor_name: vendor_name_c.as_ref().map_or(ptr::null_mut(), |c| c.as_ptr() as *mut i8),
415        device_guid: device_guid_c.as_ref().map_or(ptr::null_mut(), |c| c.as_ptr() as *mut i8),
416        disable_cat: options.disable_cat as c_int,
417        disable_signing: options.disable_signing as c_int,
418        cert_subject: cert_subject_c.as_ref().map_or(ptr::null_mut(), |c| c.as_ptr() as *mut i8),
419        use_wcid_driver: options.use_wcid_driver as c_int,
420        external_inf: options.external_inf as c_int,
421    };
422
423    unsafe {
424        let result = wdi_prepare_driver(
425            &mut device_info,
426            path_c.as_ptr(),
427            inf_name_c.as_ptr(),
428            &mut opts,
429        );
430        Error::from_code(result)?;
431    }
432
433    Ok(())
434}
435
436/// Options for installing a driver, as exposed by libwdi
437/// 
438/// You can use `default()` to construct.
439#[derive(Debug, Clone)]
440pub struct InstallDriverOptions {
441    pub install_filter_driver: bool,
442    /// Timeout in milliseconds to wait for pending installations.
443    /// Driver installation often takes around a minute to complete.
444    pub pending_install_timeout: u32,
445}
446
447impl InstallDriverOptions {
448    /// The default timeout for pending installations in milliseconds
449    pub const DEFAULT_PENDING_INSTALL_TIMEOUT: u32 = 120000;
450}
451
452impl Default for InstallDriverOptions {
453    fn default() -> Self {
454        InstallDriverOptions {
455            install_filter_driver: false,
456            pending_install_timeout: Self::DEFAULT_PENDING_INSTALL_TIMEOUT,
457        }
458    }
459}
460
461/// Installs a driver for a device using libwdi.
462/// 
463/// The driver files must already have been prepared using [`prepare_driver`],
464/// or created externally.
465/// 
466/// # Arguments
467/// * `device` - The device for which to install the driver.
468/// * `path` - The path where the driver files are located.
469/// * `inf_name` - The name of the INF file to use for installation.
470/// * `options` - The options to use when installing the driver.
471/// 
472/// # Errors
473/// * Returns an `Error` if the installation fails.
474pub fn install_driver(
475    device: &Device,
476    path: &str,
477    inf_name: &str,
478    options: &InstallDriverOptions,
479) -> Result<(), Error> {
480    let path_c = CString::new(path).map_err(|_| Error::InvalidParam)?;
481    let inf_name_c = CString::new(inf_name).map_err(|_| Error::InvalidParam)?;
482
483    // Convert device strings to CString
484    let desc_c = device.desc.as_ref()
485        .and_then(|s| CString::new(s.as_str()).ok());
486    let driver_c = device.driver.as_ref()
487        .and_then(|s| CString::new(s.as_str()).ok());
488    let device_id_c = device.device_id.as_ref()
489        .and_then(|s| CString::new(s.as_str()).ok());
490    let hardware_id_c = device.hardware_id.as_ref()
491        .and_then(|s| CString::new(s.as_str()).ok());
492    let compatible_id_c = device.compatible_id.as_ref()
493        .and_then(|s| CString::new(s.as_str()).ok());
494    let upper_filter_c = device.upper_filter.as_ref()
495        .and_then(|s| CString::new(s.as_str()).ok());
496
497    let mut device_info = WdiDeviceInfo {
498        next: ptr::null_mut(),
499        vid: device.vid,
500        pid: device.pid,
501        is_composite: device.is_composite as c_int,
502        mi: device.mi,
503        desc: desc_c.as_ref().map_or(ptr::null_mut(), |c| c.as_ptr() as *mut i8),
504        driver: driver_c.as_ref().map_or(ptr::null_mut(), |c| c.as_ptr() as *mut i8),
505        device_id: device_id_c.as_ref().map_or(ptr::null_mut(), |c| c.as_ptr() as *mut i8),
506        hardware_id: hardware_id_c.as_ref().map_or(ptr::null_mut(), |c| c.as_ptr() as *mut i8),
507        compatible_id: compatible_id_c.as_ref().map_or(ptr::null_mut(), |c| c.as_ptr() as *mut i8),
508        upper_filter: upper_filter_c.as_ref().map_or(ptr::null_mut(), |c| c.as_ptr() as *mut i8),
509        driver_version: device.driver_version,
510    };
511
512    let mut opts = WdiOptionsInstallDriver {
513        hwnd: ptr::null_mut(),
514        install_filter_driver: options.install_filter_driver as c_int,
515        pending_install_timeout: options.pending_install_timeout,
516    };
517
518    unsafe {
519        let result = wdi_install_driver(
520            &mut device_info,
521            path_c.as_ptr(),
522            inf_name_c.as_ptr(),
523            &mut opts,
524        );
525        Error::from_code(result)?;
526    }
527
528    Ok(())
529}
530
531/// Sets the log level for libwdi logging.
532pub fn set_log_level(level: LogLevel) -> Result<(), Error> {
533    unsafe {
534        let result = wdi_set_log_level(level.into());
535        Error::from_code(result)
536    }
537}