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, Debug, Eq, PartialEq)]
16pub struct Modalias {
17 pub vendor_id: u16,
18 pub product_id: u16,
19 pub device_id: u16,
20}
21
22impl Display for Modalias {
23 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
24 write!(
25 f,
26 "usb:v{:04X}p{:04X}d{:04X}",
27 self.vendor_id, self.product_id, self.device_id
28 )
29 }
30}
31
32impl FromStr for Modalias {
33 type Err = ParseModaliasError;
34
35 fn from_str(s: &str) -> Result<Self, Self::Err> {
36 RawModalias::from_str(s)?
37 .try_into()
38 .map_err(|_| ParseModaliasError(s.to_owned()))
39 }
40}
41
42impl TryFrom<RawModalias> for Modalias {
43 type Error = ();
44
45 fn try_from(raw: RawModalias) -> Result<Self, Self::Error> {
46 if raw.subtype != "usb" {
47 return Err(());
48 }
49 Ok(Modalias {
50 vendor_id: u16::from_str_radix(raw.values.get("v").ok_or(())?, 16).map_err(|_| ())?,
51 product_id: u16::from_str_radix(raw.values.get("p").ok_or(())?, 16).map_err(|_| ())?,
52 device_id: u16::from_str_radix(raw.values.get("d").ok_or(())?, 16).map_err(|_| ())?,
53 })
54 }
55}
56
57#[derive(Clone, Debug, Eq, PartialEq)]
58struct RawModalias {
59 pub subtype: String,
60 pub values: HashMap<String, String>,
61}
62
63impl FromStr for RawModalias {
64 type Err = ParseModaliasError;
65
66 fn from_str(s: &str) -> Result<RawModalias, Self::Err> {
67 if let Some((subtype, mut rest)) = s.split_once(':') {
68 let mut values = HashMap::new();
69 while !rest.is_empty() {
70 if let Some(key_end) = rest.find(|c: char| !c.is_ascii_lowercase()) {
73 let key = rest[0..key_end].to_owned();
74 rest = &rest[key_end..];
75 if let Some(key_start) = rest.find(|c: char| c.is_ascii_lowercase()) {
76 let value = rest[0..key_start].to_owned();
77 values.insert(key, value);
78 rest = &rest[key_start..];
79 } else {
80 values.insert(key, rest.to_owned());
82 break;
83 }
84 } else {
85 values.insert(rest.to_owned(), "".to_owned());
87 break;
88 }
89 }
90
91 Ok(RawModalias {
92 subtype: subtype.to_owned(),
93 values,
94 })
95 } else {
96 Err(ParseModaliasError(s.to_owned()))
97 }
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104
105 #[test]
106 fn parse() {
107 assert_eq!(
108 Modalias::from_str("usb:v0000p0000d0000").unwrap(),
109 Modalias {
110 vendor_id: 0,
111 product_id: 0,
112 device_id: 0
113 }
114 );
115 assert_eq!(
116 Modalias::from_str("usb:v1234p5678d90AB").unwrap(),
117 Modalias {
118 vendor_id: 0x1234,
119 product_id: 0x5678,
120 device_id: 0x90AB
121 }
122 );
123 }
124
125 #[test]
126 fn parse_invalid_subtype() {
127 assert!(matches!(
128 Modalias::from_str("blah:v0000p0000d0000"),
129 Err(ParseModaliasError(_))
130 ));
131 }
132
133 #[test]
134 fn parse_missing_fields() {
135 assert!(matches!(
136 Modalias::from_str("usb:"),
137 Err(ParseModaliasError(_))
138 ));
139 assert!(matches!(
140 Modalias::from_str("usb:v1234p5678"),
141 Err(ParseModaliasError(_))
142 ));
143 }
144
145 #[test]
146 fn to_string() {
147 assert_eq!(
148 Modalias {
149 vendor_id: 0,
150 product_id: 0,
151 device_id: 0
152 }
153 .to_string(),
154 "usb:v0000p0000d0000"
155 );
156 assert_eq!(
157 Modalias {
158 vendor_id: 0x1234,
159 product_id: 0x5678,
160 device_id: 0x90AB
161 }
162 .to_string(),
163 "usb:v1234p5678d90AB"
164 );
165 }
166
167 #[test]
168 fn parse_raw_empty() {
169 assert!(matches!(
170 RawModalias::from_str(""),
171 Err(ParseModaliasError(_))
172 ));
173 }
174
175 #[test]
176 fn parse_raw_empty_usb() {
177 assert_eq!(
178 RawModalias::from_str("usb:").unwrap(),
179 RawModalias {
180 subtype: "usb".to_string(),
181 values: HashMap::new()
182 }
183 );
184 }
185
186 #[test]
187 fn parse_raw_success() {
188 let mut values = HashMap::new();
189 values.insert("a".to_string(), "AB12".to_string());
190 values.insert("ab".to_string(), "01".to_string());
191 assert_eq!(
192 RawModalias::from_str("usb:aAB12ab01").unwrap(),
193 RawModalias {
194 subtype: "usb".to_string(),
195 values
196 }
197 );
198 }
199}