Coverage for src/local_deep_research/security/legacy_ipv4.py: 96%
64 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-06 15:42 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-06 15:42 +0000
1"""Recognize ambiguous historical IPv4 host syntax without network access."""
3import ipaddress
4from typing import Final
5from urllib.parse import unquote_to_bytes
8AMBIGUOUS_NUMERIC_IPV4_HOST_ERROR: Final = "Ambiguous numeric IPv4 host"
9ENCODED_NUMERIC_IPV4_HOST_ERROR: Final = "Encoded numeric IPv4 host"
10_COMPONENT_MAXIMUMS: Final[tuple[tuple[int, ...], ...]] = (
11 (0xFFFFFFFF,),
12 (0xFF, 0xFFFFFF),
13 (0xFF, 0xFF, 0xFFFF),
14 (0xFF, 0xFF, 0xFF, 0xFF),
15)
18def is_ambiguous_numeric_ipv4_host(hostname: str) -> bool:
19 """Return whether ``hostname`` is a valid noncanonical historical IPv4 form.
21 This recognizes the one-to-four-part inet_aton-style grammar using decimal,
22 leading-zero octal, and ``0x`` hexadecimal components. It deliberately does
23 not perform DNS or platform parsing.
24 """
25 try:
26 _ = ipaddress.ip_address(hostname)
27 except ValueError:
28 components = hostname.split(".")
29 else:
30 return False
32 if not 1 <= len(components) <= len(_COMPONENT_MAXIMUMS):
33 return False
35 maximums = _COMPONENT_MAXIMUMS[len(components) - 1]
36 return all(
37 _parse_component(component, maximum) is not None
38 for component, maximum in zip(components, maximums, strict=True)
39 )
42def is_percent_encoded_numeric_ipv4_host(hostname: str) -> bool:
43 """Return whether one-pass percent decoding exposes an IPv4 address form."""
44 decoded_hostname = _decode_percent_escapes(hostname)
45 if decoded_hostname is None or decoded_hostname == hostname:
46 return False
47 classification_hostname = decoded_hostname.rstrip(".")
49 try:
50 parsed_ip = ipaddress.ip_address(classification_hostname)
51 except ValueError:
52 return is_ambiguous_numeric_ipv4_host(classification_hostname)
53 return isinstance(parsed_ip, ipaddress.IPv4Address)
56def _decode_percent_escapes(hostname: str) -> str | None:
57 """Decode only well-formed ASCII percent escapes without recursive decoding."""
58 position = 0
59 while (percent := hostname.find("%", position)) >= 0:
60 if ( 60 ↛ 65line 60 didn't jump to line 65 because the condition on line 60 was never true
61 percent + 2 >= len(hostname)
62 or _digit_value(hostname[percent + 1]) is None
63 or _digit_value(hostname[percent + 2]) is None
64 ):
65 return None
66 position = percent + 3
68 try:
69 return unquote_to_bytes(hostname).decode("ascii")
70 except UnicodeDecodeError:
71 return None
74def _parse_component(component: str, maximum: int) -> int | None:
75 """Parse one historical IPv4 component without integer overflow."""
76 if not component:
77 return None
79 base = 10
80 digits = component
81 if component.startswith(("0x", "0X")):
82 base = 16
83 digits = component[2:]
84 elif len(component) > 1 and component.startswith("0"):
85 base = 8
86 digits = component[1:]
88 if not digits:
89 return None
91 value = 0
92 for character in digits:
93 digit = _digit_value(character)
94 if digit is None or digit >= base or value > (maximum - digit) // base:
95 return None
96 value = value * base + digit
97 return value
100def _digit_value(character: str) -> int | None:
101 if "0" <= character <= "9":
102 return ord(character) - ord("0")
103 if "a" <= character <= "f":
104 return ord(character) - ord("a") + 10
105 if "A" <= character <= "F":
106 return ord(character) - ord("A") + 10
107 return None