# -*- coding: utf-8 -*-
"""IP Address Conversion
===========================
.. module:: pcapkit.corekit.schema.ipaddress
:mod:`pcapkit.corekit.schema.ipaddress` converts a caller-supplied IP address to an
:mod:`ipaddress` object, refusing a :obj:`bool` before :mod:`ipaddress` reads it as
an :class:`int` (:issue:`491`), for the IP address fields
(:mod:`pcapkit.corekit.fields.ipaddress`) and the protocols' makers alike. It is one
of the :mod:`pcapkit.corekit.schema` helpers shared across the schema layer, not
part of the header schema framework in :mod:`pcapkit.protocols.schema`; see
:ref:`utilities-or-corekit`.
.. versionadded:: 1.6.0
Moved here from ``pcapkit.corekit.fields.ipaddress``, with the guards the
IP address fields share (:issue:`2674`).
"""
import contextlib
import ipaddress
from typing import TYPE_CHECKING
from pcapkit.utilities.exceptions import FieldValueError
__all__ = ['parse_ip_address']
if TYPE_CHECKING:
from ipaddress import IPv4Address, IPv6Address
from typing import Iterator, Optional
@contextlib.contextmanager
def _reraise_as_field_value_error(description: str) -> 'Iterator[None]':
"""Translate a bare :exc:`ValueError` from :mod:`ipaddress` into :exc:`FieldValueError`.
Every conversion here and in :mod:`pcapkit.corekit.fields.ipaddress`
ultimately calls into the stdlib
:mod:`ipaddress` module, which raises a bare :exc:`ValueError` (or a
subclass of it, e.g. :exc:`~ipaddress.AddressValueError` or
:exc:`~ipaddress.NetmaskValueError`) for a malformed value. Left alone,
that exception is not an instance of
:exc:`~pcapkit.utilities.exceptions.BaseError`, unlike every other
exception those modules raise -- so a caller cannot rely on
``except BaseError`` to catch a bad field value. Wrapping the conversion
in this context manager re-raises it as :exc:`FieldValueError` instead,
preserving the original message.
Args:
description: Human-readable description of the value being
converted, used to build the :exc:`FieldValueError` message.
Raises:
FieldValueError: If the code inside the ``with`` block raises
:exc:`ValueError`.
"""
try:
yield
except FieldValueError:
# NOTE: ``FieldValueError`` is itself a ``ValueError``, so without this
# clause first, a ``FieldValueError`` raised inside the ``with`` block
# (e.g. a version-mismatch check) would be caught below and re-wrapped,
# losing its original message. Callers are expected to keep such
# raises outside the ``with`` block, but this is the same ordering
# trap ``ProtocolError`` carries at ``exceptions.py``, so it is guarded
# here too rather than relied upon by convention alone.
raise
except ValueError as error:
raise FieldValueError(f'{description}: {error}') from error
[docs]
def _reject_bool(value: 'object', description: str) -> 'None':
"""Reject a :obj:`bool` value before it reaches :mod:`ipaddress`.
Args:
value: Value to check.
description: Human-readable description of what ``value`` is, used
to build the :exc:`~pcapkit.utilities.exceptions.FieldValueError` message.
Raises:
FieldValueError: If ``value`` is a :obj:`bool`.
Notes:
:obj:`bool` is an :class:`int` subclass, and every conversion here and
in :mod:`pcapkit.corekit.fields.ipaddress` ultimately calls
:func:`ipaddress.ip_address` or
:func:`ipaddress.ip_interface`, both of which treat any :class:`int`
below ``2**32`` as IPv4 -- so without this guard, ``True``/``False``
are silently accepted as ``0.0.0.1``/``0.0.0.0`` (or the equivalent
interface) on an IPv4-typed field, with **no exception and no
warning**. On an IPv6-typed field the same conversion happens to
raise instead, because the resulting
:class:`~ipaddress.IPv4Address`'s version mismatches -- and that
asymmetry is exactly what let this slip past :issue:`469`'s otherwise
equivalent guard for :meth:`MH._make_opt_mn_id
<pcapkit.protocols.internet.mh.MH._make_opt_mn_id>` (cf.
:issue:`491`).
Every caller checks this *before* dispatching on the value's type,
for the same placement reason :issue:`469` gives: a correct check
in the wrong position does not fire.
Guarding the field classes is necessary but not sufficient, because a
``_make_*`` that must know the address family before it can build the
schema converts the argument itself and so never hands the fields a
:obj:`bool` at all. :func:`parse_ip_address` is where those callers
reach this guard (cf. :issue:`508`).
"""
if isinstance(value, bool):
raise FieldValueError(
f'{description}: must not be a bool, not {value!r} -- pass '
f'int({value!r}) if the numeric value is what is wanted')
[docs]
def parse_ip_address(value: 'IPv4Address | IPv6Address | bytes | int | str',
description: str,
version: 'Optional[int]' = None) -> 'IPv4Address | IPv6Address':
"""Convert a caller-supplied address on the **construction** path.
Args:
value: Address as the caller gave it -- an :mod:`ipaddress` object,
which is returned unchanged, or anything :mod:`ipaddress` accepts.
description: Human-readable description of what ``value`` is, used to
build the :exc:`~pcapkit.utilities.exceptions.FieldValueError`
message. Callers in a protocol should carry their usual context
into it, e.g. ``f'{self.alias}: [OptNo {type}] care-of address'``.
version: IP version to demand, ``4`` or ``6``, or :obj:`None` to take
whichever family ``value`` describes. Pass it where the wire format
fixes the family, so that an :class:`int` is widened to the right
one -- ``258`` is ``::102`` for ``version=6`` but ``0.0.1.2`` for
:func:`ipaddress.ip_address`.
Returns:
The converted address.
Raises:
FieldValueError: If ``value`` is a :obj:`bool` (cf.
:func:`_reject_bool`), is not a valid IP address, or is not of
``version``.
Notes:
This is the sanctioned way for a ``_make_*`` method to turn a
caller-supplied address into an :mod:`ipaddress` object, and it exists
because doing it with :func:`ipaddress.ip_address` directly is the
defect :issue:`508` found: a ``_make_*`` that has to know the address
*family* before it can build the schema -- to size an option whose
length is the only thing on the wire that carries the family -- must
convert the argument itself, and that conversion happens **before**
the schema, so it launders a :obj:`bool` into an
:class:`~ipaddress.IPv4Address` that the :issue:`491` guard in
:meth:`_IPAddressField.pre_process
<pcapkit.corekit.fields.ipaddress._IPAddressField.pre_process>` can then only see as a
legitimate address: ``True`` / ``False`` become ``0.0.0.1`` /
``0.0.0.0`` -- or ``::1`` / ``::`` where the wire format fixes the
family as IPv6 -- without complaint.
Routing every one of them through here rather than giving each its own
:func:`isinstance` check is the whole point: :meth:`MH._make_opt_mn_id
<pcapkit.protocols.internet.mh.MH._make_opt_mn_id>` carries exactly
such a check (:issue:`469`), and :issue:`491` is the same defect at
every site that lacks one. A guard that has to be remembered per call
site is a guard that will be forgotten at the next one.
The :obj:`bool` rejection is the **first** statement here, ahead of any
dispatch on the value's type, for the placement reason :issue:`469`
gives and :func:`_reject_bool` repeats.
This raises :exc:`~pcapkit.utilities.exceptions.FieldValueError` and not
:exc:`~pcapkit.utilities.exceptions.ProtocolError`, which is deliberate
even though sibling guards for the same mistake --
:meth:`MH._make_opt_mn_id
<pcapkit.protocols.internet.mh.MH._make_opt_mn_id>` from :issue:`469`
and :class:`ESP's SecurityAssociation
<pcapkit.protocols.internet.esp.SecurityAssociation>` from :issue:`491`
-- raise the latter. The layer decides: this is a field-level
conversion, so it answers with what :meth:`_IPAddressField.pre_process
<pcapkit.corekit.fields.ipaddress._IPAddressField.pre_process>`
answers with for the identical value, and a caller sees one exception
whether the :obj:`bool` reached the field through the schema or through
a ``_make_*``. Those protocol-level guards answer for the *option*,
alongside siblings that are not about addresses at all --
``_make_opt_mn_id`` refuses a :obj:`bool` for every MN-ID subtype,
address-typed or not -- so none of them can route through here
without losing the subtype-aware message that is the point of it. Both
exception classes derive from
:exc:`~pcapkit.utilities.exceptions.BaseError` *and* :exc:`ValueError`,
so the difference is invisible to ``except BaseError`` and ``except
ValueError``, and no call of this function or of a ``_make_*`` in the
library sits inside an ``except`` for either one specifically.
.. versionadded:: 1.5.0
"""
_reject_bool(value, description)
if isinstance(value, (ipaddress.IPv4Address, ipaddress.IPv6Address)):
ip = value # type: IPv4Address | IPv6Address
else:
with _reraise_as_field_value_error(description):
if version == 4:
ip = ipaddress.IPv4Address(value)
elif version == 6:
ip = ipaddress.IPv6Address(value)
else:
ip = ipaddress.ip_address(value)
# NOTE: Checked outside the ``with`` block above, and after it, because an
# :mod:`ipaddress` object taken from the branch that skips the conversion
# has not been version-checked at all -- ``IPv6Address(IPv4Address(...))``
# would have raised, but returning the object unchanged cannot.
if version is not None and ip.version != version:
raise FieldValueError(f'{description}: IP version mismatch: {ip.version} != {version}')
return ip