|
2 | 2 | import base64 |
3 | 3 | import datetime |
4 | 4 | import gc |
| 5 | +import ipaddress |
| 6 | +import itertools |
5 | 7 | import sys |
6 | 8 | import warnings |
7 | 9 | import weakref |
@@ -319,6 +321,93 @@ def test_is_ip_address_invalid_type() -> None: |
319 | 321 | helpers.is_ip_address(object()) |
320 | 322 |
|
321 | 323 |
|
| 324 | +# ------------------------------- is_canonical_ipv4_address() --------------- |
| 325 | + |
| 326 | + |
| 327 | +@pytest.mark.parametrize( |
| 328 | + "host", |
| 329 | + [ |
| 330 | + "0.0.0.0", |
| 331 | + "127.0.0.1", |
| 332 | + "8.8.8.8", |
| 333 | + "192.168.0.1", |
| 334 | + "255.255.255.255", |
| 335 | + ], |
| 336 | +) |
| 337 | +def test_is_canonical_ipv4_address_accepts_dotted_quad(host: str) -> None: |
| 338 | + assert helpers.is_canonical_ipv4_address(host) |
| 339 | + |
| 340 | + |
| 341 | +@pytest.mark.parametrize( |
| 342 | + "host", |
| 343 | + [ |
| 344 | + "2130706433", # decimal integer form of 127.0.0.1 |
| 345 | + "017700000001", # octal form of 127.0.0.1 |
| 346 | + "127.1", # short-hand form of 127.0.0.1 |
| 347 | + "127.0.1", # 3-part short-hand |
| 348 | + "0177.0.0.1", # octal leading-zero octet |
| 349 | + "01.2.3.4", # octal leading-zero octet |
| 350 | + "256.0.0.1", # octet out of range |
| 351 | + "999.0.0.1", # octet out of range |
| 352 | + "1.2.3.4.5", # too many octets |
| 353 | + "127.0.0.", # trailing dot / empty octet |
| 354 | + "12³.0.0.1", # superscript digit (str.isdigit but not int) |
| 355 | + "127.0.0.1", # full-width digits |
| 356 | + "0xa.0.0.0", # hex octet |
| 357 | + " 127.0.0.1", # leading whitespace |
| 358 | + "127.0.0.1 ", # trailing whitespace |
| 359 | + "example.com", # domain name |
| 360 | + "", # empty |
| 361 | + ], |
| 362 | +) |
| 363 | +def test_is_canonical_ipv4_address_rejects_non_canonical(host: str) -> None: |
| 364 | + assert not helpers.is_canonical_ipv4_address(host) |
| 365 | + |
| 366 | + |
| 367 | +def _ipaddress_accepts_ipv4(host: str) -> bool: |
| 368 | + """Oracle: does the stdlib accept ``host`` as a canonical IPv4 address?""" |
| 369 | + try: |
| 370 | + ipaddress.IPv4Address(host) |
| 371 | + except ipaddress.AddressValueError: |
| 372 | + return False |
| 373 | + return True |
| 374 | + |
| 375 | + |
| 376 | +def test_is_canonical_ipv4_address_matches_stdlib() -> None: |
| 377 | + """Prove equivalence with ``ipaddress.IPv4Address`` over a broad corpus. |
| 378 | +
|
| 379 | + The helper is a fast hand-rolled substitute for the stdlib parser; this |
| 380 | + exhaustively cross-checks the two agree on every combination of a set of |
| 381 | + octet-like tokens covering the known edge cases (leading zeros, out of |
| 382 | + range, empty, unicode digits, wrong octet count). |
| 383 | + """ |
| 384 | + tokens = [ |
| 385 | + "0", |
| 386 | + "1", |
| 387 | + "9", |
| 388 | + "10", |
| 389 | + "99", |
| 390 | + "255", |
| 391 | + "256", |
| 392 | + "999", |
| 393 | + "00", |
| 394 | + "01", |
| 395 | + "0177", |
| 396 | + "1234", |
| 397 | + "", |
| 398 | + "a", |
| 399 | + "0x1", |
| 400 | + "1", # full-width 1 |
| 401 | + "1²", # trailing superscript |
| 402 | + ] |
| 403 | + for count in range(1, 5): |
| 404 | + for parts in itertools.product(tokens, repeat=count): |
| 405 | + host = ".".join(parts) |
| 406 | + assert helpers.is_canonical_ipv4_address(host) == _ipaddress_accepts_ipv4( |
| 407 | + host |
| 408 | + ), host |
| 409 | + |
| 410 | + |
322 | 411 | # ----------------------------------- TimeoutHandle ------------------- |
323 | 412 |
|
324 | 413 |
|
|
0 commit comments