container-device-interface 1.1.0

CDI (Container Device Interface), is a specification, for container-runtimes, to support third-party devices.
Documentation
use const_format::concatcp;
use lazy_static::lazy_static;
use regex::Regex;

const QNAME_CHAR_FMT: &str = "[A-Za-z0-9]";
const QNAME_EXT_CHAR_FMT: &str = "[-A-Za-z0-9_.]";
const QUALIFIED_NAME_FMT: &str = concatcp!(
    "(",
    QNAME_CHAR_FMT,
    QNAME_EXT_CHAR_FMT,
    "*)?",
    QNAME_CHAR_FMT
);
const QUALIFIED_NAME_ERR_MSG: &str = "must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character";
const QUALIFIED_NAME_MAX_LENGTH: usize = 63;

const LABEL_VALUE_FMT: &str = concatcp!("(", QUALIFIED_NAME_FMT, ")?");
const LABEL_VALUE_ERR_MSG: &str = "a valid label must be an empty string or consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character";
const LABEL_VALUE_MAX_LENGTH: usize = 63;

const DNS1123_LABEL_FMT: &str = "[a-z0-9]([-a-z0-9]*[a-z0-9])?";
const DNS1123_LABEL_ERR_MSG: &str = "a lowercase RFC 1123 label must consist of lower case alphanumeric characters or '-', and must start and end with an alphanumeric character";
const DNS1123_LABEL_MAX_LENGTH: usize = 63;

const DNS1123_SUBDOMAIN_FMT: &str = concatcp!(DNS1123_LABEL_FMT, "(\\.", DNS1123_LABEL_FMT, ")*");
const DNS1123_SUBDOMAIN_ERR_MSG: &str = "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character";
const DNS1123_SUBDOMAIN_MAX_LENGTH: usize = 253;

const DNS1035_LABEL_FMT: &str = "[a-z]([-a-z0-9]*[a-z0-9])?";
const DNS1035_LABEL_ERR_MSG: &str = "a DNS-1035 label must consist of lower case alphanumeric characters or '-', start with an alphabetic character, and end with an alphanumeric character";
const DNS1035_LABEL_MAX_LENGTH: usize = 63;

const WILDCARD_DNS1123_SUBDOMAIN_FMT: &str = concatcp!("\\*\\.", DNS1123_SUBDOMAIN_FMT);
const WILDCARD_DNS1123_SUBDOMAIN_ERR_MSG: &str = "a wildcard DNS-1123 subdomain must start with '*.', followed by a valid DNS subdomain, which must consist of lower case alphanumeric characters, '-' or '.' and end with an alphanumeric character";

lazy_static! {
    static ref QUALIFIED_NAME_REGEXP: Regex =
        Regex::new(&format!("^{}$", QUALIFIED_NAME_FMT)).unwrap();
    static ref LABEL_VALUE_REGEXP: Regex = Regex::new(&format!("^{}$", LABEL_VALUE_FMT)).unwrap();
    static ref DNS1123_LABEL_REGEXP: Regex =
        Regex::new(&format!("^{}$", DNS1123_LABEL_FMT)).unwrap();
    static ref DNS1123_SUBDOMAIN_REGEXP: Regex =
        Regex::new(&format!("^{}$", DNS1123_SUBDOMAIN_FMT)).unwrap();
    static ref DNS1035_LABEL_REGEXP: Regex =
        Regex::new(&format!("^{}$", DNS1035_LABEL_FMT)).unwrap();
    static ref WILDCARD_DNS1123_SUBDOMAIN_REGEXP: Regex =
        Regex::new(&format!("^{}$", WILDCARD_DNS1123_SUBDOMAIN_FMT)).unwrap();
}

pub fn is_qualified_name(value: &str) -> Vec<String> {
    let mut errs = Vec::new();
    let parts: Vec<&str> = value.split('/').collect();
    let name: &str;

    match parts.len() {
        1 => {
            name = parts[0];
        }
        2 => {
            let prefix = parts[0];
            name = parts[1];
            if prefix.is_empty() {
                errs.push(format!("prefix part {}", empty_error()));
            } else {
                let msgs = is_dns1123_subdomain(prefix);
                if !msgs.is_empty() {
                    errs.extend(prefix_each(&msgs, "prefix part "));
                }
            }
        }
        _ => {
            return vec![format!(
			"a qualified name {} with an optional DNS subdomain prefix and '/' (e.g. 'example.com/MyName')",
			regex_error(QUALIFIED_NAME_ERR_MSG, QUALIFIED_NAME_FMT, &["MyName", "my.name", "123-abc"])
		    )];
        }
    }

    if name.is_empty() {
        errs.push(format!("name part {}", empty_error()));
    } else if name.len() > QUALIFIED_NAME_MAX_LENGTH {
        errs.push(format!(
            "name part {}",
            max_len_error(QUALIFIED_NAME_MAX_LENGTH)
        ));
    }

    if !QUALIFIED_NAME_REGEXP.is_match(name) {
        errs.push(format!(
            "name part {}",
            regex_error(
                QUALIFIED_NAME_ERR_MSG,
                QUALIFIED_NAME_FMT,
                &["MyName", "my.name", "123-abc"]
            )
        ));
    }

    errs
}

#[allow(dead_code)]
fn is_valid_label_value(value: &str) -> Vec<String> {
    let mut errs = Vec::new();
    if value.len() > LABEL_VALUE_MAX_LENGTH {
        errs.push(max_len_error(LABEL_VALUE_MAX_LENGTH));
    }
    if !LABEL_VALUE_REGEXP.is_match(value) {
        errs.push(regex_error(
            LABEL_VALUE_ERR_MSG,
            LABEL_VALUE_FMT,
            &["MyValue", "my_value", "12345"],
        ));
    }
    errs
}

#[allow(dead_code)]
fn is_dns1123_label(value: &str) -> Vec<String> {
    let mut errs = Vec::new();
    if value.len() > DNS1123_LABEL_MAX_LENGTH {
        errs.push(max_len_error(DNS1123_LABEL_MAX_LENGTH));
    }
    if !DNS1123_LABEL_REGEXP.is_match(value) {
        errs.push(regex_error(
            DNS1123_LABEL_ERR_MSG,
            DNS1123_LABEL_FMT,
            &["my-name", "123-abc"],
        ));
    }
    errs
}

fn is_dns1123_subdomain(value: &str) -> Vec<String> {
    let mut errs = Vec::new();
    if value.len() > DNS1123_SUBDOMAIN_MAX_LENGTH {
        errs.push(max_len_error(DNS1123_SUBDOMAIN_MAX_LENGTH));
    }
    if !DNS1123_SUBDOMAIN_REGEXP.is_match(value) {
        errs.push(regex_error(
            DNS1123_SUBDOMAIN_ERR_MSG,
            DNS1123_SUBDOMAIN_FMT,
            &["example.com"],
        ));
    }
    errs
}

#[allow(dead_code)]
fn is_dns1035_label(value: &str) -> Vec<String> {
    let mut errs = Vec::new();
    if value.len() > DNS1035_LABEL_MAX_LENGTH {
        errs.push(max_len_error(DNS1035_LABEL_MAX_LENGTH));
    }
    if !DNS1035_LABEL_REGEXP.is_match(value) {
        errs.push(regex_error(
            DNS1035_LABEL_ERR_MSG,
            DNS1035_LABEL_FMT,
            &["my-name", "abc-123"],
        ));
    }
    errs
}

#[allow(dead_code)]
fn is_wildcard_dns1123_subdomain(value: &str) -> Vec<String> {
    let mut errs = Vec::new();
    if value.len() > DNS1123_SUBDOMAIN_MAX_LENGTH {
        errs.push(max_len_error(DNS1123_SUBDOMAIN_MAX_LENGTH));
    }
    if !WILDCARD_DNS1123_SUBDOMAIN_REGEXP.is_match(value) {
        errs.push(regex_error(
            WILDCARD_DNS1123_SUBDOMAIN_ERR_MSG,
            WILDCARD_DNS1123_SUBDOMAIN_FMT,
            &["*.example.com"],
        ));
    }
    errs
}

fn max_len_error(max_length: usize) -> String {
    format!("must be no more than {} characters", max_length)
}

fn regex_error(err_msg: &str, regex_fmt: &str, examples: &[&str]) -> String {
    if examples.is_empty() {
        format!("{} (regex used for validation is '{}')", err_msg, regex_fmt)
    } else {
        let examples_str = examples
            .iter()
            .map(|&e| format!("'{}'", e))
            .collect::<Vec<String>>()
            .join(" or ");
        format!(
            "{} (e.g. {}. regex used for validation is '{}')",
            err_msg, examples_str, regex_fmt
        )
    }
}

fn empty_error() -> &'static str {
    "must be non-empty"
}

fn prefix_each(msgs: &[String], prefix: &str) -> Vec<String> {
    msgs.iter()
        .map(|msg| format!("{}{}", prefix, msg))
        .collect()
}

#[allow(dead_code)]
fn inclusive_range_error(lo: usize, hi: usize) -> String {
    format!("must be between {} and {}, inclusive", lo, hi)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn qualified_names_accept_valid_forms() {
        for name in ["simple", "my.name", "123-abc", "example.com/MyName"] {
            assert!(is_qualified_name(name).is_empty(), "{name} should be valid");
        }
    }

    #[test]
    fn qualified_names_reject_invalid_name_part() {
        assert!(!is_qualified_name("").is_empty());
        assert!(!is_qualified_name("-leading.dash").is_empty());
        assert!(!is_qualified_name(&"x".repeat(64)).is_empty());
        assert!(!is_qualified_name("a/b/c").is_empty());
        assert!(!is_qualified_name("/name").is_empty());
    }

    // Regression: prefix errors were dropped (inverted is_empty check), so
    // an invalid DNS-1123 prefix used to validate successfully.
    #[test]
    fn qualified_names_reject_invalid_prefix() {
        let errs = is_qualified_name("Bad_Prefix/name");
        assert!(
            errs.iter().any(|e| e.starts_with("prefix part ")),
            "invalid prefix must produce a prefix error, got: {errs:?}"
        );
    }

    #[test]
    fn label_values() {
        assert!(is_valid_label_value("").is_empty());
        assert!(is_valid_label_value("MyValue").is_empty());
        assert!(!is_valid_label_value("no spaces").is_empty());
        assert!(!is_valid_label_value(&"x".repeat(64)).is_empty());
    }

    #[test]
    fn dns1123_labels_and_subdomains() {
        assert!(is_dns1123_label("my-name").is_empty());
        assert!(!is_dns1123_label("My-Name").is_empty());
        assert!(!is_dns1123_label(&"x".repeat(64)).is_empty());
        assert!(is_dns1123_subdomain("example.com").is_empty());
        assert!(!is_dns1123_subdomain("Example.com").is_empty());
        assert!(!is_dns1123_subdomain(&"x".repeat(254)).is_empty());
    }

    #[test]
    fn dns1035_labels() {
        assert!(is_dns1035_label("abc-123").is_empty());
        assert!(!is_dns1035_label("1abc").is_empty());
        assert!(!is_dns1035_label(&"x".repeat(64)).is_empty());
    }

    #[test]
    fn wildcard_subdomains() {
        assert!(is_wildcard_dns1123_subdomain("*.example.com").is_empty());
        assert!(!is_wildcard_dns1123_subdomain("example.com").is_empty());
        assert!(!is_wildcard_dns1123_subdomain(&format!("*.{}", "x".repeat(253))).is_empty());
    }

    #[test]
    fn error_message_helpers() {
        assert!(regex_error("msg", "fmt", &[]).contains("regex used for validation"));
        assert!(regex_error("msg", "fmt", &["ex"]).contains("e.g. 'ex'"));
        assert_eq!(
            inclusive_range_error(1, 5),
            "must be between 1 and 5, inclusive"
        );
    }
}