1use crate::ALL_WORKERS;
2use crate::custom_module::manycastr::{
3 Address, Configuration, MeasurementType, Origin, ProtocolType,
4};
5use crate::custom_module::parse_src_address;
6use bimap::BiHashMap;
7use bzip2::read::BzDecoder;
8use flate2::read::GzDecoder;
9use log::info;
10use rand::prelude::SliceRandom;
11use std::fs::File;
12use std::io::{BufRead, BufReader};
13use std::str::FromStr;
14
15pub fn resolve_workers(token: &str, worker_map: &BiHashMap<u32, String>) -> Vec<u32> {
24 if let Ok(id) = token.parse::<u32>() {
26 return if worker_map.contains_left(&id) {
27 vec![id]
28 } else {
29 Vec::new()
30 };
31 }
32
33 if token.contains('*') {
35 return worker_map
36 .iter()
37 .filter(|(_, hostname)| glob_match(token, hostname))
38 .map(|(id, _)| *id)
39 .collect();
40 }
41
42 worker_map
44 .get_by_right(token)
45 .map(|&id| vec![id])
46 .unwrap_or_default()
47}
48
49#[derive(Clone, Copy, Default, PartialEq)]
51pub struct IpVersions {
52 pub has_v4: bool,
53 pub has_v6: bool,
54}
55
56impl IpVersions {
57 pub fn from_origins(configurations: &[Configuration]) -> Self {
59 let mut versions = IpVersions::default();
60 for src in configurations.iter().filter_map(|c| c.origin?.src) {
61 if src.is_v6() {
62 versions.has_v6 = true;
63 } else {
64 versions.has_v4 = true;
65 }
66 }
67 versions
68 }
69
70 pub fn from_targets(targets: &[Address]) -> Self {
72 let mut versions = IpVersions::default();
73 for addr in targets {
74 if addr.is_v6() {
75 versions.has_v6 = true;
76 } else {
77 versions.has_v4 = true;
78 }
79 }
80 versions
81 }
82
83 pub fn label(&self) -> &'static str {
85 match (self.has_v4, self.has_v6) {
86 (true, true) => "IPv4+IPv6",
87 (false, true) => "IPv6",
88 _ => "IPv4",
89 }
90 }
91
92 pub fn file_token(&self) -> &'static str {
94 match (self.has_v4, self.has_v6) {
95 (true, true) => "mixed",
96 (false, true) => "v6",
97 _ => "v4",
98 }
99 }
100}
101
102pub fn validate_ip_versions(
114 configurations: &[Configuration],
115 hitlist_versions: Option<IpVersions>,
116 m_type: MeasurementType,
117) -> Result<IpVersions, String> {
118 let origin_versions = IpVersions::from_origins(configurations);
119 let versions = hitlist_versions.unwrap_or(origin_versions);
121
122 for (present, has_origin, label) in [
124 (versions.has_v4, origin_versions.has_v4, "IPv4"),
125 (versions.has_v6, origin_versions.has_v6, "IPv6"),
126 ] {
127 if present && !has_origin {
128 return Err(format!(
129 "The hitlist contains {label} targets but no {label} origin is configured."
130 ));
131 }
132 }
133
134 if m_type == MeasurementType::Tracemap && versions.has_v4 && versions.has_v6 {
136 return Err(
137 "tracemap does not support a mixed IPv4/IPv6 hitlist (tasks use a single origin)."
138 .to_string(),
139 );
140 }
141
142 Ok(versions)
143}
144
145fn glob_match(pattern: &str, text: &str) -> bool {
147 let parts: Vec<&str> = pattern.split('*').collect();
148 if parts.len() == 1 {
149 return pattern == text; }
151
152 let mut pos = 0;
153 if !parts[0].is_empty() {
155 if !text.starts_with(parts[0]) {
156 return false;
157 }
158 pos = parts[0].len();
159 }
160 for part in &parts[1..parts.len() - 1] {
162 match text[pos..].find(part) {
163 Some(i) => pos += i + part.len(),
164 None => return false,
165 }
166 }
167 let last = parts[parts.len() - 1];
169 text.len() >= pos + last.len() && text[pos..].ends_with(last)
170}
171
172pub fn get_hitlist(
189 hitlist_path: &str,
190 is_shuffle: bool,
191 is_responsive: bool,
192) -> (Vec<Address>, IpVersions, bool) {
193 let file =
194 File::open(hitlist_path).unwrap_or_else(|_| panic!("Unable to open file {hitlist_path}"));
195
196 let reader: Box<dyn BufRead> = if hitlist_path.ends_with(".gz") {
198 let decoder = GzDecoder::new(file);
199 Box::new(BufReader::new(decoder))
200 } else if hitlist_path.ends_with(".bz2") {
201 let decoder = BzDecoder::new(file);
202 Box::new(BufReader::new(decoder))
203 } else {
204 Box::new(BufReader::new(file))
205 };
206
207 let mut lines = reader.lines().map_while(Result::ok).peekable();
208
209 if lines.peek().is_some_and(|l| l.starts_with("#fsdb")) {
210 let ranked: Vec<Vec<Address>> = lines
212 .filter(|l| !l.is_empty() && !l.starts_with('#'))
213 .filter_map(|l| parse_isi_row(&l))
214 .filter(|candidates| !candidates.is_empty())
215 .collect();
216
217 if !is_responsive {
218 let ips = ranked.into_iter().flatten().collect();
220 let (ips, versions) = finalize_hitlist(ips, is_shuffle);
221 return (ips, versions, false);
222 }
223
224 let max_rank = ranked.iter().map(Vec::len).max().unwrap_or(0);
226 let mut ips = Vec::with_capacity(ranked.iter().map(Vec::len).sum());
227 for rank in 0..max_rank {
228 let start = ips.len();
229 ips.extend(ranked.iter().filter_map(|c| c.get(rank).copied()));
230 if is_shuffle {
232 ips[start..].shuffle(&mut rand::rng());
233 }
234 }
235 let (ips, versions) = finalize_hitlist(ips, false);
236 return (ips, versions, true);
237 }
238
239 let ips: Vec<Address> = lines .filter(|l| !l.trim().is_empty()) .map(Address::from)
242 .collect();
243
244 let (ips, versions) = finalize_hitlist(ips, is_shuffle);
245 (ips, versions, false)
246}
247
248fn parse_isi_row(line: &str) -> Option<Vec<Address>> {
256 let (block, suffixes) = line.split_once('\t')?;
257 let block = block.trim();
258 match block.len() {
259 8 => {
261 let base = u32::from_str_radix(block, 16).ok()?;
262 Some(
263 suffixes
264 .split(',')
265 .filter_map(|s| u8::from_str_radix(s.trim(), 16).ok())
266 .map(|s| Address::from(base | s as u32))
267 .collect(),
268 )
269 }
270 12 => {
272 let base = (u64::from_str_radix(block, 16).ok()? as u128) << 80;
273 Some(
274 suffixes
275 .split(',')
276 .filter_map(|s| u128::from_str_radix(s.trim(), 16).ok())
277 .filter(|s| s >> 80 == 0)
278 .map(|s| Address::from(base | s))
279 .collect(),
280 )
281 }
282 _ => None,
283 }
284}
285
286pub fn get_targets(targets: &str, is_shuffle: bool) -> (Vec<Address>, IpVersions) {
297 let ips: Vec<Address> = targets
298 .split(',')
299 .map(str::trim)
300 .filter(|t| !t.is_empty())
301 .map(Address::from)
302 .collect();
303
304 finalize_hitlist(ips, is_shuffle)
305}
306
307fn finalize_hitlist(mut ips: Vec<Address>, is_shuffle: bool) -> (Vec<Address>, IpVersions) {
314 if ips.is_empty() {
315 panic!("No target addresses provided (empty hitlist / target list)");
316 }
317
318 let versions = IpVersions::from_targets(&ips);
319
320 if is_shuffle {
322 ips.as_mut_slice().shuffle(&mut rand::rng());
323 }
324 (ips, versions)
325}
326
327pub fn parse_configurations(
341 conf_file: &str,
342 worker_map: &BiHashMap<u32, String>,
343) -> Vec<Configuration> {
344 info!("[CLI] Using configuration file: {conf_file}");
345 let file = File::open(conf_file)
346 .unwrap_or_else(|_| panic!("Unable to open configuration file {conf_file}"));
347 let buf_reader = BufReader::new(file);
348 let mut origin_id = 0;
349 let mut configurations: Vec<Configuration> = Vec::new();
350
351 for line in buf_reader.lines() {
352 let line = line.expect("Unable to read configuration line");
353 let line = line.trim();
354 if line.is_empty() || line.starts_with("#") {
356 continue;
357 }
358
359 let parts: Vec<&str> = line.split(",").map(|s| s.trim()).collect();
361 if parts.len() != 5 {
362 panic!("Invalid configuration format: {line}");
363 }
364
365 let worker_ids = if parts[0] == "ALL" {
367 vec![ALL_WORKERS]
368 } else {
369 let ids = resolve_workers(parts[0], worker_map);
370 if ids.is_empty() {
371 panic!(
372 "'{}' did not match any known worker ID or hostname.",
373 parts[0]
374 );
375 }
376 ids
377 };
378
379 let src = parse_src_address(parts[1]);
381
382 let sport = u16::from_str(parts[2]).expect("Unable to parse src port") as u32;
384 let dport = u16::from_str(parts[3]).expect("Unable to parse dst port") as u32;
385 let p_type = ProtocolType::from_str(parts[4]).expect("Unable to parse protocol type");
386 origin_id += 1;
388
389 for worker_id in worker_ids {
390 configurations.push(Configuration {
391 worker_id,
392 origin: Some(Origin {
393 src: Some(src),
394 sport,
395 dport,
396 origin_id,
397 p_type: p_type as i32,
398 }),
399 });
400 }
401 }
402 if configurations.is_empty() {
403 panic!("No valid configurations found in file {conf_file}");
404 }
405
406 configurations
407}