1use std::collections::HashMap;
2use std::convert::{TryFrom, TryInto};
3use std::fmt::{self, Display, Formatter};
4use std::str::FromStr;
5use thiserror::Error;
6
7#[derive(Clone, Debug, Error, Eq, PartialEq)]
9#[error("Error parsing modalias string {0:?}")]
10pub struct ParseModaliasError(String);
11
12#[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#[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 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 values.insert(key, rest.to_owned());
110 break;
111 }
112 } else {
113 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}