container_device_interface/internal/validation/k8s/
validation.rs

1use const_format::concatcp;
2use lazy_static::lazy_static;
3use regex::Regex;
4
5const QNAME_CHAR_FMT: &str = "[A-Za-z0-9]";
6const QNAME_EXT_CHAR_FMT: &str = "[-A-Za-z0-9_.]";
7const QUALIFIED_NAME_FMT: &str = concatcp!(
8    "(",
9    QNAME_CHAR_FMT,
10    QNAME_EXT_CHAR_FMT,
11    "*)?",
12    QNAME_CHAR_FMT
13);
14const QUALIFIED_NAME_ERR_MSG: &str = "must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character";
15const QUALIFIED_NAME_MAX_LENGTH: usize = 63;
16
17const LABEL_VALUE_FMT: &str = concatcp!("(", QUALIFIED_NAME_FMT, ")?");
18const 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";
19const LABEL_VALUE_MAX_LENGTH: usize = 63;
20
21const DNS1123_LABEL_FMT: &str = "[a-z0-9]([-a-z0-9]*[a-z0-9])?";
22const 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";
23const DNS1123_LABEL_MAX_LENGTH: usize = 63;
24
25const DNS1123_SUBDOMAIN_FMT: &str = concatcp!(DNS1123_LABEL_FMT, "(\\.", DNS1123_LABEL_FMT, ")*");
26const 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";
27const DNS1123_SUBDOMAIN_MAX_LENGTH: usize = 253;
28
29const DNS1035_LABEL_FMT: &str = "[a-z]([-a-z0-9]*[a-z0-9])?";
30const 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";
31const DNS1035_LABEL_MAX_LENGTH: usize = 63;
32
33const WILDCARD_DNS1123_SUBDOMAIN_FMT: &str = concatcp!("\\*\\.", DNS1123_SUBDOMAIN_FMT);
34const 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";
35
36lazy_static! {
37    static ref QUALIFIED_NAME_REGEXP: Regex =
38        Regex::new(&format!("^{}$", QUALIFIED_NAME_FMT)).unwrap();
39    static ref LABEL_VALUE_REGEXP: Regex = Regex::new(&format!("^{}$", LABEL_VALUE_FMT)).unwrap();
40    static ref DNS1123_LABEL_REGEXP: Regex =
41        Regex::new(&format!("^{}$", DNS1123_LABEL_FMT)).unwrap();
42    static ref DNS1123_SUBDOMAIN_REGEXP: Regex =
43        Regex::new(&format!("^{}$", DNS1123_SUBDOMAIN_FMT)).unwrap();
44    static ref DNS1035_LABEL_REGEXP: Regex =
45        Regex::new(&format!("^{}$", DNS1035_LABEL_FMT)).unwrap();
46    static ref WILDCARD_DNS1123_SUBDOMAIN_REGEXP: Regex =
47        Regex::new(&format!("^{}$", WILDCARD_DNS1123_SUBDOMAIN_FMT)).unwrap();
48}
49
50pub fn is_qualified_name(value: &str) -> Vec<String> {
51    let mut errs = Vec::new();
52    let parts: Vec<&str> = value.split('/').collect();
53    let name: &str;
54
55    match parts.len() {
56        1 => {
57            name = parts[0];
58        }
59        2 => {
60            let prefix = parts[0];
61            name = parts[1];
62            if prefix.is_empty() {
63                errs.push(format!("prefix part {}", empty_error()));
64            } else {
65                let msgs = is_dns1123_subdomain(prefix);
66                if msgs.is_empty() {
67                    errs.extend(prefix_each(&msgs, "prefix part "));
68                }
69            }
70        }
71        _ => {
72            return vec![format!(
73			"a qualified name {} with an optional DNS subdomain prefix and '/' (e.g. 'example.com/MyName')",
74			regex_error(QUALIFIED_NAME_ERR_MSG, QUALIFIED_NAME_FMT, &["MyName", "my.name", "123-abc"])
75		    )];
76        }
77    }
78
79    if name.is_empty() {
80        errs.push(format!("name part {}", empty_error()));
81    } else if name.len() > QUALIFIED_NAME_MAX_LENGTH {
82        errs.push(format!(
83            "name part {}",
84            max_len_error(QUALIFIED_NAME_MAX_LENGTH)
85        ));
86    }
87
88    if !QUALIFIED_NAME_REGEXP.is_match(name) {
89        errs.push(format!(
90            "name part {}",
91            regex_error(
92                QUALIFIED_NAME_ERR_MSG,
93                QUALIFIED_NAME_FMT,
94                &["MyName", "my.name", "123-abc"]
95            )
96        ));
97    }
98
99    errs
100}
101
102#[allow(dead_code)]
103fn is_valid_label_value(value: &str) -> Vec<String> {
104    let mut errs = Vec::new();
105    if value.len() > LABEL_VALUE_MAX_LENGTH {
106        errs.push(max_len_error(LABEL_VALUE_MAX_LENGTH));
107    }
108    if !LABEL_VALUE_REGEXP.is_match(value) {
109        errs.push(regex_error(
110            LABEL_VALUE_ERR_MSG,
111            LABEL_VALUE_FMT,
112            &["MyValue", "my_value", "12345"],
113        ));
114    }
115    errs
116}
117
118#[allow(dead_code)]
119fn is_dns1123_label(value: &str) -> Vec<String> {
120    let mut errs = Vec::new();
121    if value.len() > DNS1123_LABEL_MAX_LENGTH {
122        errs.push(max_len_error(DNS1123_LABEL_MAX_LENGTH));
123    }
124    if !DNS1123_LABEL_REGEXP.is_match(value) {
125        errs.push(regex_error(
126            DNS1123_LABEL_ERR_MSG,
127            DNS1123_LABEL_FMT,
128            &["my-name", "123-abc"],
129        ));
130    }
131    errs
132}
133
134fn is_dns1123_subdomain(value: &str) -> Vec<String> {
135    let mut errs = Vec::new();
136    if value.len() > DNS1123_SUBDOMAIN_MAX_LENGTH {
137        errs.push(max_len_error(DNS1123_SUBDOMAIN_MAX_LENGTH));
138    }
139    if !DNS1123_SUBDOMAIN_REGEXP.is_match(value) {
140        errs.push(regex_error(
141            DNS1123_SUBDOMAIN_ERR_MSG,
142            DNS1123_SUBDOMAIN_FMT,
143            &["example.com"],
144        ));
145    }
146    errs
147}
148
149#[allow(dead_code)]
150fn is_dns1035_label(value: &str) -> Vec<String> {
151    let mut errs = Vec::new();
152    if value.len() > DNS1035_LABEL_MAX_LENGTH {
153        errs.push(max_len_error(DNS1035_LABEL_MAX_LENGTH));
154    }
155    if !DNS1035_LABEL_REGEXP.is_match(value) {
156        errs.push(regex_error(
157            DNS1035_LABEL_ERR_MSG,
158            DNS1035_LABEL_FMT,
159            &["my-name", "abc-123"],
160        ));
161    }
162    errs
163}
164
165#[allow(dead_code)]
166fn is_wildcard_dns1123_subdomain(value: &str) -> Vec<String> {
167    let mut errs = Vec::new();
168    if value.len() > DNS1123_SUBDOMAIN_MAX_LENGTH {
169        errs.push(max_len_error(DNS1123_SUBDOMAIN_MAX_LENGTH));
170    }
171    if !WILDCARD_DNS1123_SUBDOMAIN_REGEXP.is_match(value) {
172        errs.push(regex_error(
173            WILDCARD_DNS1123_SUBDOMAIN_ERR_MSG,
174            WILDCARD_DNS1123_SUBDOMAIN_FMT,
175            &["*.example.com"],
176        ));
177    }
178    errs
179}
180
181fn max_len_error(max_length: usize) -> String {
182    format!("must be no more than {} characters", max_length)
183}
184
185fn regex_error(err_msg: &str, regex_fmt: &str, examples: &[&str]) -> String {
186    if examples.is_empty() {
187        format!("{} (regex used for validation is '{}')", err_msg, regex_fmt)
188    } else {
189        let examples_str = examples
190            .iter()
191            .map(|&e| format!("'{}'", e))
192            .collect::<Vec<String>>()
193            .join(" or ");
194        format!(
195            "{} (e.g. {}. regex used for validation is '{}')",
196            err_msg, examples_str, regex_fmt
197        )
198    }
199}
200
201fn empty_error() -> &'static str {
202    "must be non-empty"
203}
204
205fn prefix_each(msgs: &[String], prefix: &str) -> Vec<String> {
206    msgs.iter()
207        .map(|msg| format!("{}{}", prefix, msg))
208        .collect()
209}
210
211#[allow(dead_code)]
212fn inclusive_range_error(lo: usize, hi: usize) -> String {
213    format!("must be between {} and {}, inclusive", lo, hi)
214}