@@ -347,6 +347,106 @@ class _IPInterfaceField(_IPField[_IT]):
347347 """
348348
349349
350+ class _RawMaskIPv4Interface (ipaddress .IPv4Interface ):
351+ """IPv4 interface whose mask is not a contiguous netmask.
352+
353+ Args:
354+ address: Dotted address and dotted mask, as ``'a.b.c.d/m.m.m.m'``.
355+
356+ :class:`ipaddress.IPv4Interface` can only hold a prefix length, so it
357+ reads a hostmask-shaped mask such as ``0.0.0.255`` as ``/24`` and rejects
358+ a mask such as ``255.0.255.0``. This class keeps the mask octets as given
359+ in :attr:`netmask`, so :meth:`IPv4InterfaceField.pre_process` writes them
360+ back unchanged. :func:`str`, :func:`repr` and :mod:`pickle` use the
361+ dotted ``address/mask`` form, which the field also packs octet for octet.
362+
363+ Equality and ordering use the mask too. An instance sorts as the stdlib
364+ sorts ``address/32``, then by mask, so the order stays total when mixed
365+ with :class:`ipaddress.IPv4Interface` values.
366+
367+ Such a mask has no prefix length, so :attr:`network`, :attr:`with_prefixlen`
368+ and the prefix length all describe the address alone (``/32``); only
369+ :attr:`netmask`, :attr:`hostmask`, :attr:`with_netmask` and
370+ :attr:`with_hostmask` carry the mask.
371+
372+ """
373+
374+ def __init__ (self , address : 'str' ) -> 'None' :
375+ addr , _ , mask = address .partition ('/' )
376+ super ().__init__ (addr )
377+ self .netmask = ipaddress .IPv4Address (mask )
378+
379+ @property
380+ def hostmask (self ) -> 'IPv4Address' :
381+ """Bitwise inverse of :attr:`netmask`."""
382+ return ipaddress .IPv4Address (int (self .netmask ) ^ 0xFFFFFFFF )
383+
384+ def __str__ (self ) -> 'str' :
385+ return f'{ self .ip } /{ self .netmask } '
386+
387+ def __eq__ (self , other : 'object' ) -> 'bool' :
388+ if not isinstance (other , ipaddress .IPv4Interface ):
389+ return NotImplemented
390+ return self .ip == other .ip and self .netmask == other .netmask
391+
392+ def __hash__ (self ) -> 'int' :
393+ return hash ((int (self .ip ), int (self .netmask )))
394+
395+ def __lt__ (self , other : 'Any' ) -> 'bool' :
396+ if not isinstance (other , ipaddress .IPv4Interface ):
397+ return ipaddress .IPv4Interface .__lt__ (self , other )
398+ return _interface_order (self ) < _interface_order (other )
399+
400+ def __le__ (self , other : 'Any' ) -> 'bool' :
401+ if not isinstance (other , ipaddress .IPv4Interface ):
402+ return ipaddress .IPv4Interface .__le__ (self , other )
403+ return _interface_order (self ) <= _interface_order (other )
404+
405+ def __gt__ (self , other : 'Any' ) -> 'bool' :
406+ if not isinstance (other , ipaddress .IPv4Interface ):
407+ return ipaddress .IPv4Interface .__gt__ (self , other )
408+ return _interface_order (self ) > _interface_order (other )
409+
410+ def __ge__ (self , other : 'Any' ) -> 'bool' :
411+ if not isinstance (other , ipaddress .IPv4Interface ):
412+ return ipaddress .IPv4Interface .__ge__ (self , other )
413+ return _interface_order (self ) >= _interface_order (other )
414+
415+
416+ def _is_netmask (mask : 'IPv4Address' ) -> 'bool' :
417+ """Tell whether ``mask`` is a contiguous netmask (leading ones, then zeros).
418+
419+ Args:
420+ mask: Dotted mask.
421+
422+ Returns:
423+ :data:`True` for ``0.0.0.0`` through ``255.255.255.255`` with no gap
424+ in the ones, :data:`False` otherwise.
425+
426+ """
427+ inverse = int (mask ) ^ 0xFFFFFFFF
428+ return not inverse & (inverse + 1 )
429+
430+
431+ def _interface_order (value : 'IPv4Interface' ) -> 'tuple[int, int, int, int]' :
432+ """Sort key for an IPv4 interface, consistent with ``==``.
433+
434+ Args:
435+ value: IPv4 interface, either from the stdlib or a
436+ :class:`_RawMaskIPv4Interface`.
437+
438+ Returns:
439+ The stdlib order (network address, netmask, address), with a third
440+ element that is ``0`` for a stdlib interface and ``1 + mask`` for a
441+ :class:`_RawMaskIPv4Interface`, whose stdlib network is
442+ ``address/32``.
443+
444+ """
445+ if isinstance (value , _RawMaskIPv4Interface ):
446+ return int (value .ip ), 0xFFFFFFFF , 1 + int (value .netmask ), int (value .ip )
447+ return int (value .network .network_address ), int (value .network .netmask ), 0 , int (value .ip )
448+
449+
350450class IPv4InterfaceField (_IPInterfaceField [ipaddress .IPv4Interface ]):
351451 """IPv4 interface value for protocol fields.
352452
@@ -383,14 +483,30 @@ def pre_process(self, value: 'IPv4Interface | bytes | int | str', packet: 'dict[
383483 :func:`_reject_bool`), is not a valid IP interface, or is the
384484 wrong IP version for this field.
385485
486+ Notes:
487+ A string with a dotted mask that is not a contiguous netmask,
488+ such as ``'10.0.0.1/0.0.0.255'``, packs those mask octets as
489+ given -- the form :func:`str` gives a
490+ :class:`_RawMaskIPv4Interface` -- where :func:`ipaddress.ip_interface`
491+ would read a hostmask or reject the mask. Any other string goes
492+ through :func:`ipaddress.ip_interface`.
493+
386494 """
387495 _reject_bool (value , 'invalid IP interface' )
388496
389497 if isinstance (value , ipaddress .IPv4Interface ):
390498 val = value
391499 else :
392- with _reraise_as_field_value_error ('invalid IP interface' ):
393- parsed = ipaddress .ip_interface (value )
500+ parsed = None # type: Optional[IPv4Interface | IPv6Interface]
501+ if isinstance (value , str ) and '.' in value .partition ('/' )[2 ]:
502+ # a dotted mask that is not a netmask is kept as given, as
503+ # in :meth:`post_process`, rather than read as a hostmask
504+ with _reraise_as_field_value_error ('invalid IP interface' ):
505+ if not _is_netmask (ipaddress .IPv4Address (value .partition ('/' )[2 ])):
506+ parsed = _RawMaskIPv4Interface (value )
507+ if parsed is None :
508+ with _reraise_as_field_value_error ('invalid IP interface' ):
509+ parsed = ipaddress .ip_interface (value )
394510 if not isinstance (parsed , ipaddress .IPv4Interface ):
395511 raise FieldValueError (f'IP version mismatch: { parsed .version } != { self .version } ' )
396512 val = parsed
@@ -410,26 +526,32 @@ def post_process(self, value: 'bytes', packet: 'dict[str, Any]') -> 'IPv4Interfa
410526 Processed field value.
411527
412528 Raises:
413- FieldValueError: If the trailing four octets are not a valid
414- dotted netmask, or if the resulting interface is the wrong IP
415- version for this field. The leading four octets cannot
416- actually fail here -- they are always exactly 4 octets, fixed
417- by this field's length, and any such octet string is a valid
418- address -- but the conversion is still wrapped for
419- consistency with the rest of this module.
529+ FieldValueError: If the resulting interface is the wrong IP
530+ version for this field. The eight octets cannot actually fail
531+ to convert here -- they are always exactly 8 octets, fixed by
532+ this field's length, any four octets are a valid address, and
533+ a mask that is not a netmask is kept as given -- but the
534+ conversion is still wrapped for consistency with the rest of
535+ this module.
420536
421537 Notes:
422538 The trailing four octets are a dotted netmask, as written by
423539 :meth:`pre_process` -- not a prefix length as in
424- :meth:`IPv6InterfaceField.post_process`.
540+ :meth:`IPv6InterfaceField.post_process`. When they are not a
541+ contiguous netmask, the value is a :class:`_RawMaskIPv4Interface`
542+ that keeps them as given, rather than an interface the stdlib
543+ would reinterpret (``0.0.0.255`` as the hostmask of ``/24``).
425544
426545 """
427546 with _reraise_as_field_value_error ('invalid IPv4 address' ):
428547 ip = ipaddress .IPv4Address (value [:4 ])
429548 mask = ipaddress .IPv4Address (value [4 :])
430549
431550 with _reraise_as_field_value_error ('invalid IPv4 interface' ):
432- val = ipaddress .ip_interface (f'{ ip } /{ mask } ' )
551+ if _is_netmask (mask ):
552+ val = ipaddress .ip_interface (f'{ ip } /{ mask } ' )
553+ else :
554+ val = _RawMaskIPv4Interface (f'{ ip } /{ mask } ' )
433555 if not isinstance (val , ipaddress .IPv4Interface ):
434556 raise FieldValueError (f'IP version mismatch: { val .version } != { self .version } ' )
435557 return val
0 commit comments