""" Clase híbrida para direcciones IPv4 """ from class_base import ClassBase from sympy_Base import SympyClassBase from typing import Optional, Union import re import sympy class IP4Mask(ClassBase): """ 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]): prefix = self._parse_mask(mask_input) super().__init__(prefix, str(mask_input)) self._prefix = prefix self._mask_int = self._prefix_to_mask_int(self._prefix) def _parse_mask(self, mask_input: Union[int, str]) -> int: """Helper to parse mask_input and return prefix.""" prefix_val: int 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.") prefix_val = mask_input elif isinstance(mask_input, str): try: parsed_int = int(mask_input) if not (0 <= parsed_int <= 32): raise ValueError(f"Invalid prefix string: '{mask_input}'. Must be between 0 and 32.") prefix_val = parsed_int except ValueError: parsed_prefix_from_str = self._netmask_str_to_prefix(mask_input) if parsed_prefix_from_str is None: raise ValueError(f"Invalid netmask string format: '{mask_input}'.") prefix_val = parsed_prefix_from_str else: raise TypeError(f"Invalid type for mask_input: {type(mask_input)}. Must be int or str.") return prefix_val @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 @staticmethod def Helper(input_str): if re.match(r"^\s*IP4Mask\\b", input_str, re.IGNORECASE): return 'Máscara: IP4Mask[24], IP4Mask[255.255.255.0]. Funciones: get_prefix_int(), get_netmask_str(), hosts_count()' return None @staticmethod def PopupFunctionList(): return [ ("get_prefix_int", "Obtiene prefijo CIDR (ej: 24)"), ("get_netmask_str", "Obtiene máscara como string (ej: 255.255.255.0)"), ("hosts_count", "Número de hosts disponibles"), ("to_sympy", "Convierte a expresión SymPy para álgebra"), ] def hosts_count(self): """Número de hosts disponibles""" return 2**(32 - self._prefix) - 2 if self._prefix < 31 else 0 def to_sympy(self): """Convierte a SymPy cuando se necesite álgebra""" return sympy.sympify(self._prefix) class Class_IP4(SympyClassBase): """Clase híbrida para direcciones IPv4""" def __new__(cls, *args, **kwargs): temp_ip_str_candidate = args[0] if args else "" temp_ip_str = "" if isinstance(temp_ip_str_candidate, str): if '/' in temp_ip_str_candidate: temp_ip_str = temp_ip_str_candidate.split('/', 1)[0].strip() elif ' ' in temp_ip_str_candidate: temp_ip_str = temp_ip_str_candidate.split(' ', 1)[0].strip() else: temp_ip_str = temp_ip_str_candidate.strip() if IP4Mask._is_valid_ip_string(temp_ip_str): ip_parts_for_new = [int(x) for x in temp_ip_str.split('.')] ip_int_for_new = (ip_parts_for_new[0] << 24) | \ (ip_parts_for_new[1] << 16) | \ (ip_parts_for_new[2] << 8) | \ ip_parts_for_new[3] obj = SympyClassBase.__new__(cls, ip_int_for_new) else: raise ValueError(f"Invalid IP address format for __new__: {temp_ip_str}") else: if not args: raise ValueError("IP4 constructor requires arguments.") obj = SympyClassBase.__new__(cls, *args) return obj def __init__(self, *args): self._raw_constructor_args = args 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] 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()) elif ' ' 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()) else: ip_str = input_str.strip() elif len(args) == 2: ip_str = args[0].strip() mask_arg = args[1] if isinstance(mask_arg, IP4Mask): self._mask_obj = mask_arg elif isinstance(mask_arg, (str, 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): 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] sympy_base_original_str: str if len(args) == 1 and isinstance(args[0], str): sympy_base_original_str = args[0] elif len(args) == 2: mask_arg_for_repr = args[1] if isinstance(mask_arg_for_repr, IP4Mask): 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: mask_repr_str = str(mask_arg_for_repr) sympy_base_original_str = f"{args[0]};{mask_repr_str}" else: sympy_base_original_str = self._ip_str super().__init__(self._ip_int, sympy_base_original_str) def __repr__(self): arg_reprs = [repr(arg) for arg in self._raw_constructor_args] return f"{self.__class__.__name__}({', '.join(arg_reprs)})" def __str__(self): if self._mask_obj: return f"{self._ip_str}/{self._mask_obj.get_prefix_int()}" return self._ip_str def _sympystr(self, printer): return str(self) @staticmethod def Helper(input_str): 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(), mask()') return None @staticmethod def PopupFunctionList(): return [ ("NetworkAddress", "Dirección de red"), ("BroadcastAddress", "Dirección de broadcast"), ("Nodes", "Hosts disponibles"), ("mask", "Objeto máscara con sus métodos"), ("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]: return self._mask_obj.get_mask_str() if self._mask_obj else None def get_prefix_length(self) -> Optional[int]: return self._mask_obj.get_prefix_int() if self._mask_obj else None def NetworkAddress(self): 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) def BroadcastAddress(self): 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) def Nodes(self): prefix = self.get_prefix_length() if prefix is None: raise ValueError("No prefix/mask defined for Nodes calculation.") if prefix >= 31: return 0 return (2 ** (32 - prefix)) - 2 def mask(self) -> Optional[IP4Mask]: """Retorna objeto máscara para autocompletado""" if not self._mask_obj: raise ValueError("No mask defined for this IP4 object.") return self._mask_obj