container_device_interface/
parser.rs1use anyhow::{anyhow, Result};
2
3#[allow(dead_code)]
16pub(crate) fn qualified_name(vendor: &str, class: &str, name: &str) -> String {
17 format!("{}/{}={}", vendor, class, name)
18}
19
20#[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
35pub(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
65pub(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
87pub(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
102pub(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
115pub(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
128pub(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
159pub(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}