@@ -221,21 +221,50 @@ def _get_cidr_linux(ip: str) -> int:
221221
222222def validate_ip (ip_str : str ) -> None :
223223 """Validate IPv4 address format."""
224+ if not ip_str or not ip_str .strip ():
225+ raise ValueError ("IP address cannot be empty" )
226+
227+ ip_str = ip_str .strip ()
228+
229+ # Check basic format with regex first for better error messages
230+ ip_pattern = r"^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$"
231+ match = re .match (ip_pattern , ip_str )
232+ if not match :
233+ raise ValueError (f"Invalid IP address format. Expected format: x.x.x.x (e.g., 192.168.1.1), got: { ip_str } " )
234+
235+ # Check each octet range
236+ octets = [int (match .group (i )) for i in range (1 , 5 )]
237+ for i , octet in enumerate (octets , 1 ):
238+ if not 0 <= octet <= 255 :
239+ raise ValueError (f"Invalid octet { i } : { octet } . Each octet must be between 0 and 255" )
240+
241+ # Final validation with ipaddress module
224242 try :
225243 ipaddress .IPv4Address (ip_str )
226244 except ipaddress .AddressValueError as e :
227- raise ValueError (f"Invalid IP address format : { ip_str } " ) from e
245+ raise ValueError (f"Invalid IP address: { ip_str } " ) from e
228246
229247
230248def validate_prefix (prefix_str : str ) -> int :
231249 """Validate CIDR prefix and return as integer."""
250+ if not prefix_str or not prefix_str .strip ():
251+ raise ValueError ("Prefix cannot be empty" )
252+
253+ prefix_str = prefix_str .strip ()
254+
255+ # Check if it's a valid integer
256+ if not prefix_str .isdigit ():
257+ raise ValueError (f"Prefix must be a number, got: { prefix_str } " )
258+
232259 try :
233260 p = int (prefix_str )
234- if not 0 <= p <= 32 :
235- raise ValueError (f"CIDR prefix must be 0-32, got { p } " )
236- return p
237- except ValueError as e :
238- raise ValueError (f"Invalid CIDR prefix: { prefix_str } " ) from e
261+ except ValueError :
262+ p = int (prefix_str )
263+
264+ if not 0 <= p <= 32 :
265+ raise ValueError (f"Prefix must be between 0 and 32, got: { p } " )
266+
267+ return p
239268
240269
241270def compute (ip : str , prefix : int ) -> dict :
@@ -288,6 +317,45 @@ def compute(ip: str, prefix: int) -> dict:
288317 }
289318
290319
320+ def validate_cidr_format (cidr_str : str ) -> tuple [str , str ]:
321+ """
322+ Validate CIDR format and provide detailed error messages.
323+
324+ Args:
325+ cidr_str: CIDR notation string
326+
327+ Returns:
328+ Tuple of (ip, prefix_str) if valid
329+
330+ Raises:
331+ ValueError: with specific error message about what's wrong
332+ """
333+ if not cidr_str or not cidr_str .strip ():
334+ raise ValueError ("Empty input. Please provide an address in CIDR format (e.g., 192.168.1.1/24)" )
335+
336+ cidr_str = cidr_str .strip ()
337+
338+ # Check if it contains the slash separator
339+ if '/' not in cidr_str :
340+ raise ValueError (f"Missing '/' separator. Expected format: IP/PREFIX (e.g., 192.168.1.1/24), got: { cidr_str } " )
341+
342+ # Split into IP and prefix parts
343+ parts = cidr_str .split ('/' )
344+ if len (parts ) != 2 :
345+ raise ValueError (f"Invalid format. Expected exactly one '/' separator, got: { cidr_str } " )
346+
347+ ip_part , prefix_part = parts
348+
349+ # Validate IP part format
350+ if not ip_part .strip ():
351+ raise ValueError ("IP address part is empty. Expected format: IP/PREFIX (e.g., 192.168.1.1/24)" )
352+
353+ if not prefix_part .strip ():
354+ raise ValueError ("Prefix part is empty. Expected format: IP/PREFIX (e.g., 192.168.1.1/24)" )
355+
356+ return ip_part .strip (), prefix_part .strip ()
357+
358+
291359def parse_cidr (cidr_str : str ) -> tuple [str , int ]:
292360 """
293361 Parse CIDR notation (e.g., "192.168.1.1/24") into IP and prefix.
@@ -301,15 +369,22 @@ def parse_cidr(cidr_str: str) -> tuple[str, int]:
301369 Raises:
302370 ValueError: if CIDR format is invalid
303371 """
304- pattern = r"^(?P<ip>\d{1,3}(?:\.\d{1,3}){3})/(?P<prefix>\d{1,2})$"
305- match = re .match (pattern , cidr_str .strip ())
306- if not match :
307- raise ValueError ("Expected ADDRESS in CIDR form, e.g. 192.168.88.254/24" )
372+ # First validate the format and get parts
373+ ip_part , prefix_part = validate_cidr_format (cidr_str )
308374
309- ip = match .group ('ip' )
310- prefix = validate_prefix (match .group ('prefix' ))
375+ # Validate IP address format
376+ try :
377+ validate_ip (ip_part )
378+ except ValueError as e :
379+ raise ValueError (f"Invalid IP address '{ ip_part } ': { str (e )} " )
311380
312- return ip , prefix
381+ # Validate prefix
382+ try :
383+ prefix = validate_prefix (prefix_part )
384+ except ValueError as e :
385+ raise ValueError (f"Invalid prefix '{ prefix_part } ': { str (e )} " )
386+
387+ return ip_part , prefix
313388
314389
315390def compute_from_cidr (cidr_str : str ) -> dict :
@@ -692,7 +767,7 @@ def main(argv=None) -> int:
692767
693768 args = parser .parse_args (argv )
694769
695- have_addr = bool (args .address )
770+ have_addr = bool (args .address and args . address . strip () )
696771 headless = is_headless_linux ()
697772
698773 # Conditions for CLI: there is an address OR CLI is forced OR headless environment
@@ -708,6 +783,10 @@ def main(argv=None) -> int:
708783 else :
709784 print_result_stdout (res )
710785 return 0
786+ except ValueError as e :
787+ # Log validation errors to stderr only
788+ logger .error (f"Validation error: { str (e )} " )
789+ return 1
711790 except Exception as e :
712791 logger .error (f"{ type (e ).__name__ } { str (e )} \n { traceback .format_exc ()} " )
713792 return 1
0 commit comments