11//! Provides a means to read, parse and hold configuration options for scans.
22use clap:: { Parser , ValueEnum } ;
33use serde_derive:: Deserialize ;
4- use std:: collections:: HashMap ;
54use std:: fs;
65use std:: path:: PathBuf ;
76
@@ -28,35 +27,83 @@ pub enum ScriptsRequired {
2827 Custom ,
2928}
3029
31- /// Represents the range of ports to be scanned.
32- #[ derive( Deserialize , Debug , Clone , PartialEq , Eq ) ]
33- pub struct PortRange {
34- pub start : u16 ,
35- pub end : u16 ,
30+ #[ cfg( not( tarpaulin_include) ) ]
31+ pub fn parse_ports_and_ranges ( input : & str ) -> Result < Vec < u16 > , String > {
32+ let mut ports = Vec :: new ( ) ;
33+
34+ for part in input. split ( ',' ) {
35+ let part = part. trim ( ) ;
36+ if part. is_empty ( ) {
37+ continue ;
38+ }
39+
40+ if part. contains ( '-' ) {
41+ let range_ports = parse_port_range ( part) ?;
42+ ports. extend ( range_ports) ;
43+ } else {
44+ let port = parse_single_port ( part) ?;
45+ ports. push ( port) ;
46+ }
47+ }
48+
49+ if ports. is_empty ( ) {
50+ return Err ( "No valid ports or ranges provided" . to_string ( ) ) ;
51+ }
52+
53+ ports. sort_unstable ( ) ;
54+ ports. dedup ( ) ;
55+
56+ Ok ( ports)
3657}
3758
38- #[ cfg( not( tarpaulin_include) ) ]
39- fn parse_range ( input : & str ) -> Result < PortRange , String > {
40- let range = input
41- . split ( '-' )
42- . map ( str:: parse)
43- . collect :: < Result < Vec < u16 > , std:: num:: ParseIntError > > ( ) ;
44-
45- if range. is_err ( ) {
46- return Err ( String :: from (
47- "the range format must be 'start-end'. Example: 1-1000." ,
59+ fn parse_port_range ( range_str : & str ) -> Result < Vec < u16 > , String > {
60+ let range_parts: Vec < & str > = range_str. split ( '-' ) . collect ( ) ;
61+ if range_parts. len ( ) != 2 {
62+ return Err ( format ! (
63+ "Invalid range format '{range_str}'. Expected 'start-end'. Example: 1-1000." ,
64+ ) ) ;
65+ }
66+
67+ let start: u16 = range_parts[ 0 ] . parse ( ) . map_err ( |_| {
68+ format ! (
69+ "Invalid start port '{}' in range '{range_str}'" ,
70+ range_parts[ 0 ]
71+ )
72+ } ) ?;
73+ let end: u16 = range_parts[ 1 ] . parse ( ) . map_err ( |_| {
74+ format ! (
75+ "Invalid end port '{}' in range '{range_str}'" ,
76+ range_parts[ 1 ]
77+ )
78+ } ) ?;
79+
80+ if start > end {
81+ return Err ( format ! (
82+ "Start port {start} is greater than end port {end} in range '{range_str}'" ,
4883 ) ) ;
4984 }
5085
51- match range. unwrap ( ) . as_slice ( ) {
52- [ start, end] => Ok ( PortRange {
53- start : * start,
54- end : * end,
55- } ) ,
56- _ => Err ( String :: from (
57- "the range format must be 'start-end'. Example: 1-1000." ,
58- ) ) ,
86+ if start < LOWEST_PORT_NUMBER {
87+ return Err ( format ! (
88+ "Ports in range '{range_str}' must be between {LOWEST_PORT_NUMBER} and {TOP_PORT_NUMBER}" ,
89+ ) ) ;
90+ }
91+
92+ Ok ( ( start..=end) . collect ( ) )
93+ }
94+
95+ fn parse_single_port ( port_str : & str ) -> Result < u16 , String > {
96+ let port: u16 = port_str
97+ . parse ( )
98+ . map_err ( |_| format ! ( "Invalid port number '{port_str}'" ) ) ?;
99+
100+ if port < LOWEST_PORT_NUMBER {
101+ return Err ( format ! (
102+ "Port {port} must be between {LOWEST_PORT_NUMBER} and {TOP_PORT_NUMBER}" ,
103+ ) ) ;
59104 }
105+
106+ Ok ( port)
60107}
61108
62109#[ derive( Parser , Debug , Clone ) ]
@@ -77,14 +124,10 @@ pub struct Opts {
77124 #[ arg( short, long, value_delimiter = ',' ) ]
78125 pub addresses : Vec < String > ,
79126
80- /// A list of comma separated ports to be scanned. Example : 80,443,8080.
81- #[ arg( short, long, value_delimiter = ',' ) ]
127+ /// A list of ports and/or port ranges to be scanned. Examples : 80,443,8080 or 1-1000 or 1-1000,8080
128+ #[ arg( short, long, alias = "range" , value_parser = parse_ports_and_ranges ) ]
82129 pub ports : Option < Vec < u16 > > ,
83130
84- /// A range of ports with format start-end. Example: 1-1000.
85- #[ arg( short, long, conflicts_with = "ports" , value_parser = parse_range) ]
86- pub range : Option < PortRange > ,
87-
88131 /// Whether to ignore the configuration file or not.
89132 #[ arg( short, long) ]
90133 pub no_config : bool ,
@@ -169,11 +212,8 @@ impl Opts {
169212 pub fn read ( ) -> Self {
170213 let mut opts = Opts :: parse ( ) ;
171214
172- if opts. ports . is_none ( ) && opts. range . is_none ( ) {
173- opts. range = Some ( PortRange {
174- start : LOWEST_PORT_NUMBER ,
175- end : TOP_PORT_NUMBER ,
176- } ) ;
215+ if opts. ports . is_none ( ) {
216+ opts. ports = Some ( ( LOWEST_PORT_NUMBER ..=TOP_PORT_NUMBER ) . collect ( ) ) ;
177217 }
178218
179219 opts
@@ -218,14 +258,10 @@ impl Opts {
218258
219259 // Only use top ports when the user asks for them
220260 if self . top && config. ports . is_some ( ) {
221- let mut ports: Vec < u16 > = Vec :: with_capacity ( config. ports . as_ref ( ) . unwrap ( ) . len ( ) ) ;
222- for entry in config. ports . as_ref ( ) . unwrap ( ) . keys ( ) {
223- ports. push ( entry. parse ( ) . unwrap ( ) ) ;
224- }
225- self . ports = Some ( ports) ;
261+ self . ports = config. ports . clone ( ) ;
226262 }
227263
228- merge_optional ! ( range , resolver, ulimit, exclude_ports, exclude_addresses) ;
264+ merge_optional ! ( resolver, ulimit, exclude_ports, exclude_addresses) ;
229265 }
230266}
231267
@@ -234,7 +270,6 @@ impl Default for Opts {
234270 Self {
235271 addresses : vec ! [ ] ,
236272 ports : None ,
237- range : None ,
238273 greppable : true ,
239274 batch_size : 0 ,
240275 timeout : 0 ,
@@ -263,8 +298,7 @@ impl Default for Opts {
263298#[ derive( Debug , Deserialize ) ]
264299pub struct Config {
265300 addresses : Option < Vec < String > > ,
266- ports : Option < HashMap < String , u16 > > ,
267- range : Option < PortRange > ,
301+ ports : Option < Vec < u16 > > ,
268302 greppable : Option < bool > ,
269303 accessible : Option < bool > ,
270304 batch_size : Option < u16 > ,
@@ -332,14 +366,13 @@ mod tests {
332366 use clap:: { CommandFactory , Parser } ;
333367 use parameterized:: parameterized;
334368
335- use super :: { Config , Opts , PortRange , ScanOrder , ScriptsRequired } ;
369+ use super :: { parse_ports_and_ranges , Config , Opts , ScanOrder , ScriptsRequired } ;
336370
337371 impl Config {
338372 fn default ( ) -> Self {
339373 Self {
340374 addresses : Some ( vec ! [ "127.0.0.1" . to_owned( ) ] ) ,
341375 ports : None ,
342- range : None ,
343376 greppable : Some ( true ) ,
344377 batch_size : Some ( 25_000 ) ,
345378 timeout : Some ( 1_000 ) ,
@@ -417,17 +450,129 @@ mod tests {
417450 fn opts_merge_optional_arguments ( ) {
418451 let mut opts = Opts :: default ( ) ;
419452 let mut config = Config :: default ( ) ;
420- config. range = Some ( PortRange {
421- start : 1 ,
422- end : 1_000 ,
423- } ) ;
453+ config. ports = Some ( ( 1 ..=1000 ) . collect :: < Vec < u16 > > ( ) ) ;
424454 config. ulimit = Some ( 1_000 ) ;
425455 config. resolver = Some ( "1.1.1.1" . to_owned ( ) ) ;
426456
427457 opts. merge_optional ( & config) ;
428458
429- assert_eq ! ( opts. range , config . range ) ;
459+ assert_eq ! ( opts. ports , Some ( ( 1 ..= 1000 ) . collect :: < Vec < u16 >> ( ) ) ) ;
430460 assert_eq ! ( opts. ulimit, config. ulimit) ;
431461 assert_eq ! ( opts. resolver, config. resolver) ;
432462 }
463+
464+ #[ test]
465+ fn test_parse_ports_and_ranges_single_port ( ) {
466+ let result = parse_ports_and_ranges ( "80" ) ;
467+ assert_eq ! ( result, Ok ( vec![ 80 ] ) ) ;
468+ }
469+
470+ #[ test]
471+ fn test_parse_ports_and_ranges_multiple_ports ( ) {
472+ let result = parse_ports_and_ranges ( "80,443,8080" ) ;
473+ assert_eq ! ( result, Ok ( vec![ 80 , 443 , 8080 ] ) ) ;
474+ }
475+
476+ #[ test]
477+ fn test_parse_ports_and_ranges_single_range ( ) {
478+ let result = parse_ports_and_ranges ( "1-5" ) ;
479+ assert_eq ! ( result, Ok ( vec![ 1 , 2 , 3 , 4 , 5 ] ) ) ;
480+ }
481+
482+ #[ test]
483+ fn test_parse_ports_and_ranges_mixed_ports_and_ranges ( ) {
484+ let result = parse_ports_and_ranges ( "80,443,1-3,8080" ) ;
485+ assert_eq ! ( result, Ok ( vec![ 1 , 2 , 3 , 80 , 443 , 8080 ] ) ) ;
486+ }
487+
488+ #[ test]
489+ fn test_parse_ports_and_ranges_with_spaces ( ) {
490+ let result = parse_ports_and_ranges ( "80, 443, 1-3, 8080" ) ;
491+ assert_eq ! ( result, Ok ( vec![ 1 , 2 , 3 , 80 , 443 , 8080 ] ) ) ;
492+ }
493+
494+ #[ test]
495+ fn test_parse_ports_and_ranges_duplicates ( ) {
496+ let result = parse_ports_and_ranges ( "80,443,80,443" ) ;
497+ assert_eq ! ( result, Ok ( vec![ 80 , 443 ] ) ) ;
498+ }
499+
500+ #[ test]
501+ fn test_parse_ports_and_ranges_empty_input ( ) {
502+ let result = parse_ports_and_ranges ( "" ) ;
503+ assert ! ( result. is_err( ) ) ;
504+ assert ! ( result
505+ . unwrap_err( )
506+ . contains( "No valid ports or ranges provided" ) ) ;
507+ }
508+
509+ #[ test]
510+ fn test_parse_ports_and_ranges_invalid_port ( ) {
511+ let result = parse_ports_and_ranges ( "80,abc,443" ) ;
512+ assert ! ( result. is_err( ) ) ;
513+ assert ! ( result. unwrap_err( ) . contains( "Invalid port number 'abc'" ) ) ;
514+ }
515+
516+ #[ test]
517+ fn test_parse_ports_and_ranges_invalid_range ( ) {
518+ let result = parse_ports_and_ranges ( "80,1-abc,443" ) ;
519+ assert ! ( result. is_err( ) ) ;
520+ assert ! ( result
521+ . unwrap_err( )
522+ . contains( "Invalid end port 'abc' in range '1-abc'" ) ) ;
523+ }
524+
525+ #[ test]
526+ fn test_parse_ports_and_ranges_invalid_range_format ( ) {
527+ let result = parse_ports_and_ranges ( "80,1-2-3,443" ) ;
528+ assert ! ( result. is_err( ) ) ;
529+ assert ! ( result
530+ . unwrap_err( )
531+ . contains( "Invalid range format '1-2-3'. Expected 'start-end'" ) ) ;
532+ }
533+
534+ #[ test]
535+ fn test_parse_ports_and_ranges_reverse_range ( ) {
536+ let result = parse_ports_and_ranges ( "80,5-1,443" ) ;
537+ assert ! ( result. is_err( ) ) ;
538+ assert ! ( result
539+ . unwrap_err( )
540+ . contains( "Start port 5 is greater than end port 1 in range '5-1'" ) ) ;
541+ }
542+
543+ #[ test]
544+ fn test_parse_ports_and_ranges_out_of_bounds_port ( ) {
545+ let result = parse_ports_and_ranges ( "80,70000,443" ) ;
546+ assert ! ( result. is_err( ) ) ;
547+ let error_msg = result. unwrap_err ( ) ;
548+ println ! ( "Actual error message: {}" , error_msg) ;
549+ assert ! ( error_msg. contains( "Invalid port number '70000'" ) ) ;
550+ }
551+
552+ #[ test]
553+ fn test_parse_ports_and_ranges_out_of_bounds_range ( ) {
554+ let result = parse_ports_and_ranges ( "80,1-70000,443" ) ;
555+ assert ! ( result. is_err( ) ) ;
556+ let error_msg = result. unwrap_err ( ) ;
557+ println ! ( "Actual error message: {}" , error_msg) ;
558+ assert ! ( error_msg. contains( "Invalid end port '70000' in range '1-70000'" ) ) ;
559+ }
560+
561+ #[ test]
562+ fn test_parse_ports_and_ranges_zero_port ( ) {
563+ let result = parse_ports_and_ranges ( "80,0,443" ) ;
564+ assert ! ( result. is_err( ) ) ;
565+ assert ! ( result
566+ . unwrap_err( )
567+ . contains( "Port 0 must be between 1 and 65535" ) ) ;
568+ }
569+
570+ #[ test]
571+ fn test_parse_ports_and_ranges_complex_mixed ( ) {
572+ let result = parse_ports_and_ranges ( "1,80,443,1-5,8080,9090,10-12" ) ;
573+ assert_eq ! (
574+ result,
575+ Ok ( vec![ 1 , 2 , 3 , 4 , 5 , 10 , 11 , 12 , 80 , 443 , 8080 , 9090 ] )
576+ ) ;
577+ }
433578}
0 commit comments