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/// A parsed modalias string.
13///
14/// For now only the USB subtype is supported.
15#[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                // Find the end of the next key, which must only consist of lowercase ASCII
71                // characters.
72                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                        // There are no more values, the rest is the key.
81                        values.insert(key, rest.to_owned());
82                        break;
83                    }
84                } else {
85                    // The rest of the string is a key, with no value.
86                    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}