use crate::core::models::ip::set::IpSet;
use crate::core::models::port::{PortSet, Protocol};
use std::{net::IpAddr, sync::Arc};
use thiserror::Error;
#[derive(Error, Debug, PartialEq, Eq)]
pub enum TargetError {
#[error("Target collection is in a dirty state; call `canonicalize()` before concurrent reads")]
UncanonicalizedState,
#[error("Target calculation resulted in an integer overflow")]
CapacityOverflow,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Target {
pub ip: IpAddr,
pub port: u16,
pub protocol: Protocol,
}
#[derive(Debug, Clone, Default)]
pub struct TargetSet {
ips: IpSet,
ports: PortSet,
is_canonicalized: bool,
}
impl TargetSet {
pub fn new(ips: IpSet, ports: PortSet) -> Self {
Self {
ips,
ports,
is_canonicalized: false,
}
}
pub fn ips(&self) -> &IpSet {
&self.ips
}
pub fn ports(&self) -> &PortSet {
&self.ports
}
pub fn ip_count(&mut self) -> u128 {
self.ips.len()
}
pub fn port_count(&self) -> usize {
self.ports.len()
}
pub fn canonicalize(&mut self) {
if !self.is_canonicalized {
self.ips.canonicalize();
self.is_canonicalized = true;
}
}
pub fn total_targets(&mut self) -> Result<u128, TargetError> {
self.canonicalize();
let port_len = self.ports.len() as u128;
self.ips
.len()
.checked_mul(port_len)
.ok_or(TargetError::CapacityOverflow)
}
pub fn iter(&mut self) -> impl Iterator<Item = Target> + Send + '_ {
self.canonicalize();
let ports_arc: Arc<[(u16, Protocol)]> = self.ports.to_vec().into();
self.ips.iter().flat_map(move |ip| {
let local_ports = Arc::clone(&ports_arc);
(0..local_ports.len()).map(move |i| Target {
ip,
port: local_ports[i].0,
protocol: local_ports[i].1,
})
})
}
pub fn total_targets_canonical(&self) -> Result<u128, TargetError> {
if !self.is_canonicalized {
return Err(TargetError::UncanonicalizedState);
}
let port_len = self.ports.len() as u128;
self.ips
.len_canonical()
.checked_mul(port_len)
.ok_or(TargetError::CapacityOverflow)
}
pub fn is_empty(&self) -> bool {
self.ips.is_empty() || self.ports.is_empty()
}
}
#[derive(Debug, Clone, Default)]
pub struct TargetMap {
pub units: Vec<TargetSet>,
}
impl TargetMap {
pub fn new() -> Self {
Self::default()
}
pub fn add_unit(&mut self, unit: TargetSet) {
self.units.push(unit);
}
pub fn canonicalize(&mut self) {
for unit in &mut self.units {
unit.canonicalize();
}
}
pub fn gross_targets(&mut self) -> Result<u128, TargetError> {
let mut total: u128 = 0;
for unit in &mut self.units {
let unit_total = unit.total_targets()?;
total = total
.checked_add(unit_total)
.ok_or(TargetError::CapacityOverflow)?;
}
Ok(total)
}
pub fn gross_ips(&mut self) -> Result<u128, TargetError> {
let mut total: u128 = 0;
for unit in &mut self.units {
total = total
.checked_add(unit.ip_count())
.ok_or(TargetError::CapacityOverflow)?;
}
Ok(total)
}
pub fn is_empty(&self) -> bool {
self.units.is_empty() || self.units.iter().all(|u| u.is_empty())
}
pub fn iter(&mut self) -> impl Iterator<Item = Target> + Send + '_ {
self.units.iter_mut().flat_map(|unit| unit.iter())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn mock_ip_set(input: &str) -> IpSet {
input.parse().expect("Valid IP input")
}
fn mock_port_set(input: &str) -> PortSet {
input.parse().expect("Valid Port input")
}
#[test]
fn target_set_lazy_math() {
let mut ts = TargetSet::new(mock_ip_set("192.168.1.0/24"), mock_port_set("80, 443"));
assert_eq!(ts.total_targets().unwrap(), 256 * 2);
}
#[test]
fn thread_safe_reads_require_canonicalization() {
let ts = TargetSet::new(mock_ip_set("192.168.1.0/24"), mock_port_set("80"));
let err = ts.total_targets_canonical().unwrap_err();
assert_eq!(err, TargetError::UncanonicalizedState);
}
#[test]
fn thread_safe_reads_succeed_when_prepared() {
let mut ts = TargetSet::new(mock_ip_set("192.168.1.0/24"), mock_port_set("80"));
ts.canonicalize();
assert_eq!(ts.total_targets_canonical().unwrap(), 256);
}
#[test]
fn target_map_aggregation() {
let mut map = TargetMap::new();
map.add_unit(TargetSet::new(
mock_ip_set("10.0.0.1-10.0.0.5"),
mock_port_set("80,443"),
));
assert_eq!(map.gross_targets().unwrap(), 10);
}
}