zombienet_configuration/shared/
helpers.rs1use std::{cell::RefCell, collections::HashSet, rc::Rc};
2
3use support::constants::{BORROWABLE, THIS_IS_A_BUG};
4use tracing::warn;
5
6use super::{
7 errors::ValidationError,
8 types::{ParaId, Port, ValidationContext},
9};
10
11pub fn merge_errors(errors: Vec<anyhow::Error>, new_error: anyhow::Error) -> Vec<anyhow::Error> {
12 let mut errors = errors;
13 errors.push(new_error);
14
15 errors
16}
17
18pub fn merge_errors_vecs(
19 errors: Vec<anyhow::Error>,
20 new_errors: Vec<anyhow::Error>,
21) -> Vec<anyhow::Error> {
22 let mut errors = errors;
23
24 for new_error in new_errors.into_iter() {
25 errors.push(new_error);
26 }
27
28 errors
29}
30
31pub fn generate_unique_node_name(
37 node_name: impl Into<String>,
38 validation_context: Rc<RefCell<ValidationContext>>,
39) -> String {
40 let mut context = validation_context
41 .try_borrow_mut()
42 .expect(&format!("{BORROWABLE}, {THIS_IS_A_BUG}"));
43
44 generate_unique_node_name_from_names(node_name, &mut context.used_nodes_names)
45}
46
47pub const NODE_NAME_MAX_LEN: usize = 63;
51
52const NODE_NAME_PREFIX: &str = "node-";
55
56pub fn sanitize_node_name(name: &str) -> String {
70 if name.is_empty() {
71 return String::new();
72 }
73
74 let mapped: String = name
75 .to_lowercase()
76 .chars()
77 .map(|c| {
78 if c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-' {
79 c
80 } else {
81 '-'
82 }
83 })
84 .collect();
85
86 let trimmed = mapped.trim_matches('-');
87 let mut sanitized = if trimmed.starts_with(|c: char| c.is_ascii_lowercase()) {
88 trimmed.to_string()
89 } else {
90 format!("{NODE_NAME_PREFIX}{trimmed}")
91 };
92
93 sanitized.truncate(NODE_NAME_MAX_LEN);
95 sanitized.trim_end_matches('-').to_string()
96}
97
98fn with_counter(base: &str, counter: usize) -> String {
102 let suffix = format!("-{counter}");
103 let keep = NODE_NAME_MAX_LEN
104 .saturating_sub(suffix.len())
105 .min(base.len());
106 format!("{}{suffix}", base[..keep].trim_end_matches('-'))
107}
108
109pub fn generate_unique_node_name_from_names(
119 node_name: impl Into<String>,
120 names: &mut HashSet<String>,
121) -> String {
122 let original = node_name.into();
123 let node_name = sanitize_node_name(&original);
124 if node_name != original {
125 warn!(
126 original = %original,
127 adjusted = %node_name,
128 "Node name is not valid on every provider (lowercase letters, digits and '-', \
129 starting with a letter, at most {NODE_NAME_MAX_LEN} characters); using the adjusted name."
130 );
131 }
132
133 if names.insert(node_name.clone()) {
134 return node_name;
135 }
136
137 let mut counter = 1;
138 let mut candidate = node_name.clone();
139 while names.contains(&candidate) {
140 candidate = with_counter(&node_name, counter);
141 counter += 1;
142 }
143
144 warn!(
145 original = %node_name,
146 adjusted = %candidate,
147 "Duplicate node name detected."
148 );
149
150 names.insert(candidate.clone());
151 candidate
152}
153
154pub fn ensure_value_is_not_empty(value: &str) -> Result<(), anyhow::Error> {
155 if value.is_empty() {
156 Err(ValidationError::CantBeEmpty().into())
157 } else {
158 Ok(())
159 }
160}
161
162pub fn ensure_port_unique(
163 port: Port,
164 validation_context: Rc<RefCell<ValidationContext>>,
165) -> Result<(), anyhow::Error> {
166 let mut context = validation_context
167 .try_borrow_mut()
168 .expect(&format!("{BORROWABLE}, {THIS_IS_A_BUG}"));
169
170 if !context.used_ports.contains(&port) {
171 context.used_ports.push(port);
172 return Ok(());
173 }
174
175 Err(ValidationError::PortAlreadyUsed(port).into())
176}
177
178pub fn generate_unique_para_id(
179 para_id: ParaId,
180 validation_context: Rc<RefCell<ValidationContext>>,
181) -> String {
182 let mut context = validation_context
183 .try_borrow_mut()
184 .expect(&format!("{BORROWABLE}, {THIS_IS_A_BUG}"));
185
186 if let Some(suffix) = context.used_para_ids.get_mut(¶_id) {
187 *suffix += 1;
188 format!("{para_id}-{suffix}")
189 } else {
190 context.used_para_ids.insert(para_id, 0);
192 para_id.to_string()
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 fn is_rfc1035_label(name: &str) -> bool {
202 let bytes = name.as_bytes();
203 !name.is_empty()
204 && name.len() <= NODE_NAME_MAX_LEN
205 && bytes[0].is_ascii_lowercase()
206 && bytes[bytes.len() - 1].is_ascii_alphanumeric()
207 && bytes
208 .iter()
209 .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || *b == b'-')
210 }
211
212 #[test]
213 fn a_valid_name_is_kept_as_is() {
214 for name in ["alice", "bob-1", "collator-1000", "a"] {
215 assert_eq!(sanitize_node_name(name), name);
216 }
217 }
218
219 #[test]
220 fn an_invalid_name_is_made_a_valid_one() {
221 for (name, expected) in [
222 ("Alice", "alice"),
223 ("Collator-1000", "collator-1000"),
224 ("myNode", "mynode"),
225 ("my_node.1", "my-node-1"),
226 ("my node", "my-node"),
227 ("-alice-", "alice"),
228 ("1st", "node-1st"),
229 ("_", "node"),
230 ("Élan", "lan"),
231 ] {
232 let sanitized = sanitize_node_name(name);
233 assert_eq!(sanitized, expected, "{name}");
234 assert!(is_rfc1035_label(&sanitized), "{name} -> {sanitized}");
235 }
236 }
237
238 #[test]
239 fn a_long_name_is_cut_to_the_limit() {
240 let long = format!("a{}", "-b".repeat(40));
241 let sanitized = sanitize_node_name(&long);
242 assert!(sanitized.len() <= NODE_NAME_MAX_LEN, "{sanitized}");
243 assert!(is_rfc1035_label(&sanitized), "{sanitized}");
244 }
245
246 #[test]
247 fn sanitizing_twice_changes_nothing() {
248 for name in ["Alice", "my_node.1", "1st", "Élan", &"x".repeat(80)] {
249 let once = sanitize_node_name(name);
250 assert_eq!(sanitize_node_name(&once), once, "{name}");
251 }
252 }
253
254 #[test]
255 fn an_empty_name_stays_empty_to_be_reported() {
256 assert_eq!(sanitize_node_name(""), "");
257 }
258
259 #[test]
260 fn names_that_only_differ_in_case_are_still_unique() {
261 let mut names = HashSet::new();
262 assert_eq!(
263 generate_unique_node_name_from_names("Alice", &mut names),
264 "alice"
265 );
266 assert_eq!(
267 generate_unique_node_name_from_names("alice", &mut names),
268 "alice-1"
269 );
270 }
271
272 #[test]
273 fn a_counter_suffix_keeps_a_long_name_within_the_limit() {
274 let mut names = HashSet::new();
275 let long = "n".repeat(NODE_NAME_MAX_LEN);
276 assert_eq!(
277 generate_unique_node_name_from_names(&long, &mut names),
278 long
279 );
280 let second = generate_unique_node_name_from_names(&long, &mut names);
281 assert!(is_rfc1035_label(&second), "{second}");
282 assert!(second.ends_with("-1"), "{second}");
283 }
284}