use anyhow::{anyhow, Result};
#[allow(dead_code)]
pub(crate) fn qualified_name(vendor: &str, class: &str, name: &str) -> String {
format!("{}/{}={}", vendor, class, name)
}
#[allow(dead_code)]
pub(crate) fn is_qualified_name(name: &str) -> bool {
match parse_qualified_name(name) {
Ok(_) => {
println!("{} is a qualified name", name);
true
}
Err(e) => {
println!("{} is not a qualified name, {}", name, e);
false
}
}
}
pub(crate) fn parse_qualified_name(
device: &str,
) -> Result<(String, String, String), anyhow::Error> {
let (vendor, class, name) = parse_device(device);
if vendor.is_empty() {
return Err(anyhow!("unqualified device {}, missing vendor", device));
}
if class.is_empty() {
return Err(anyhow!("unqualified device {}, missing class", device));
}
if name.is_empty() {
return Err(anyhow!("unqualified device {}, missing name", device));
}
if let Err(e) = validate_vendor_name(vendor) {
return Err(anyhow!("invalid vendor {}: {}", device, e));
}
if let Err(e) = validate_class_name(class) {
return Err(anyhow!("invalid class {}: {}", device, e));
}
if let Err(e) = validate_device_name(name) {
return Err(anyhow!("invalid device {}: {}", device, e));
}
Ok((vendor.to_string(), class.to_string(), name.to_string()))
}
pub(crate) fn parse_device(device: &str) -> (&str, &str, &str) {
if device.is_empty() || device.starts_with('/') {
return ("", "", device);
}
let parts: Vec<&str> = device.split('=').collect();
if parts.len() != 2 || parts[0].is_empty() || parts[1].is_empty() {
return ("", "", device);
}
let name = parts[1];
let (vendor, class) = parse_qualifier(parts[0]);
if vendor.is_empty() {
return ("", "", device);
}
(vendor, class, name)
}
pub(crate) fn parse_qualifier(kind: &str) -> (&str, &str) {
let parts: Vec<&str> = kind.split('/').collect();
if parts.len() != 2 || parts[0].is_empty() || parts[1].is_empty() {
return ("", kind);
}
(parts[0], parts[1])
}
pub(crate) fn validate_vendor_name(vendor: &str) -> Result<()> {
if let Err(e) = validate_vendor_or_class_name(vendor) {
return Err(anyhow!("invalid vendor. {}", e));
}
Ok(())
}
pub(crate) fn validate_class_name(class: &str) -> Result<()> {
if let Err(e) = validate_vendor_or_class_name(class) {
return Err(anyhow!("invalid class. {}", e));
}
Ok(())
}
pub(crate) fn validate_vendor_or_class_name(name: &str) -> Result<()> {
if name.is_empty() {
return Err(anyhow!("empty name"));
}
if !name.chars().next().is_some_and(|c| c.is_ascii_alphabetic()) {
return Err(anyhow!("name should start with a letter"));
}
if !name
.chars()
.last()
.is_some_and(|c| c.is_ascii_alphanumeric())
{
return Err(anyhow!("name should end with a letter or digit"));
}
let char_count = name.chars().count();
if let Some(c) = name
.chars()
.skip(1)
.take(char_count.saturating_sub(2))
.find(|&c| !c.is_ascii_alphanumeric() && c != '-' && c != '_' && c != '.')
{
return Err(anyhow!("invalid character '{}' in name {}", c, name));
}
Ok(())
}
pub(crate) fn validate_device_name(name: &str) -> Result<()> {
if name.is_empty() {
return Err(anyhow!("empty name"));
}
if !name
.chars()
.next()
.is_some_and(|c| c.is_ascii_alphanumeric())
{
return Err(anyhow!("name should start with a letter or digit"));
}
if !name
.chars()
.last()
.is_some_and(|c| c.is_ascii_alphanumeric())
{
return Err(anyhow!("name should end with a letter or digit"));
}
let char_count = name.chars().count();
if let Some(c) = name
.chars()
.skip(1)
.take(char_count.saturating_sub(2))
.find(|&c| !c.is_ascii_alphanumeric() && c != '-' && c != '_' && c != '.' && c != ':')
{
return Err(anyhow!("invalid character '{}' in device name {}", c, name));
}
Ok(())
}
#[cfg(test)]
mod tests {
use crate::parser;
#[test]
fn qualified_name() {
let vendor = "nvidia.com";
let class = "gpu";
let name = "0";
let device = parser::qualified_name(vendor, class, name);
assert_eq!(device, "nvidia.com/gpu=0");
assert!(parser::is_qualified_name(&device));
}
#[test]
fn parse_qualified_name() {
let device = "nvidia.com/gpu=0";
match parser::parse_qualified_name(device) {
Ok((vendor, class, name)) => {
assert_eq!(vendor, "nvidia.com");
assert_eq!(class, "gpu");
assert_eq!(name, "0");
}
Err(e) => {
println!("error: {}", e);
}
}
}
#[test]
fn parse_device() {
let device = "nvidia.com/gpu=0";
let (vendor, class, name) = parser::parse_device(device);
assert_eq!(vendor, "nvidia.com");
assert_eq!(class, "gpu");
assert_eq!(name, "0");
}
#[test]
fn parse_qualifier() {
let qualifier = "nvidia.com/gpu";
let (vendor, class) = parser::parse_qualifier(qualifier);
assert_eq!(vendor, "nvidia.com");
assert_eq!(class, "gpu");
}
#[test]
fn validate_vendor_name() {
let vendor = "nvidia.com";
assert!(parser::validate_vendor_name(vendor).is_ok());
let vendor = "nvi((dia";
assert!(parser::validate_vendor_name(vendor).is_err());
}
#[test]
fn validate_class_name() {
let class = "gpu";
assert!(parser::validate_class_name(class).is_ok());
let class = "g(pu";
assert!(parser::validate_class_name(class).is_err());
}
#[test]
fn validate_device_name() {
let name = "0";
assert!(parser::validate_device_name(name).is_ok());
let name = "0(";
assert!(parser::validate_device_name(name).is_err());
}
#[test]
fn validate_vendor_or_class_name() {
let name = "nvidia.com";
assert!(parser::validate_vendor_or_class_name(name).is_ok());
let name = "nvi((dia.com";
assert!(parser::validate_vendor_or_class_name(name).is_err());
}
#[test]
fn validate_names_require_alphanumeric_endpoints() {
assert!(parser::validate_vendor_name("vendor.com").is_ok());
assert!(parser::validate_vendor_name("vendor.").is_err());
assert!(parser::validate_class_name("gpu-").is_err());
assert!(parser::validate_device_name("_gpu").is_err());
assert!(parser::validate_device_name("gpu_").is_err());
}
#[test]
fn qualified_name_detection_both_ways() {
assert!(parser::is_qualified_name("vendor.com/class=dev"));
assert!(!parser::is_qualified_name("/dev/null"));
}
#[test]
fn parse_qualified_name_rejects_malformed_input() {
for (input, needle) in [
("", "missing vendor"),
("/dev/null", "missing vendor"),
("class=dev", "missing vendor"),
("vendor.com/class", "missing vendor"),
("vendor.com/class=", "missing vendor"),
("_vendor/class=dev", "invalid vendor"),
("vendor.com/cl*ss=dev", "invalid class"),
("vendor.com/class=dev name", "invalid device"),
] {
let err = parser::parse_qualified_name(input).unwrap_err();
assert!(
err.to_string().contains(needle),
"{input:?}: expected {needle:?} in {err}"
);
}
}
#[test]
fn parse_qualifier_splits_or_returns_verbatim() {
assert_eq!(parser::parse_qualifier("vendor/class"), ("vendor", "class"));
assert_eq!(parser::parse_qualifier("noslash"), ("", "noslash"));
assert_eq!(parser::parse_qualifier("vendor/"), ("", "vendor/"));
}
}