Calc/ip4_type.py

262 lines
10 KiB
Python

"""
Clase híbrida para direcciones IPv4
"""
from sympy_Base import SympyClassBase
from typing import Optional, Union
import re
class IP4Mask:
"""
Helper class to manage IPv4 masks.
It can be initialized with an integer prefix (0-32) or a netmask string (e.g., "255.255.255.0").
"""
_prefix: int
_mask_int: int
def __init__(self, mask_input: Union[int, str]):
if isinstance(mask_input, int):
if not (0 <= mask_input <= 32):
raise ValueError(f"Invalid prefix length: {mask_input}. Must be between 0 and 32.")
self._prefix = mask_input
elif isinstance(mask_input, str):
try:
# Try to interpret as prefix length string, e.g., "24"
prefix_val = int(mask_input)
if not (0 <= prefix_val <= 32):
raise ValueError(f"Invalid prefix string: '{mask_input}'. Must be between 0 and 32.")
self._prefix = prefix_val
except ValueError:
# Try to interpret as netmask string, e.g., "255.255.255.0"
parsed_prefix = self._netmask_str_to_prefix(mask_input)
if parsed_prefix is None:
raise ValueError(f"Invalid netmask string format: '{mask_input}'.")
self._prefix = parsed_prefix
else:
raise TypeError(f"Invalid type for mask_input: {type(mask_input)}. Must be int or str.")
self._mask_int = self._prefix_to_mask_int(self._prefix)
@staticmethod
def _is_valid_ip_octet_str(octet_str: str) -> bool:
try:
octet_val = int(octet_str)
return 0 <= octet_val <= 255
except ValueError:
return False
@staticmethod
def _is_valid_ip_string(ip_str: str) -> bool:
parts = ip_str.split('.')
if len(parts) != 4:
return False
return all(IP4Mask._is_valid_ip_octet_str(p.strip()) for p in parts)
@staticmethod
def _netmask_str_to_prefix(netmask_str: str) -> Optional[int]:
if not IP4Mask._is_valid_ip_string(netmask_str):
return None
octets = [int(x) for x in netmask_str.split('.')]
mask_val = (octets[0] << 24) | (octets[1] << 16) | (octets[2] << 8) | octets[3]
binary_mask = bin(mask_val)[2:].zfill(32)
if not re.fullmatch(r"1*0*", binary_mask): # Must be contiguous 1s followed by 0s
return None
return binary_mask.count('1')
@staticmethod
def _prefix_to_mask_int(prefix: int) -> int:
if not (0 <= prefix <= 32): # Should be validated before calling
raise ValueError("Prefix out of range")
if prefix == 0:
return 0
return (0xFFFFFFFF << (32 - prefix)) & 0xFFFFFFFF
def get_mask_str(self) -> str:
return f"{(self._mask_int >> 24) & 0xFF}.{(self._mask_int >> 16) & 0xFF}.{(self._mask_int >> 8) & 0xFF}.{self._mask_int & 0xFF}"
def get_prefix_int(self) -> int:
return self._prefix
def __str__(self) -> str:
return self.get_mask_str()
def __repr__(self) -> str:
return f"IP4Mask({self._prefix})"
def __eq__(self, other):
if isinstance(other, IP4Mask):
return self._prefix == other._prefix
return False
class Class_IP4(SympyClassBase):
"""Clase híbrida para direcciones IPv4"""
def __new__(cls, *args):
"""Crear objeto SymPy válido"""
obj = SympyClassBase.__new__(cls)
return obj
def __init__(self, *args):
"""Inicialización de IP4"""
self._raw_constructor_args = args # Store for __repr__
ip_str: str
self._mask_obj: Optional[IP4Mask] = None
if not args:
raise ValueError("IP4 constructor requires at least one argument (the IP string).")
ip_str_candidate = args[0]
if not isinstance(ip_str_candidate, str):
raise TypeError(f"First argument to IP4 must be a string (IP address), got {type(ip_str_candidate)}")
if len(args) == 1:
input_str = args[0]
# Try "ip/prefix"
if '/' in input_str:
parts = input_str.split('/', 1)
ip_str = parts[0].strip()
if len(parts) > 1 and parts[1].strip():
self._mask_obj = IP4Mask(parts[1].strip())
# If no prefix after '/', it's just an IP
# Try "ip mask_str" (space separated)
elif ' ' in input_str:
parts = input_str.split()
ip_str = parts[0].strip()
if len(parts) > 1 and parts[1].strip():
self._mask_obj = IP4Mask(parts[1].strip())
# If no mask after space, it's just an IP
else: # Just an IP string
ip_str = input_str.strip()
elif len(args) == 2:
ip_str = args[0].strip()
mask_arg = args[1]
if isinstance(mask_arg, IP4Mask): # Allow passing IP4Mask instance directly
self._mask_obj = mask_arg
elif isinstance(mask_arg, (str, int)): # Parser will pass str, programmatic use might pass int
self._mask_obj = IP4Mask(mask_arg)
else:
raise TypeError(f"Second argument (mask) for IP4 must be int, str, or IP4Mask instance, got {type(mask_arg)}")
else:
raise ValueError(f"IP4 constructor takes 1 or 2 arguments, got {len(args)}: {args}")
if not IP4Mask._is_valid_ip_string(ip_str): # Use IP4Mask's validator
raise ValueError(f"Invalid IP address format: {ip_str}")
self._ip_str = ip_str
ip_parts = [int(x) for x in ip_str.split('.')]
self._ip_int = (ip_parts[0] << 24) | (ip_parts[1] << 16) | (ip_parts[2] << 8) | ip_parts[3]
# Determine the 'original_str' for SympyClassBase's _sympystr.
# This string is what appears inside ClassName(...) in Sympy output.
# It should reflect the arguments as they would be if typed in brackets.
sympy_base_original_str: str
if len(args) == 1 and isinstance(args[0], str):
# Covers "1.1.1.1/24", "1.1.1.1 255.255.0.0", or just "1.1.1.1"
sympy_base_original_str = args[0]
elif len(args) == 2:
# Reconstruct as "ip_str;mask_representation"
mask_arg_for_repr = args[1]
if isinstance(mask_arg_for_repr, IP4Mask): # Should not happen from parser
mask_repr_str = str(mask_arg_for_repr.get_prefix_int())
elif isinstance(mask_arg_for_repr, int):
mask_repr_str = str(mask_arg_for_repr)
else: # string
mask_repr_str = str(mask_arg_for_repr) # e.g., "24" or "255.255.255.0"
sympy_base_original_str = f"{args[0]};{mask_repr_str}"
else: # Only ip_str was derived, no mask (or error caught earlier)
sympy_base_original_str = self._ip_str
super().__init__(self._ip_int, sympy_base_original_str)
def __repr__(self):
# This should be a valid Python expression to recreate the object.
arg_reprs = [repr(arg) for arg in self._raw_constructor_args]
return f"{self.__class__.__name__}({', '.join(arg_reprs)})"
def __str__(self):
"""Representación string para display"""
if self._mask_obj:
return f"{self._ip_str}/{self._mask_obj.get_prefix_int()}"
return self._ip_str
def _sympystr(self, printer):
"""Representación SymPy string"""
return str(self)
@staticmethod
def Helper(input_str):
"""Ayuda contextual para IP4"""
if re.match(r"^\s*IP4\b", input_str, re.IGNORECASE):
return ('Ej: IP4[192.168.1.1/24], IP4[10.0.0.1;8], IP4[172.16.0.5;255.255.0.0]\n'
'Funciones: NetworkAddress(), BroadcastAddress(), Nodes(), get_netmask_str(), get_prefix_length()')
return None
@staticmethod
def PopupFunctionList():
"""Lista de métodos sugeridos para autocompletado de IP4"""
return [
("NetworkAddress", "Obtiene la dirección de red"),
("BroadcastAddress", "Obtiene la dirección de broadcast"),
("Nodes", "Cantidad de nodos usables en la subred"),
("get_netmask_str", "Obtiene la máscara de red (ej: 255.255.255.0)"),
("get_prefix_length", "Obtiene la longitud del prefijo CIDR (ej: 24)"),
]
def get_netmask_str(self) -> Optional[str]:
"""Returns the netmask as a string (e.g., "255.255.255.0") if a mask is defined."""
return self._mask_obj.get_mask_str() if self._mask_obj else None
def get_prefix_length(self) -> Optional[int]:
"""Returns the prefix length (e.g., 24) if a mask is defined."""
return self._mask_obj.get_prefix_int() if self._mask_obj else None
def NetworkAddress(self):
"""Obtiene la dirección de red"""
prefix = self.get_prefix_length()
if prefix is None:
raise ValueError("No prefix/mask defined for NetworkAddress calculation.")
mask_int = IP4Mask._prefix_to_mask_int(prefix)
network_int = self._ip_int & mask_int
parts = [
(network_int >> 24) & 0xFF,
(network_int >> 16) & 0xFF,
(network_int >> 8) & 0xFF,
network_int & 0xFF
]
network_str = '.'.join(str(x) for x in parts)
return Class_IP4(network_str, prefix) # Return new IP4 object for the network
def BroadcastAddress(self):
"""Obtiene la dirección de broadcast"""
prefix = self.get_prefix_length()
if prefix is None:
raise ValueError("No prefix/mask defined for BroadcastAddress calculation.")
mask_int = IP4Mask._prefix_to_mask_int(prefix)
broadcast_int = self._ip_int | (~mask_int & 0xFFFFFFFF)
parts = [
(broadcast_int >> 24) & 0xFF,
(broadcast_int >> 16) & 0xFF,
(broadcast_int >> 8) & 0xFF,
broadcast_int & 0xFF
]
broadcast_str = '.'.join(str(x) for x in parts)
return Class_IP4(broadcast_str, prefix) # Return new IP4 object for broadcast
def Nodes(self):
"""Obtiene el número de nodos disponibles"""
prefix = self.get_prefix_length()
if prefix is None:
raise ValueError("No prefix/mask defined for Nodes calculation.")
if prefix >= 31: # For /31 and /32, typically 0 usable host addresses in standard subnetting
return 0
return (2 ** (32 - prefix)) - 2