Skip to main content

bluez_async/
modalias.rs

1use std::collections::HashMap;
2use std::convert::{TryFrom, TryInto};
3use std::fmt::{self, Display, Formatter};
4use std::str::FromStr;
5use thiserror::Error;
6
7/// An error parsing a [`Modalias`] from a string.
8#[derive(Clone, Debug, Error, Eq, PartialEq)]
9#[error("Error parsing modalias string {0:?}")]
10pub struct ParseModaliasError(String);
11
12/// The subtype of a modalias string.
13#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
14pub enum ModaliasSubtype {
15    Usb,
16    Bluetooth,
17}
18
19impl Display for ModaliasSubtype {
20    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
21        f.write_str(match self {
22            Self::Usb => "usb",
23            Self::Bluetooth => "bluetooth",
24        })
25    }
26}
27
28impl FromStr for ModaliasSubtype {
29    type Err = ();
30
31    fn from_str(s: &str) -> Result<Self, Self::Err> {
32        match s {
33            "usb" => Ok(Self::Usb),
34            "bluetooth" => Ok(Self::Bluetooth),
35            _ => Err(()),
36        }
37    }
38}
39
40/// A parsed modalias string.
41///
42/// The `usb` and `bluetooth` subtypes are accepted and retained alongside the
43/// numeric vendor, product, and device fields.
44#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
45pub struct Modalias {
46    pub subtype: ModaliasSubtype,
47    pub vendor_id: u16,
48    pub product_id: u16,
49    pub device_id: u16,
50}
51
52impl Display for Modalias {
53    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
54        write!(
55            f,
56            "{}:v{:04X}p{:04X}d{:04X}",
57            self.subtype, self.vendor_id, self.product_id, self.device_id
58        )
59    }
60}
61
62impl FromStr for Modalias {
63    type Err = ParseModaliasError;
64
65    fn from_str(s: &str) -> Result<Self, Self::Err> {
66        RawModalias::from_str(s)?
67            .try_into()
68            .map_err(|_| ParseModaliasError(s.to_owned()))
69    }
70}
71
72impl TryFrom<RawModalias> for Modalias {
73    type Error = ();
74
75    fn try_from(raw: RawModalias) -> Result<Self, Self::Error> {
76        Ok(Modalias {
77            subtype: raw.subtype.as_str().parse()?,
78            vendor_id: u16::from_str_radix(raw.values.get("v").ok_or(())?, 16).map_err(|_| ())?,
79            product_id: u16::from_str_radix(raw.values.get("p").ok_or(())?, 16).map_err(|_| ())?,
80            device_id: u16::from_str_radix(raw.values.get("d").ok_or(())?, 16).map_err(|_| ())?,
81        })
82    }
83}
84
85#[derive(Clone, Debug, Eq, PartialEq)]
86struct RawModalias {
87    pub subtype: String,
88    pub values: HashMap<String, String>,
89}
90
91impl FromStr for RawModalias {
92    type Err = ParseModaliasError;
93
94    fn from_str(s: &str) -> Result<RawModalias, Self::Err> {
95        if let Some((subtype, mut rest)) = s.split_once(':') {
96            let mut values = HashMap::new();
97            while !rest.is_empty() {
98                // Find the end of the next key, which must only consist of lowercase ASCII
99                // characters.
100                if let Some(key_end) = rest.find(|c: char| !c.is_ascii_lowercase()) {
101                    let key = rest[0..key_end].to_owned();
102                    rest = &rest[key_end..];
103                    if let Some(key_start) = rest.find(|c: char| c.is_ascii_lowercase()) {
104                        let value = rest[0..key_start].to_owned();
105                        values.insert(key, value);
106                        rest = &rest[key_start..];
107                    } else {
108                        // There are no more values, the rest is the key.
109                        values.insert(key, rest.to_owned());
110                        break;
111                    }
112                } else {
113                    // The rest of the string is a key, with no value.
114                    values.insert(rest.to_owned(), "".to_owned());
115                    break;
116                }
117            }
118
119            Ok(RawModalias {
120                subtype: subtype.to_owned(),
121                values,
122            })
123        } else {
124            Err(ParseModaliasError(s.to_owned()))
125        }
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn parse() {
135        assert_eq!(
136            Modalias::from_str("usb:v0000p0000d0000").unwrap(),
137            Modalias {
138                subtype: ModaliasSubtype::Usb,
139                vendor_id: 0,
140                product_id: 0,
141                device_id: 0
142            }
143        );
144        assert_eq!(
145            Modalias::from_str("usb:v1234p5678d90AB").unwrap(),
146            Modalias {
147                subtype: ModaliasSubtype::Usb,
148                vendor_id: 0x1234,
149                product_id: 0x5678,
150                device_id: 0x90AB
151            }
152        );
153    }
154
155    #[test]
156    fn parse_bluetooth_modalias() {
157        assert_eq!(
158            Modalias::from_str("bluetooth:vABCDp0123d4567").unwrap(),
159            Modalias {
160                subtype: ModaliasSubtype::Bluetooth,
161                vendor_id: 0xABCD,
162                product_id: 0x0123,
163                device_id: 0x4567,
164            }
165        );
166    }
167
168    #[test]
169    fn bluetooth_modalias_round_trips() {
170        let modalias = Modalias::from_str("bluetooth:vABCDp0123d4567").unwrap();
171        assert_eq!(modalias.to_string(), "bluetooth:vABCDp0123d4567");
172    }
173
174    #[test]
175    fn parse_invalid_subtype() {
176        assert!(matches!(
177            Modalias::from_str("blah:v0000p0000d0000"),
178            Err(ParseModaliasError(_))
179        ));
180    }
181
182    #[test]
183    fn parse_missing_fields() {
184        assert!(matches!(
185            Modalias::from_str("usb:"),
186            Err(ParseModaliasError(_))
187        ));
188        assert!(matches!(
189            Modalias::from_str("usb:v1234p5678"),
190            Err(ParseModaliasError(_))
191        ));
192    }
193
194    #[test]
195    fn to_string() {
196        assert_eq!(
197            Modalias {
198                subtype: ModaliasSubtype::Usb,
199                vendor_id: 0,
200                product_id: 0,
201                device_id: 0
202            }
203            .to_string(),
204            "usb:v0000p0000d0000"
205        );
206        assert_eq!(
207            Modalias {
208                subtype: ModaliasSubtype::Usb,
209                vendor_id: 0x1234,
210                product_id: 0x5678,
211                device_id: 0x90AB
212            }
213            .to_string(),
214            "usb:v1234p5678d90AB"
215        );
216    }
217
218    #[test]
219    fn parse_raw_empty() {
220        assert!(matches!(
221            RawModalias::from_str(""),
222            Err(ParseModaliasError(_))
223        ));
224    }
225
226    #[test]
227    fn parse_raw_empty_usb() {
228        assert_eq!(
229            RawModalias::from_str("usb:").unwrap(),
230            RawModalias {
231                subtype: "usb".to_string(),
232                values: HashMap::new()
233            }
234        );
235    }
236
237    #[test]
238    fn parse_raw_success() {
239        let mut values = HashMap::new();
240        values.insert("a".to_string(), "AB12".to_string());
241        values.insert("ab".to_string(), "01".to_string());
242        assert_eq!(
243            RawModalias::from_str("usb:aAB12ab01").unwrap(),
244            RawModalias {
245                subtype: "usb".to_string(),
246                values
247            }
248        );
249    }
250}