""" 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