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().unwrap_or_default().is_alphabetic() {
138        return Err(anyhow!("name should start with a letter"));
139    }
140    if let Some(c) = name
141        .chars()
142        .find(|&c| !c.is_alphanumeric() && c != '-' && c != '_' && c != '.')
143    {
144        return Err(anyhow!("invalid character '{}' in name {}", c, name));
145    }
146    Ok(())
147}
148
149// ValidateDeviceName checks the validity of a device name.
150// A device name may contain the following ASCII characters:
151//   - upper- and lowercase letters ('A'-'Z', 'a'-'z')
152//   - digits ('0'-'9')
153//   - underscore, dash, dot, colon ('_', '-', '.', ':')
154pub(crate) fn validate_device_name(name: &str) -> Result<()> {
155    if name.is_empty() {
156        return Err(anyhow!("empty name"));
157    }
158    if let Some(c) = name
159        .chars()
160        .find(|&c| !c.is_alphanumeric() && c != '-' && c != '_' && c != '.' && c != ':')
161    {
162        return Err(anyhow!("invalid character '{}' in device name {}", c, name));
163    }
164    Ok(())
165}
166
167#[cfg(test)]
168mod tests {
169
170    use crate::parser;
171
172    #[test]
173
174    fn qualified_name() {
175        let vendor = "nvidia.com";
176        let class = "gpu";
177        let name = "0";
178        let device = parser::qualified_name(vendor, class, name);
179        assert_eq!(device, "nvidia.com/gpu=0");
180        assert!(parser::is_qualified_name(&device));
181    }
182
183    #[test]
184    fn parse_qualified_name() {
185        let device = "nvidia.com/gpu=0";
186        match parser::parse_qualified_name(device) {
187            Ok((vendor, class, name)) => {
188                assert_eq!(vendor, "nvidia.com");
189                assert_eq!(class, "gpu");
190                assert_eq!(name, "0");
191            }
192            Err(e) => {
193                println!("error: {}", e);
194            }
195        }
196    }
197
198    #[test]
199    fn parse_device() {
200        let device = "nvidia.com/gpu=0";
201        let (vendor, class, name) = parser::parse_device(device);
202        assert_eq!(vendor, "nvidia.com");
203        assert_eq!(class, "gpu");
204        assert_eq!(name, "0");
205    }
206
207    #[test]
208    fn parse_qualifier() {
209        let qualifier = "nvidia.com/gpu";
210        let (vendor, class) = parser::parse_qualifier(qualifier);
211        assert_eq!(vendor, "nvidia.com");
212        assert_eq!(class, "gpu");
213    }
214
215    #[test]
216    fn validate_vendor_name() {
217        let vendor = "nvidia.com";
218        assert!(parser::validate_vendor_name(vendor).is_ok());
219
220        let vendor = "nvi((dia";
221        assert!(parser::validate_vendor_name(vendor).is_err());
222    }
223    #[test]
224    fn validate_class_name() {
225        let class = "gpu";
226        assert!(parser::validate_class_name(class).is_ok());
227
228        let class = "g(pu";
229        assert!(parser::validate_class_name(class).is_err());
230    }
231
232    #[test]
233    fn validate_device_name() {
234        let name = "0";
235        assert!(parser::validate_device_name(name).is_ok());
236
237        let name = "0(";
238        assert!(parser::validate_device_name(name).is_err());
239    }
240    #[test]
241    fn validate_vendor_or_class_name() {
242        let name = "nvidia.com";
243        assert!(parser::validate_vendor_or_class_name(name).is_ok());
244
245        let name = "nvi((dia.com";
246        assert!(parser::validate_vendor_or_class_name(name).is_err());
247    }
248}