Skip to main content

container_device_interface/
parser.rs

1use anyhow::{anyhow, Result};
2
3// QualifiedName returns the qualified name for a device.
4// The syntax for a qualified device names is
5//
6//	"<vendor>/<class>=<name>".
7//
8// A valid vendor and class name may contain the following runes:
9//
10//	'A'-'Z', 'a'-'z', '0'-'9', '.', '-', '_'.
11//
12// A valid device name may contain the following runes:
13//
14//	'A'-'Z', 'a'-'z', '0'-'9', '-', '_', '.', ':'
15#[allow(dead_code)]
16pub(crate) fn qualified_name(vendor: &str, class: &str, name: &str) -> String {
17    format!("{}/{}={}", vendor, class, name)
18}
19
20// IsQualifiedName tests if a device name is qualified.
21#[allow(dead_code)]
22pub(crate) fn is_qualified_name(name: &str) -> bool {
23    match parse_qualified_name(name) {
24        Ok(_) => {
25            println!("{} is a qualified name", name);
26            true
27        }
28        Err(e) => {
29            println!("{} is not a qualified name, {}", name, e);
30            false
31        }
32    }
33}
34
35// ParseQualifiedName splits a qualified name into device vendor, class,
36// and name. If the device fails to parse as a qualified name, or if any
37// of the split components fail to pass syntax validation, vendor and
38// class are returned as empty, together with the verbatim input as the
39// name and an error describing the reason for failure.
40pub(crate) fn parse_qualified_name(
41    device: &str,
42) -> Result<(String, String, String), anyhow::Error> {
43    let (vendor, class, name) = parse_device(device);
44    if vendor.is_empty() {
45        return Err(anyhow!("unqualified device {}, missing vendor", device));
46    }
47    if class.is_empty() {
48        return Err(anyhow!("unqualified device {}, missing class", device));
49    }
50    if name.is_empty() {
51        return Err(anyhow!("unqualified device {}, missing name", device));
52    }
53    if let Err(e) = validate_vendor_name(vendor) {
54        return Err(anyhow!("invalid vendor {}: {}", device, e));
55    }
56    if let Err(e) = validate_class_name(class) {
57        return Err(anyhow!("invalid class {}: {}", device, e));
58    }
59    if let Err(e) = validate_device_name(name) {
60        return Err(anyhow!("invalid device {}: {}", device, e));
61    }
62    Ok((vendor.to_string(), class.to_string(), name.to_string()))
63}
64
65// ParseDevice tries to split a device name into vendor, class, and name.
66// If this fails, for instance in the case of unqualified device names,
67// ParseDevice returns an empty vendor and class together with name set
68// to the verbatim input.
69pub(crate) fn parse_device(device: &str) -> (&str, &str, &str) {
70    if device.is_empty() || device.starts_with('/') {
71        return ("", "", device);
72    }
73
74    let parts: Vec<&str> = device.split('=').collect();
75    if parts.len() != 2 || parts[0].is_empty() || parts[1].is_empty() {
76        return ("", "", device);
77    }
78
79    let name = parts[1];
80    let (vendor, class) = parse_qualifier(parts[0]);
81    if vendor.is_empty() {
82        return ("", "", device);
83    }
84    (vendor, class, name)
85}
86
87// ParseQualifier splits a device qualifier into vendor and class.
88// The syntax for a device qualifier is
89//
90//	"<vendor>/<class>"
91//
92// If parsing fails, an empty vendor and the class set to the
93// verbatim input is returned.
94pub(crate) fn parse_qualifier(kind: &str) -> (&str, &str) {
95    let parts: Vec<&str> = kind.split('/').collect();
96    if parts.len() != 2 || parts[0].is_empty() || parts[1].is_empty() {
97        return ("", kind);
98    }
99    (parts[0], parts[1])
100}
101
102// ValidateVendorName checks the validity of a vendor name.
103// A vendor name may contain the following ASCII characters:
104//   - upper- and lowercase letters ('A'-'Z', 'a'-'z')
105//   - digits ('0'-'9')
106//   - underscore, dash, and dot ('_', '-', and '.')
107pub(crate) fn validate_vendor_name(vendor: &str) -> Result<()> {
108    if let Err(e) = validate_vendor_or_class_name(vendor) {
109        return Err(anyhow!("invalid vendor. {}", e));
110    }
111
112    Ok(())
113}
114
115// ValidateClassName checks the validity of class name.
116// A class name may contain the following ASCII characters:
117//   - upper- and lowercase letters ('A'-'Z', 'a'-'z')
118//   - digits ('0'-'9')
119//   - underscore, dash, and dot ('_', '-', and '.')
120pub(crate) fn validate_class_name(class: &str) -> Result<()> {
121    if let Err(e) = validate_vendor_or_class_name(class) {
122        return Err(anyhow!("invalid class. {}", e));
123    }
124
125    Ok(())
126}
127
128// validateVendorOrClassName checks the validity of vendor or class name.
129// A name may contain the following ASCII characters:
130//   - upper- and lowercase letters ('A'-'Z', 'a'-'z')
131//   - digits ('0'-'9')
132//   - underscore, dash, and dot ('_', '-', and '.')
133pub(crate) fn validate_vendor_or_class_name(name: &str) -> Result<()> {
134    if name.is_empty() {
135        return Err(anyhow!("empty name"));
136    }
137    if !name.chars().next().is_some_and(|c| c.is_ascii_alphabetic()) {
138        return Err(anyhow!("name should start with a letter"));
139    }
140    if !name
141        .chars()
142        .last()
143        .is_some_and(|c| c.is_ascii_alphanumeric())
144    {
145        return Err(anyhow!("name should end with a letter or digit"));
146    }
147    let char_count = name.chars().count();
148    if let Some(c) = name
149        .chars()
150        .skip(1)
151        .take(char_count.saturating_sub(2))
152        .find(|&c| !c.is_ascii_alphanumeric() && c != '-' && c != '_' && c != '.')
153    {
154        return Err(anyhow!("invalid character '{}' in name {}", c, name));
155    }
156    Ok(())
157}
158
159// ValidateDeviceName checks the validity of a device name.
160// A device name may contain the following ASCII characters:
161//   - upper- and lowercase letters ('A'-'Z', 'a'-'z')
162//   - digits ('0'-'9')
163//   - underscore, dash, dot, colon ('_', '-', '.', ':')
164pub(crate) fn validate_device_name(name: &str) -> Result<()> {
165    if name.is_empty() {
166        return Err(anyhow!("empty name"));
167    }
168    if !name
169        .chars()
170        .next()
171        .is_some_and(|c| c.is_ascii_alphanumeric())
172    {
173        return Err(anyhow!("name should start with a letter or digit"));
174    }
175    if !name
176        .chars()
177        .last()
178        .is_some_and(|c| c.is_ascii_alphanumeric())
179    {
180        return Err(anyhow!("name should end with a letter or digit"));
181    }
182    let char_count = name.chars().count();
183    if let Some(c) = name
184        .chars()
185        .skip(1)
186        .take(char_count.saturating_sub(2))
187        .find(|&c| !c.is_ascii_alphanumeric() && c != '-' && c != '_' && c != '.' && c != ':')
188    {
189        return Err(anyhow!("invalid character '{}' in device name {}", c, name));
190    }
191    Ok(())
192}
193
194#[cfg(test)]
195mod tests {
196
197    use crate::parser;
198
199    #[test]
200
201    fn qualified_name() {
202        let vendor = "nvidia.com";
203        let class = "gpu";
204        let name = "0";
205        let device = parser::qualified_name(vendor, class, name);
206        assert_eq!(device, "nvidia.com/gpu=0");
207        assert!(parser::is_qualified_name(&device));
208    }
209
210    #[test]
211    fn parse_qualified_name() {
212        let device = "nvidia.com/gpu=0";
213        match parser::parse_qualified_name(device) {
214            Ok((vendor, class, name)) => {
215                assert_eq!(vendor, "nvidia.com");
216                assert_eq!(class, "gpu");
217                assert_eq!(name, "0");
218            }
219            Err(e) => {
220                println!("error: {}", e);
221            }
222        }
223    }
224
225    #[test]
226    fn parse_device() {
227        let device = "nvidia.com/gpu=0";
228        let (vendor, class, name) = parser::parse_device(device);
229        assert_eq!(vendor, "nvidia.com");
230        assert_eq!(class, "gpu");
231        assert_eq!(name, "0");
232    }
233
234    #[test]
235    fn parse_qualifier() {
236        let qualifier = "nvidia.com/gpu";
237        let (vendor, class) = parser::parse_qualifier(qualifier);
238        assert_eq!(vendor, "nvidia.com");
239        assert_eq!(class, "gpu");
240    }
241
242    #[test]
243    fn validate_vendor_name() {
244        let vendor = "nvidia.com";
245        assert!(parser::validate_vendor_name(vendor).is_ok());
246
247        let vendor = "nvi((dia";
248        assert!(parser::validate_vendor_name(vendor).is_err());
249    }
250    #[test]
251    fn validate_class_name() {
252        let class = "gpu";
253        assert!(parser::validate_class_name(class).is_ok());
254
255        let class = "g(pu";
256        assert!(parser::validate_class_name(class).is_err());
257    }
258
259    #[test]
260    fn validate_device_name() {
261        let name = "0";
262        assert!(parser::validate_device_name(name).is_ok());
263
264        let name = "0(";
265        assert!(parser::validate_device_name(name).is_err());
266    }
267    #[test]
268    fn validate_vendor_or_class_name() {
269        let name = "nvidia.com";
270        assert!(parser::validate_vendor_or_class_name(name).is_ok());
271
272        let name = "nvi((dia.com";
273        assert!(parser::validate_vendor_or_class_name(name).is_err());
274    }
275
276    #[test]
277    fn validate_names_require_alphanumeric_endpoints() {
278        assert!(parser::validate_vendor_name("vendor.com").is_ok());
279        assert!(parser::validate_vendor_name("vendor.").is_err());
280        assert!(parser::validate_class_name("gpu-").is_err());
281        assert!(parser::validate_device_name("_gpu").is_err());
282        assert!(parser::validate_device_name("gpu_").is_err());
283    }
284
285    #[test]
286    fn qualified_name_detection_both_ways() {
287        assert!(parser::is_qualified_name("vendor.com/class=dev"));
288        assert!(!parser::is_qualified_name("/dev/null"));
289    }
290
291    #[test]
292    fn parse_qualified_name_rejects_malformed_input() {
293        for (input, needle) in [
294            ("", "missing vendor"),
295            ("/dev/null", "missing vendor"),
296            ("class=dev", "missing vendor"),
297            ("vendor.com/class", "missing vendor"),
298            ("vendor.com/class=", "missing vendor"),
299            ("_vendor/class=dev", "invalid vendor"),
300            ("vendor.com/cl*ss=dev", "invalid class"),
301            ("vendor.com/class=dev name", "invalid device"),
302        ] {
303            let err = parser::parse_qualified_name(input).unwrap_err();
304            assert!(
305                err.to_string().contains(needle),
306                "{input:?}: expected {needle:?} in {err}"
307            );
308        }
309    }
310
311    #[test]
312    fn parse_qualifier_splits_or_returns_verbatim() {
313        assert_eq!(parser::parse_qualifier("vendor/class"), ("vendor", "class"));
314        assert_eq!(parser::parse_qualifier("noslash"), ("", "noslash"));
315        assert_eq!(parser::parse_qualifier("vendor/"), ("", "vendor/"));
316    }
317}