use std::collections::HashSet;
use std::fs;
use std::path::Path;
use super::types::WorkerEntry;
pub fn parse_worker_file(path: &Path) -> Result<Vec<WorkerEntry>, String> {
let content = fs::read_to_string(path)
.map_err(|e| format!("Failed to read worker file '{}': {}", path.display(), e))?;
parse_worker_content(&content, path.to_string_lossy().as_ref())
}
pub fn parse_worker_content(content: &str, source: &str) -> Result<Vec<WorkerEntry>, String> {
let mut workers = Vec::new();
let mut seen_hosts = HashSet::new();
for (line_num, line) in content.lines().enumerate() {
let line_num = line_num + 1; let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
let entry = parse_worker_line(trimmed, line_num, source)?;
if seen_hosts.contains(&entry.host) {
return Err(format!(
"{}:{}: Duplicate host '{}' (each host should only appear once)",
source, line_num, entry.host
));
}
seen_hosts.insert(entry.host.clone());
workers.push(entry);
}
if workers.is_empty() {
return Err(format!(
"Worker file '{}' contains no valid entries",
source
));
}
Ok(workers)
}
fn parse_worker_line(line: &str, line_num: usize, source: &str) -> Result<WorkerEntry, String> {
let original = line.to_string();
let (user, host_port) = if let Some(at_pos) = line.find('@') {
let user = &line[..at_pos];
let rest = &line[at_pos + 1..];
if user.is_empty() {
return Err(format!(
"{}:{}: Empty username before '@' in '{}'",
source, line_num, line
));
}
(Some(user.to_string()), rest)
} else {
(None, line)
};
let (host, port) = if host_port.starts_with('[') {
if let Some(bracket_end) = host_port.find(']') {
let ipv6 = &host_port[1..bracket_end];
let rest = &host_port[bracket_end + 1..];
if rest.is_empty() {
(ipv6.to_string(), None)
} else if let Some(port_str) = rest.strip_prefix(':') {
let port: u16 = port_str.parse().map_err(|_| {
format!(
"{}:{}: Invalid port '{}' in '{}'",
source, line_num, port_str, line
)
})?;
(ipv6.to_string(), Some(port))
} else {
return Err(format!(
"{}:{}: Invalid format after IPv6 address in '{}'",
source, line_num, line
));
}
} else {
return Err(format!(
"{}:{}: Unclosed bracket in IPv6 address '{}'",
source, line_num, line
));
}
} else {
if let Some(colon_pos) = host_port.rfind(':') {
let host = &host_port[..colon_pos];
let port_str = &host_port[colon_pos + 1..];
if port_str.chars().all(|c| c.is_ascii_digit()) && !port_str.is_empty() {
let port: u16 = port_str.parse().map_err(|_| {
format!(
"{}:{}: Invalid port '{}' in '{}'",
source, line_num, port_str, line
)
})?;
(host.to_string(), Some(port))
} else {
(host_port.to_string(), None)
}
} else {
(host_port.to_string(), None)
}
};
if host.is_empty() {
return Err(format!(
"{}:{}: Empty hostname in '{}'",
source, line_num, line
));
}
Ok(WorkerEntry {
original,
user,
host,
port,
})
}