container-device-interface 0.1.2

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)
}