use regex::Regex;
use std::fs;
use std::io::{BufRead, BufReader};
#[derive(Debug, Clone)]
pub(crate) struct PortConfigEntry {
pub(crate) device_id: Option<String>,
pub(crate) min_port: i32,
pub(crate) max_port: i32,
}
pub(crate) struct PortConfig {
entries: Vec<PortConfigEntry>,
re: Regex,
}
impl PortConfig {
pub(crate) fn new() -> Self {
let re = Regex::new(
r"(?i)^[ \t]*(([a-fA-F0-9-]{25,}|\*|null)[ \t]*:?|:)[ \t]*(-?[0-9]+)?([ \t]*-[ \t]*([0-9]+))?[ \t]*$"
).expect("Regex compilation failed");
PortConfig {
entries: Vec::new(),
re,
}
}
pub(crate) fn clear(&mut self) {
self.entries.clear();
}
pub(crate) fn add(&mut self, device_id: Option<String>, min_port: i32, max_port: i32) {
self.entries.push(PortConfigEntry {
device_id,
min_port,
max_port,
});
}
pub(crate) fn add_line(&mut self, line: &str) -> Option<usize> {
let mut curr = 0;
let bytes = line.as_bytes();
let len = bytes.len();
while curr < len {
while curr < len && (bytes[curr] == b' ' || bytes[curr] == b'\t') {
curr += 1;
}
let mut end = curr;
while end < len
&& bytes[end] != 0
&& bytes[end] != b'\n'
&& bytes[end] != b'#'
&& bytes[end] != b','
{
end += 1;
}
if curr < end {
let segment = &line[curr..end];
if let Some(caps) = self.re.captures(segment) {
let device_id = if let Some(m) = caps.get(2) {
let val = m.as_str();
if val.eq_ignore_ascii_case("null") {
None
} else {
Some(val.to_string())
}
} else {
Some("*".to_string())
};
let min_port: i32 = if let Some(m) = caps.get(3) {
m.as_str().parse().unwrap_or(0)
} else {
0 };
let max_port = if let Some(m) = caps.get(5) {
m.as_str().parse().unwrap_or(min_port)
} else {
min_port
};
self.add(device_id, min_port, max_port);
} else {
return Some(curr);
}
}
if end >= len || bytes[end] != b',' {
break;
}
curr = end + 1;
}
None
}
pub(crate) fn add_file(&mut self, filename: &str) -> Result<(), String> {
let file =
fs::File::open(filename).map_err(|e| format!("Cannot open file {}: {}", filename, e))?;
let reader = BufReader::new(file);
for (line_num, line_result) in reader.lines().enumerate() {
let line = line_result.map_err(|e| format!("Failed to read file: {}", e))?;
if let Some(_pos) = self.add_line(&line) {
eprintln!("Ignoring {}:{}: {}", filename, line_num, line);
}
}
Ok(())
}
pub(crate) fn find(&self, device_id: Option<&str>) -> Option<&PortConfigEntry> {
for entry in &self.entries {
match (&entry.device_id, device_id) {
(Some(s), _) if s == "*" => return Some(entry),
(Some(s), Some(did)) if s.eq_ignore_ascii_case(did) => return Some(entry),
(None, None) => return Some(entry),
_ => continue,
}
}
None
}
pub(crate) fn select_port(
&self,
device_id: Option<&str>,
current_port: &mut i32,
min_port: &mut i32,
max_port: &mut i32,
) -> Result<(), ()> {
if let Some(config) = self.find(device_id) {
*min_port = config.min_port;
*max_port = config.max_port;
if *current_port >= 0 && (*current_port < *min_port || *current_port > *max_port) {
*current_port = -1;
}
Ok(())
} else {
*min_port = -1;
*max_port = -1;
*current_port = -1;
Err(())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_config_line() {
let mut pc = PortConfig::new();
assert!(pc.add_line("null:9221,:9222-9322").is_none());
assert_eq!(pc.entries.len(), 2);
assert!(pc.entries[0].device_id.is_none());
assert_eq!(pc.entries[0].min_port, 9221);
assert_eq!(pc.entries[1].device_id, Some("*".to_string()));
assert_eq!(pc.entries[1].min_port, 9222);
assert_eq!(pc.entries[1].max_port, 9322);
}
#[test]
fn test_parse_config_empty_port() {
let mut pc = PortConfig::new();
assert!(pc.add_line("null:9221,:").is_none());
assert_eq!(pc.entries.len(), 2);
assert!(pc.entries[0].device_id.is_none());
assert_eq!(pc.entries[0].min_port, 9221);
assert_eq!(pc.entries[1].device_id, Some("*".to_string()));
assert_eq!(pc.entries[1].min_port, 0);
assert_eq!(pc.entries[1].max_port, 0);
}
}