Skip to main content

usb_gadget/
lib.rs

1//! This library allows implementation of USB peripherals, so called **USB gadgets**,
2//! on Linux devices that have a USB device controller (UDC).
3//! Both, pre-defined USB functions and fully custom implementations of the USB
4//! interface are supported.
5//!
6//! The following pre-defined USB functions, implemented by kernel drivers, are supported:
7//!
8//! * [network interface]: CDC ECM, ECM subset, EEM, NCM, RNDIS
9//! * [serial port]: CDC ACM, generic
10//! * [human interface device (HID)]
11//! * [mass-storage device (MSD)]
12//! * [printer device]
13//! * [musical instrument digital interface (MIDI)]
14//! * [audio device]: UAC1 and UAC2
15//! * [video device] (UVC)
16//!
17//! In addition fully [custom USB functions] can be implemented in user-mode Rust code.
18//!
19//! [network interface]: function::net
20//! [serial port]: function::serial
21//! [human interface device (HID)]: function::hid
22//! [mass-storage device (MSD)]: function::msd
23//! [printer device]: function::printer
24//! [musical instrument digital interface (MIDI)]: function::midi
25//! [audio device]: function::audio
26//! [video device]: function::video
27//! [custom USB functions]: function::custom
28//!
29//! Support for OS-specific descriptors and WebUSB is also provided.
30//!
31//! ### Requirements
32//!
33//! A USB device controller (UDC) supported by Linux is required.
34//!
35//! The Linux kernel configuration options `CONFIG_USB_GADGET` and `CONFIG_USB_CONFIGFS`
36//! need to be enabled.
37//!
38//! root permissions are required to configure USB gadgets and
39//! the `configfs` filesystem needs to be mounted.
40//!
41//! Some settings are only supported by newer kernels.
42//! Optional settings are only written when set; if left at `None`, the kernel default is used.
43//! Setting an option that is unsupported by the running kernel makes registration
44//! fail with an error of kind [`ErrorKind::Unsupported`].
45//! The exceptions are OS descriptors and WebUSB, which are skipped with a warning.
46//!
47//! ### Usage
48//!
49//! Start defining a USB gadget by calling [`Gadget::new`].
50//! When the gadget is fully specified, call [`Gadget::bind`] to register it with
51//! a [USB device controller (UDC)](Udc).
52
53#![warn(missing_docs)]
54#![cfg_attr(docsrs, feature(doc_cfg))]
55
56#[cfg(not(target_os = "linux"))]
57compile_error!("usb_gadget only supports Linux");
58
59use proc_mounts::MountIter;
60use std::{
61    ffi::OsStr,
62    fs,
63    io::{Error, ErrorKind, Result},
64    os::unix::prelude::OsStrExt,
65    path::{Path, PathBuf},
66    process::Command,
67    sync::OnceLock,
68};
69
70pub mod function;
71
72mod ioctl;
73
74mod gadget;
75pub use gadget::*;
76
77mod udc;
78pub use udc::*;
79
80mod lang;
81pub use lang::*;
82
83/// USB speed.
84#[derive(
85    Default, Debug, strum::Display, strum::EnumString, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash,
86)]
87#[non_exhaustive]
88pub enum Speed {
89    /// Unknown speed.
90    #[default]
91    #[strum(serialize = "UNKNOWN")]
92    Unknown,
93    /// USB 1.0: 1.5 Mbit/s.
94    #[strum(serialize = "low-speed")]
95    LowSpeed,
96    /// USB 1.0: 12 Mbit/s.
97    #[strum(serialize = "full-speed")]
98    FullSpeed,
99    /// USB 2.0: 480 Mbit/s.
100    #[strum(serialize = "high-speed")]
101    HighSpeed,
102    /// USB 3.0: 5 Gbit/s.
103    #[strum(serialize = "super-speed")]
104    SuperSpeed,
105    /// USB 3.1: 10 Gbit/s.
106    #[strum(serialize = "super-speed-plus")]
107    SuperSpeedPlus,
108}
109
110/// 8-bit value to hexadecimal notation.
111fn hex_u8(value: u8) -> String {
112    format!("0x{:02x}", value)
113}
114
115/// 8-bit value to hexadecimal notation without 0x prefix.
116fn hex_u8_noprefix(value: u8) -> String {
117    format!("{:02x}", value)
118}
119
120/// 16-bit value to hexadecimal notation.
121fn hex_u16(value: u16) -> String {
122    format!("0x{:04x}", value)
123}
124
125/// Writes a configfs attribute.
126///
127/// Fails with [`ErrorKind::Unsupported`] if the attribute does not exist,
128/// i.e. it is not supported by the running kernel.
129fn write_attr(path: &Path, value: impl AsRef<[u8]>) -> Result<()> {
130    fs::write(path, value).map_err(|err| match path.try_exists() {
131        Ok(false) => Error::new(
132            ErrorKind::Unsupported,
133            format!("attribute {} is not supported by the kernel", path.display()),
134        ),
135        _ => err,
136    })
137}
138
139/// Returns where configfs is mounted.
140fn configfs_dir() -> Result<PathBuf> {
141    for mount in MountIter::new()? {
142        let Ok(mount) = mount else { continue };
143        if mount.fstype == "configfs" {
144            return Ok(mount.dest);
145        }
146    }
147
148    Err(Error::new(ErrorKind::NotFound, "configfs is not mounted"))
149}
150
151/// Trims an OsStr.
152fn trim_os_str(value: &OsStr) -> &OsStr {
153    let mut value = value.as_bytes();
154
155    while value.first() == Some(&b'\n') || value.first() == Some(&b' ') || value.first() == Some(&b'\0') {
156        value = &value[1..];
157    }
158
159    while value.last() == Some(&b'\n') || value.last() == Some(&b' ') || value.last() == Some(&b'\0') {
160        value = &value[..value.len() - 1];
161    }
162
163    OsStr::from_bytes(value)
164}
165
166/// Request a kernel module to be loaded.
167fn request_module(name: impl AsRef<OsStr>) -> Result<()> {
168    let mut res = Command::new("modprobe").arg("-q").arg(name.as_ref()).output();
169
170    match res {
171        Err(err) if err.kind() == ErrorKind::NotFound => {
172            res = Command::new("/sbin/modprobe").arg("-q").arg(name.as_ref()).output();
173        }
174        _ => (),
175    }
176
177    match res {
178        Ok(out) if out.status.success() => Ok(()),
179        Ok(_) => Err(Error::other("modprobe failed")),
180        Err(err) => Err(err),
181    }
182}
183
184/// Gets the Linux kernel version.
185fn linux_version() -> Option<(u16, u16)> {
186    static VERSION: OnceLock<Result<(u16, u16)>> = OnceLock::new();
187    let version = VERSION.get_or_init(|| {
188        let uts = rustix::system::uname();
189
190        let release =
191            uts.release().to_str().map_err(|_| Error::new(ErrorKind::InvalidData, "invalid release string"))?;
192
193        let parts: Vec<&str> = release.split('.').collect();
194        if parts.len() < 2 {
195            return Err(Error::new(ErrorKind::InvalidData, "invalid kernel version"));
196        }
197
198        let major = parts[0].parse().map_err(|e| Error::new(ErrorKind::InvalidData, e))?;
199        let minor = parts[1].parse().map_err(|e| Error::new(ErrorKind::InvalidData, e))?;
200
201        Ok((major, minor))
202    });
203
204    match version {
205        Ok(version) => Some(*version),
206        Err(err) => {
207            log::warn!("failed to obtain Linux version: {err}");
208            None
209        }
210    }
211}
212
213#[cfg(test)]
214mod test {
215    #[test]
216    fn linux_version() {
217        let (major, minor) = super::linux_version().expect("failed to get Linux version");
218        println!("Linux {major}.{minor}");
219    }
220}