diff --git a/Cargo.lock b/Cargo.lock index c344e0911..897f862f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -266,6 +266,21 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bitflags" version = "2.8.0" @@ -1682,6 +1697,7 @@ dependencies = [ "anstream", "anyhow", "async-std", + "bit-set", "cidr-utils", "clap", "colored", diff --git a/Cargo.toml b/Cargo.toml index c6d47183e..7c76ffa81 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ env_logger = "0.11.8" anstream = "=0.6.20" dirs = "6.0.0" gcd = "2.0.1" +bit-set = "0.8" rand = "0.9.2" colorful = "0.3.2" ansi_term = "0.12.1" diff --git a/benches/benchmark_portscan.rs b/benches/benchmark_portscan.rs index d7c989b86..10e794cbd 100644 --- a/benches/benchmark_portscan.rs +++ b/benches/benchmark_portscan.rs @@ -1,6 +1,6 @@ use async_std::task::block_on; use criterion::{criterion_group, criterion_main, Criterion}; -use rustscan::input::{Opts, PortRange, ScanOrder}; +use rustscan::input::{Opts, PortRanges, ScanOrder}; use rustscan::port_strategy::PortStrategy; use rustscan::scanner::Scanner; use std::hint::black_box; @@ -20,11 +20,8 @@ fn bench_address() { } fn bench_port_strategy() { - let range = PortRange { - start: 1, - end: 1_000, - }; - let _strategy = PortStrategy::pick(&Some(range.clone()), None, ScanOrder::Serial); + let range = PortRanges(vec![(1, 1_000)]); + let _strategy = PortStrategy::pick(Some(range.clone()), None, ScanOrder::Serial); } fn bench_address_parsing() { @@ -47,12 +44,9 @@ fn bench_address_parsing() { fn criterion_benchmark(c: &mut Criterion) { let addrs = vec!["127.0.0.1".parse::().unwrap()]; - let range = PortRange { - start: 1, - end: 1_000, - }; - let strategy_tcp = PortStrategy::pick(&Some(range.clone()), None, ScanOrder::Serial); - let strategy_udp = PortStrategy::pick(&Some(range.clone()), None, ScanOrder::Serial); + let range = PortRanges(vec![(1, 1_000)]); + let strategy_tcp = PortStrategy::pick(Some(range.clone()), None, ScanOrder::Serial); + let strategy_udp = PortStrategy::pick(Some(range.clone()), None, ScanOrder::Serial); let scanner_tcp = Scanner::new( &addrs, diff --git a/src/input.rs b/src/input.rs index 8a08845af..ded9eacb2 100644 --- a/src/input.rs +++ b/src/input.rs @@ -27,35 +27,52 @@ pub enum ScriptsRequired { Custom, } -/// Represents the range of ports to be scanned. +/// Represents the ranges of ports to be scanned Vec<(start:u16, end: u16)> #[derive(Deserialize, Debug, Clone, PartialEq, Eq)] -pub struct PortRange { - pub start: u16, - pub end: u16, -} +pub struct PortRanges(pub Vec<(u16, u16)>); #[cfg(not(tarpaulin_include))] -fn parse_range(input: &str) -> Result { - let range = input - .split('-') - .map(str::parse) - .collect::, std::num::ParseIntError>>(); - - if range.is_err() { - return Err(String::from( - "the range format must be 'start-end'. Example: 1-1000.", - )); +/// Parse a single `start-end` token (e.g. "100-200") into `(start, end)`. +/// Returns `None` when the token is malformed, cannot be parsed as `u16`, +/// or `start > end`. +fn parse_range(input: &str) -> Option<(u16, u16)> { + let mut parts = input.trim().splitn(2, '-').map(str::trim); + let a = parts.next()?; + let b = parts.next()?; + + let start = a.parse::().ok()?; + let end = b.parse::().ok()?; + + if start <= end { + Some((start, end)) + } else { + None } - - match range.unwrap().as_slice() { - [start, end] => Ok(PortRange { - start: *start, - end: *end, - }), - _ => Err(String::from( - "the range format must be 'start-end'. Example: 1-1000.", - )), +} +#[cfg(not(tarpaulin_include))] +/// Parse a comma-separated list of `start-end` ranges into `PortRanges`. +/// +/// Errors with a helpful message identifying the bad token. +fn parse_ranges(input: &str) -> Result { + let s = input.trim(); + if s.is_empty() { + return Err("empty input: expected one or more comma-separated 'start-end' pairs".into()); } + + let ranges_res: Result, String> = s + .split(',') + .map(|token| { + let t = token.trim(); + parse_range(t).ok_or_else(|| { + format!( + "invalid range token `{}` — expected `start-end` with 0 <= start <= end <= 65535", + t + ) + }) + }) + .collect(); + + ranges_res.map(PortRanges) } #[derive(Parser, Debug, Clone)] @@ -80,9 +97,9 @@ pub struct Opts { #[arg(short, long, value_delimiter = ',')] pub ports: Option>, - /// A range of ports with format start-end. Example: 1-1000. - #[arg(short, long, conflicts_with = "ports", value_parser = parse_range)] - pub range: Option, + /// Ranges of ports with comma seperated start-end pairs. Example: 1-500,1000-2500,4000-7000 + #[arg(short, long, conflicts_with = "ports", value_parser = parse_ranges)] + pub range: Option, /// Whether to ignore the configuration file or not. #[arg(short, long)] @@ -169,10 +186,7 @@ impl Opts { let mut opts = Opts::parse(); if opts.ports.is_none() && opts.range.is_none() { - opts.range = Some(PortRange { - start: LOWEST_PORT_NUMBER, - end: TOP_PORT_NUMBER, - }); + opts.range = Some(PortRanges(vec![(LOWEST_PORT_NUMBER, TOP_PORT_NUMBER)])); } opts @@ -259,7 +273,7 @@ impl Default for Opts { pub struct Config { addresses: Option>, ports: Option>, - range: Option, + range: Option, greppable: Option, accessible: Option, batch_size: Option, @@ -344,7 +358,7 @@ mod tests { use clap::{CommandFactory, Parser}; use parameterized::parameterized; - use super::{Config, Opts, PortRange, ScanOrder, ScriptsRequired}; + use super::{Config, Opts, PortRanges, ScanOrder, ScriptsRequired}; impl Config { fn default() -> Self { @@ -430,10 +444,7 @@ mod tests { fn opts_merge_optional_arguments() { let mut opts = Opts::default(); let mut config = Config::default(); - config.range = Some(PortRange { - start: 1, - end: 1_000, - }); + config.range = Some(PortRanges(vec![(1, 1_000)])); config.ulimit = Some(1_000); config.resolver = Some("1.1.1.1".to_owned()); diff --git a/src/lib.rs b/src/lib.rs index e335de58a..59489a595 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,17 +11,14 @@ //! use async_std::task::block_on; //! use std::{net::IpAddr, time::Duration}; //! -//! use rustscan::input::{PortRange, ScanOrder}; +//! use rustscan::input::{PortRanges, ScanOrder}; //! use rustscan::port_strategy::PortStrategy; //! use rustscan::scanner::Scanner; //! //! fn main() { //! let addrs = vec!["127.0.0.1".parse::().unwrap()]; -//! let range = PortRange { -//! start: 1, -//! end: 1_000, -//! }; -//! let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Random); // can be serial, random or manual https://github.com/RustScan/RustScan/blob/master/src/port_strategy/mod.rs +//! let range = PortRanges(vec![(1, 1_000)]); +//! let strategy = PortStrategy::pick(Some(range), None, ScanOrder::Random); // can be serial, random or manual https://github.com/RustScan/RustScan/blob/master/src/port_strategy/mod.rs //! let scanner = Scanner::new( //! &addrs, // the addresses to scan //! 10, // batch_size is how many ports at a time should be scanned diff --git a/src/main.rs b/src/main.rs index 08e6eaf80..9b9ba289d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -89,7 +89,7 @@ fn main() { Duration::from_millis(opts.timeout.into()), opts.tries, opts.greppable, - PortStrategy::pick(&opts.range, opts.ports, opts.scan_order), + PortStrategy::pick(opts.range, opts.ports, opts.scan_order), opts.accessible, opts.exclude_ports.unwrap_or_default(), opts.udp, diff --git a/src/port_strategy/mod.rs b/src/port_strategy/mod.rs index cf989d8cb..bd19a882f 100644 --- a/src/port_strategy/mod.rs +++ b/src/port_strategy/mod.rs @@ -1,6 +1,6 @@ //! Provides a means to hold configuration options specifically for port scanning. mod range_iterator; -use crate::input::{PortRange, ScanOrder}; +use crate::input::{PortRanges, ScanOrder}; use rand::rng; use rand::seq::SliceRandom; use range_iterator::RangeIterator; @@ -17,20 +17,18 @@ pub enum PortStrategy { } impl PortStrategy { - pub fn pick(range: &Option, ports: Option>, order: ScanOrder) -> Self { + pub fn pick(range: Option, ports: Option>, order: ScanOrder) -> Self { match order { ScanOrder::Serial if ports.is_none() => { - let range = range.as_ref().unwrap(); + let port_ranges = range.unwrap(); PortStrategy::Serial(SerialRange { - start: range.start, - end: range.end, + range: port_ranges.0, }) } ScanOrder::Random if ports.is_none() => { - let range = range.as_ref().unwrap(); + let port_ranges = range.unwrap(); PortStrategy::Random(RandomRange { - start: range.start, - end: range.end, + range: port_ranges.0, }) } ScanOrder::Serial => PortStrategy::Manual(ports.unwrap()), @@ -62,13 +60,12 @@ trait RangeOrder { /// ascending order. #[derive(Debug)] pub struct SerialRange { - start: u16, - end: u16, + range: Vec<(u16, u16)>, } impl RangeOrder for SerialRange { fn generate(&self) -> Vec { - (self.start..=self.end).collect() + RangeIterator::new_serial(&self.range).collect() } } @@ -76,8 +73,7 @@ impl RangeOrder for SerialRange { /// a random order. This vector is built following the LCG algorithm. #[derive(Debug)] pub struct RandomRange { - start: u16, - end: u16, + range: Vec<(u16, u16)>, } impl RangeOrder for RandomRange { @@ -91,50 +87,64 @@ impl RangeOrder for RandomRange { // port numbers close to each other are pretty slim due to the way the // algorithm works. fn generate(&self) -> Vec { - RangeIterator::new(self.start.into(), self.end.into()).collect() + RangeIterator::new_random(&self.range).collect() } } #[cfg(test)] mod tests { use super::PortStrategy; - use crate::input::{PortRange, ScanOrder}; + use crate::input::{PortRanges, ScanOrder}; + use std::collections::HashSet; + fn expected_ports_from_ranges(input: &[(u16, u16)]) -> Vec { + let mut s = HashSet::new(); + for &(start, end) in input { + for p in start..=end { + s.insert(p); + } + } + let mut v: Vec = s.into_iter().collect(); + v.sort_unstable(); + v + } #[test] fn serial_strategy_with_range() { - let range = PortRange { start: 1, end: 100 }; - let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Serial); + let ranges = PortRanges(vec![(1u16, 10u16), (20u16, 30u16), (100u16, 110u16)]); + let strategy = PortStrategy::pick(Some(ranges.clone()), None, ScanOrder::Serial); let result = strategy.order(); - let expected_range = (1..=100).collect::>(); - assert_eq!(expected_range, result); + let expected = expected_ports_from_ranges(&ranges.0); + + assert_eq!(expected, result); } #[test] fn random_strategy_with_range() { - let range = PortRange { start: 1, end: 100 }; - let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Random); + let ranges = PortRanges(vec![(1u16, 10u16), (20u16, 30u16), (100u16, 110u16)]); + let strategy = PortStrategy::pick(Some(ranges.clone()), None, ScanOrder::Random); let mut result = strategy.order(); - let expected_range = (1..=100).collect::>(); - assert_ne!(expected_range, result); + let expected = expected_ports_from_ranges(&ranges.0); + assert_ne!(expected, result); result.sort_unstable(); - assert_eq!(expected_range, result); + + assert_eq!(expected, result); } #[test] fn serial_strategy_with_ports() { - let strategy = PortStrategy::pick(&None, Some(vec![80, 443]), ScanOrder::Serial); + let strategy = PortStrategy::pick(None, Some(vec![80, 443]), ScanOrder::Serial); let result = strategy.order(); assert_eq!(vec![80, 443], result); } #[test] fn random_strategy_with_ports() { - let strategy = PortStrategy::pick(&None, Some((1..10).collect()), ScanOrder::Random); + let strategy = PortStrategy::pick(None, Some((1..10).collect()), ScanOrder::Random); let mut result = strategy.order(); - let expected_range = (1..10).collect::>(); - assert_ne!(expected_range, result); + let expected = (1..10).collect::>(); + assert_ne!(expected, result); result.sort_unstable(); - assert_eq!(expected_range, result); + assert_eq!(expected, result); } } diff --git a/src/port_strategy/range_iterator.rs b/src/port_strategy/range_iterator.rs index f26c2ab4a..fb2abd053 100644 --- a/src/port_strategy/range_iterator.rs +++ b/src/port_strategy/range_iterator.rs @@ -1,71 +1,201 @@ +use bit_set::BitSet; use gcd::Gcd; use rand::Rng; use std::convert::TryInto; pub struct RangeIterator { active: bool, - normalized_end: u32, + total: u32, normalized_first_pick: u32, normalized_pick: u32, - actual_start: u32, step: u32, + ranges: Vec<(u32, u32)>, + prefix: Vec, + serial_itr: Option>>, + serial_itr_bitset: Option, } -/// An iterator that follows the `Linear Congruential Generator` algorithm. +/// Yields ports produced from a collection of (possibly overlapping) +/// inclusive `u16` ranges in two modes: /// -/// For more information: +/// **Randomized** — created by `RangeIterator::new_random`: +/// - The algorithm generates a permutation of indices `0..N-1` using the +/// additive congruential step `x_{i+1} = (x_i + step) % N`. +/// - `step` is chosen so `gcd(step, N) == 1` to ensure the sequence is a +/// full-length cycle (visits each index exactly once). +/// - `x_0` (the seed stored as `normalized_first_pick`) is chosen uniformly +/// in `0..N`. +/// +/// For more information: +/// +/// **Serial** — `RangeIterator::new_serial`: +/// Iterates the input ranges in the **original input order** and yields +/// each port the first time it is encountered. Duplicate ports (from +/// overlapping ranges) are skipped using a small `BitSet` of size 65_536. +/// + impl RangeIterator { - /// Receives the start and end of a range and normalize - /// these values before selecting a coprime for the end of the range - /// which will server as the step for the algorithm. + /// Construct a randomized iterator (LCG permutation). /// - /// For example, the range `1000-2500` will be normalized to `0-1500` - /// before going through the algorithm. - pub fn new(start: u32, end: u32) -> Self { - let normalized_end = end - start + 1; - let step = pick_random_coprime(normalized_end); - - // Randomly choose a number within the range to be the first - // and assign it as a pick. + /// Preconditions: + /// - `input` must contain at least one `(u16,u16)` + /// and each pair must satisfy `start <= end`. + + pub fn new_random(input: &[(u16, u16)]) -> Self { + // normalize & merge into (start, len) u32 pairs + // Example: [(10,12),(11,15)] -> merged [(10,6)] + let mut ranges: Vec<(u32, u32)> = input + .into_iter() + .map(|(s, e)| { + let start = *s as u32; + let end_excl = (*e as u32) + 1; // convert inclusive -> exclusive + (start, end_excl) + }) + .collect(); + + ranges.sort_unstable_by_key(|&(s, _)| s); + + let mut merged: Vec<(u32, u32)> = Vec::with_capacity(ranges.len()); + if !ranges.is_empty() { + let mut iter = ranges.into_iter(); + let (mut cur_s, mut cur_end) = iter.next().unwrap(); // cur_end exclusive + for (s, end_excl) in iter { + if s <= cur_end { + // overlap/adjacent -> extend + if end_excl > cur_end { + cur_end = end_excl; + } + } else { + // push disjoint segment as (start, len) + merged.push((cur_s, cur_end - cur_s)); // len = excl - start + cur_s = s; + cur_end = end_excl; + } + } + merged.push((cur_s, cur_end - cur_s)); + } + // build prefix sums (prefix[0] = 0; prefix.len() == merged.len() + 1) + let prefix = merged.iter().fold(vec![0u32], |mut acc, (_, len)| { + let last = acc.last().unwrap(); + acc.push(last.saturating_add(*len)); + acc + }); + + // total is guaranteed > 0 by precondition (input.len() >= 1) + let total = *prefix.last().unwrap(); + + // pick step and seed + let step = pick_random_coprime(total); let mut rng = rand::rng(); - let normalized_first_pick = rng.random_range(0..normalized_end); + let first = rng.random_range(0..total); Self { active: true, - normalized_end, + total, + normalized_first_pick: first, + normalized_pick: first, step, - normalized_first_pick, - normalized_pick: normalized_first_pick, - actual_start: start, + ranges: merged, + prefix, + serial_itr: None, + serial_itr_bitset: None, } } -} + /// Construct a serial iterator that yields ports in original input order, + /// skipping duplicates. The deduplication is done on the fly with a BitSet. + /// + /// Preconditions: + /// - `input` must contain at least one `(u16,u16)` and each pair must satisfy `start <= end`. + pub fn new_serial(input: &[(u16, u16)]) -> Self { + // Build a serial iterator that yields ports in *input order* (start..=end). + // We keep the merged ranges/prefix empty here (they are not needed for serial mode). + let input = input.to_vec(); + let serial_itr = input.into_iter().flat_map(|(start, end)| start..=end); + + let serial_itr_boxed: Box> = Box::new(serial_itr); + // BitSet needs to be large enough for ports 0..=65535 + let bitset = BitSet::with_capacity(65536); + + Self { + active: true, + total: 0, + normalized_first_pick: 0, + normalized_pick: 0, + step: 0, + ranges: Vec::new(), + prefix: Vec::new(), + serial_itr: Some(serial_itr_boxed), + serial_itr_bitset: Some(bitset), + } + } +} impl Iterator for RangeIterator { type Item = u16; - // The next step is always bound by the formula: N+1 = (N + STEP) % TOP_OF_THE_RANGE - // It will only stop once we generate a number equal to the first generated number. + /// Advance the iterator by one port. + /// + /// 1. Read the current normalized index `cur`. + /// 2. Compute `next = (cur + step) % total` and update `normalized_pick`. + /// 3. If `next == normalized_first_pick` mark `active = false` (we completed the cycle). + /// 4. Map the returned index `cur` into the merged ranges via the prefix array: + /// - find the range index `idx` where `prefix[idx] <= cur < prefix[idx+1]`, + /// - offset = `cur - prefix[idx]`, + /// - port = `ranges[idx].0 + offset`. + /// 5. Return `port as u16`. + /// fn next(&mut self) -> Option { if !self.active { return None; } - let current_pick = self.normalized_pick; - let next_pick = (current_pick + self.step) % self.normalized_end; + // SERIAL iterator fast-path: preserve original input order but skip duplicates. + if let (Some(it), Some(bitset)) = + (self.serial_itr.as_mut(), self.serial_itr_bitset.as_mut()) + { + while let Some(p) = it.next() { + // `insert` returns true when the value was NOT present before. + if bitset.insert(p as usize) { + return Some(p); + } + // otherwise skip duplicate and continue + } + // serial iterator exhausted: drop it and mark inactive + self.serial_itr = None; + self.serial_itr_bitset = None; + self.active = false; + return None; + } - // If the next pick is equal to the first pick this means that - // we have iterated through the entire range. - if next_pick == self.normalized_first_pick { + // RANDOMIZED (LCG) path + let cur = self.normalized_pick; + let next = (cur + self.step) % self.total; + + // if next equals the original seed we finished the cycle after returning cur + if next == self.normalized_first_pick { self.active = false; } - self.normalized_pick = next_pick; - Some( - (self.actual_start + current_pick) - .try_into() - .expect("Could not convert u32 to u16"), - ) + self.normalized_pick = next; + + // Map cur -> port using prefix + ranges (binary search) + let mut lo: usize = 0; + let mut hi: usize = self.ranges.len(); + while lo < hi { + let mid = (lo + hi) / 2; + if self.prefix[mid + 1] > cur { + hi = mid; + } else { + lo = mid + 1; + } + } + let idx = lo; + let offset = cur - self.prefix[idx]; + let (start, _len) = self.ranges[idx]; + let port = (start + offset) + .try_into() + .expect("Could not convert u32 to u16"); + Some(port) } } @@ -98,36 +228,105 @@ fn pick_random_coprime(end: u32) -> u32 { #[cfg(test)] mod tests { - use super::RangeIterator; + use super::*; + use std::collections::HashSet; + + // Helper: collect, sort and return ports produced by randomized RangeIterator + fn generate_sorted_from_ranges_random(input: &[(u16, u16)]) -> Vec { + let mut it = RangeIterator::new_random(input); + let mut v: Vec = it.by_ref().collect(); + v.sort_unstable(); + v + } + + // Helper: collect, sort and return ports produced by serial RangeIterator + fn generate_sorted_from_ranges_serial(input: &[(u16, u16)]) -> Vec { + let mut it = RangeIterator::new_serial(input); + let mut v: Vec = it.by_ref().collect(); + v.sort_unstable(); + v + } + + // Build expected sorted unique ports from input ranges (inclusive) + fn expected_ports_from_ranges(input: &[(u16, u16)]) -> Vec { + let mut s = HashSet::new(); + for &(start, end) in input { + for p in start..=end { + s.insert(p); + } + } + let mut v: Vec = s.into_iter().collect(); + v.sort_unstable(); + v + } #[test] - fn range_iterator_iterates_through_the_entire_range() { - let result = generate_sorted_range(1, 10); - let expected_range = (1..=10).collect::>(); - assert_eq!(expected_range, result); - - let result = generate_sorted_range(1, 100); - let expected_range = (1..=100).collect::>(); - assert_eq!(expected_range, result); - - let result = generate_sorted_range(1, 1000); - let expected_range = (1..=1000).collect::>(); - assert_eq!(expected_range, result); - - let result = generate_sorted_range(1, 65_535); - let expected_range = (1..=65_535).collect::>(); - assert_eq!(expected_range, result); - - let result = generate_sorted_range(1000, 2000); - let expected_range = (1000..=2000).collect::>(); - assert_eq!(expected_range, result); + fn random_range_iterator_test() { + // small disjoint ranges + let input = &[(1u16, 10u16), (20u16, 30u16), (100u16, 110u16)]; + let result = generate_sorted_from_ranges_random(input); + let expected = expected_ports_from_ranges(input); + assert_eq!(expected, result); + + // larger disjoint ranges + let input = &[(1u16, 100u16), (200u16, 500u16)]; + let result = generate_sorted_from_ranges_random(input); + let expected = expected_ports_from_ranges(input); + assert_eq!(expected, result); + + // overlapping and adjacent + let input = &[(10u16, 20u16), (15u16, 25u16), (26u16, 30u16)]; + let result = generate_sorted_from_ranges_random(input); + let expected = expected_ports_from_ranges(input); + assert_eq!(expected, result); + + // near-full domain (heavy): we only assert lengths & equality + let input = &[(1u16, 65_535u16)]; + let result = generate_sorted_from_ranges_random(input); + let expected = expected_ports_from_ranges(input); + assert_eq!(expected.len(), result.len()); + assert_eq!(expected, result); + + // multiple disjoint ranges - check dedupe & coverage + let input = &[(50u16, 100u16), (1000u16, 2000u16), (30000u16, 30010u16)]; + let result = generate_sorted_from_ranges_random(input); + let set_len = result.iter().copied().collect::>().len(); + assert_eq!(set_len, result.len()); + let expected = expected_ports_from_ranges(input); + assert_eq!(expected, result); } - fn generate_sorted_range(start: u32, end: u32) -> Vec { - let range = RangeIterator::new(start, end); - let mut result = range.into_iter().collect::>(); - result.sort_unstable(); + #[test] + fn serial_range_iterator_test() { + // serial should preserve input-order semantics but here we only assert + // coverage (no duplicates) by sorting results and comparing expected set. + + // small disjoint ranges + let input = &[(1u16, 10u16), (20u16, 30u16), (100u16, 110u16)]; + let result = generate_sorted_from_ranges_serial(input); + let expected = expected_ports_from_ranges(input); + assert_eq!(expected, result); + + // overlapping and adjacent + let input = &[(10u16, 20u16), (15u16, 25u16), (26u16, 30u16)]; + let result = generate_sorted_from_ranges_serial(input); + let expected = expected_ports_from_ranges(input); + assert_eq!(expected, result); + + // multiple disjoint ranges + let input = &[(50u16, 100u16), (1000u16, 2000u16), (30000u16, 30010u16)]; + let result = generate_sorted_from_ranges_serial(input); + let set_len = result.iter().copied().collect::>().len(); + assert_eq!(set_len, result.len()); + let expected = expected_ports_from_ranges(input); + assert_eq!(expected, result); - result + // all possible inputs + let input = &[(u16::MIN, u16::MAX)]; + let result = generate_sorted_from_ranges_serial(input); + let set_len = result.iter().copied().collect::>().len(); + assert_eq!(set_len, result.len()); + let expected = expected_ports_from_ranges(input); + assert_eq!(expected, result); } } diff --git a/src/scanner/mod.rs b/src/scanner/mod.rs index 760783479..736853746 100644 --- a/src/scanner/mod.rs +++ b/src/scanner/mod.rs @@ -309,7 +309,7 @@ impl Scanner { #[cfg(test)] mod tests { use super::*; - use crate::input::{PortRange, ScanOrder}; + use crate::input::{PortRanges, ScanOrder}; use async_std::task::block_on; use std::{net::IpAddr, time::Duration}; @@ -317,11 +317,8 @@ mod tests { fn scanner_runs() { // Makes sure the program still runs and doesn't panic let addrs = vec!["127.0.0.1".parse::().unwrap()]; - let range = PortRange { - start: 1, - end: 1_000, - }; - let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Random); + let range = PortRanges(vec![(1, 1_000)]); + let strategy = PortStrategy::pick(Some(range), None, ScanOrder::Random); let scanner = Scanner::new( &addrs, 10, @@ -341,11 +338,8 @@ mod tests { fn ipv6_scanner_runs() { // Makes sure the program still runs and doesn't panic let addrs = vec!["::1".parse::().unwrap()]; - let range = PortRange { - start: 1, - end: 1_000, - }; - let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Random); + let range = PortRanges(vec![(1, 1_000)]); + let strategy = PortStrategy::pick(Some(range), None, ScanOrder::Random); let scanner = Scanner::new( &addrs, 10, @@ -364,11 +358,8 @@ mod tests { #[test] fn quad_zero_scanner_runs() { let addrs = vec!["0.0.0.0".parse::().unwrap()]; - let range = PortRange { - start: 1, - end: 1_000, - }; - let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Random); + let range = PortRanges(vec![(1, 1_000)]); + let strategy = PortStrategy::pick(Some(range), None, ScanOrder::Random); let scanner = Scanner::new( &addrs, 10, @@ -386,11 +377,8 @@ mod tests { #[test] fn google_dns_runs() { let addrs = vec!["8.8.8.8".parse::().unwrap()]; - let range = PortRange { - start: 400, - end: 445, - }; - let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Random); + let range = PortRanges(vec![(400, 445)]); + let strategy = PortStrategy::pick(Some(range), None, ScanOrder::Random); let scanner = Scanner::new( &addrs, 10, @@ -411,11 +399,8 @@ mod tests { let addrs = vec!["8.8.8.8".parse::().unwrap()]; // mac should have this automatically scaled down - let range = PortRange { - start: 400, - end: 600, - }; - let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Random); + let range = PortRanges(vec![(400, 600)]); + let strategy = PortStrategy::pick(Some(range), None, ScanOrder::Random); let scanner = Scanner::new( &addrs, 10, @@ -435,11 +420,8 @@ mod tests { fn udp_scan_runs() { // Makes sure the program still runs and doesn't panic let addrs = vec!["127.0.0.1".parse::().unwrap()]; - let range = PortRange { - start: 1, - end: 1_000, - }; - let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Random); + let range = PortRanges(vec![(1, 1_000)]); + let strategy = PortStrategy::pick(Some(range), None, ScanOrder::Random); let scanner = Scanner::new( &addrs, 10, @@ -459,11 +441,8 @@ mod tests { fn udp_ipv6_runs() { // Makes sure the program still runs and doesn't panic let addrs = vec!["::1".parse::().unwrap()]; - let range = PortRange { - start: 1, - end: 1_000, - }; - let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Random); + let range = PortRanges(vec![(1, 1_000)]); + let strategy = PortStrategy::pick(Some(range), None, ScanOrder::Random); let scanner = Scanner::new( &addrs, 10, @@ -482,11 +461,8 @@ mod tests { #[test] fn udp_quad_zero_scanner_runs() { let addrs = vec!["0.0.0.0".parse::().unwrap()]; - let range = PortRange { - start: 1, - end: 1_000, - }; - let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Random); + let range = PortRanges(vec![(1, 1_000)]); + let strategy = PortStrategy::pick(Some(range), None, ScanOrder::Random); let scanner = Scanner::new( &addrs, 10, @@ -504,11 +480,8 @@ mod tests { #[test] fn udp_google_dns_runs() { let addrs = vec!["8.8.8.8".parse::().unwrap()]; - let range = PortRange { - start: 100, - end: 150, - }; - let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Random); + let range = PortRanges(vec![(100, 150)]); + let strategy = PortStrategy::pick(Some(range), None, ScanOrder::Random); let scanner = Scanner::new( &addrs, 10,