container_device_interface/internal/validation/k8s/
validation.rs1use 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}