codec.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. from __future__ import annotations
  2. import codecs
  3. from typing import Any
  4. from .core import IDNAError, _unicode_dots_re, alabel, decode, encode, ulabel
  5. class Codec(codecs.Codec):
  6. """Stateless IDNA 2008 codec.
  7. Implements the :class:`codecs.Codec` protocol so that the whole-domain
  8. encoder (:func:`idna.encode`) and decoder (:func:`idna.decode`) are
  9. accessible through the standard codec machinery as ``"idna2008"``.
  10. Only the ``"strict"`` error handler is supported; any other handler
  11. raises :exc:`~idna.IDNAError`.
  12. """
  13. def encode(self, data: str, errors: str = "strict") -> tuple[bytes, int]: # ty: ignore[invalid-method-override]
  14. if errors != "strict":
  15. raise IDNAError(f'Unsupported error handling "{errors}"', code="unsupported_errors")
  16. if not data:
  17. return b"", 0
  18. return encode(data), len(data)
  19. def decode(self, data: bytes, errors: str = "strict") -> tuple[str, int]: # ty: ignore[invalid-method-override]
  20. if errors != "strict":
  21. raise IDNAError(f'Unsupported error handling "{errors}"', code="unsupported_errors")
  22. if not data:
  23. return "", 0
  24. return decode(data), len(data)
  25. class IncrementalEncoder(codecs.BufferedIncrementalEncoder):
  26. """Incremental IDNA 2008 encoder.
  27. Buffers a partial trailing label across calls until either the next
  28. label separator is seen or ``final=True``, so that streamed input is
  29. encoded one whole label at a time. Any of the four Unicode label
  30. separators (``U+002E``, ``U+3002``, ``U+FF0E``, ``U+FF61``) ends a
  31. label; the result always uses ``U+002E`` as the separator.
  32. Only the ``"strict"`` error handler is supported.
  33. """
  34. def _buffer_encode(self, data: str, errors: str, final: bool) -> tuple[bytes, int]: # ty: ignore[invalid-method-override]
  35. if errors != "strict":
  36. raise IDNAError(f'Unsupported error handling "{errors}"', code="unsupported_errors")
  37. if not data:
  38. return b"", 0
  39. labels = _unicode_dots_re.split(data)
  40. trailing_dot = b""
  41. if labels:
  42. if not labels[-1]:
  43. trailing_dot = b"."
  44. del labels[-1]
  45. elif not final:
  46. # Keep potentially unfinished label until the next call
  47. del labels[-1]
  48. if labels:
  49. trailing_dot = b"."
  50. result = []
  51. size = 0
  52. for label in labels:
  53. result.append(alabel(label))
  54. if size:
  55. size += 1
  56. size += len(label)
  57. result_bytes = b".".join(result) + trailing_dot
  58. size += len(trailing_dot)
  59. return result_bytes, size
  60. class IncrementalDecoder(codecs.BufferedIncrementalDecoder):
  61. """Incremental IDNA 2008 decoder.
  62. Buffers a partial trailing label across calls until either the next
  63. label separator is seen or ``final=True``, so that streamed input is
  64. decoded one whole label at a time.
  65. Only the ``"strict"`` error handler is supported.
  66. """
  67. def _buffer_decode(self, data: Any, errors: str, final: bool) -> tuple[str, int]: # ty: ignore[invalid-method-override]
  68. if errors != "strict":
  69. raise IDNAError(f'Unsupported error handling "{errors}"', code="unsupported_errors")
  70. if not data:
  71. return ("", 0)
  72. if not isinstance(data, str):
  73. try:
  74. data = str(data, "ascii")
  75. except UnicodeDecodeError as err:
  76. raise IDNAError("Invalid ASCII in A-label", code="invalid_ascii") from err
  77. labels = _unicode_dots_re.split(data)
  78. trailing_dot = ""
  79. if labels:
  80. if not labels[-1]:
  81. trailing_dot = "."
  82. del labels[-1]
  83. elif not final:
  84. # Keep potentially unfinished label until the next call
  85. del labels[-1]
  86. if labels:
  87. trailing_dot = "."
  88. result = []
  89. size = 0
  90. for label in labels:
  91. result.append(ulabel(label))
  92. if size:
  93. size += 1
  94. size += len(label)
  95. result_str = ".".join(result) + trailing_dot
  96. size += len(trailing_dot)
  97. return (result_str, size)
  98. class StreamWriter(Codec, codecs.StreamWriter):
  99. pass
  100. class StreamReader(Codec, codecs.StreamReader):
  101. pass
  102. def search_function(name: str) -> codecs.CodecInfo | None:
  103. """Codec search function registered with :mod:`codecs`.
  104. Returns a :class:`codecs.CodecInfo` for the ``"idna2008"`` codec name
  105. so that ``str.encode("idna2008")`` and ``bytes.decode("idna2008")``
  106. invoke the IDNA 2008 codec defined in this module.
  107. :param name: The codec name being looked up.
  108. :returns: A :class:`codecs.CodecInfo` instance if ``name`` is
  109. ``"idna2008"``, otherwise ``None``.
  110. """
  111. if name != "idna2008":
  112. return None
  113. return codecs.CodecInfo(
  114. name=name,
  115. encode=Codec().encode,
  116. decode=Codec().decode, # type: ignore
  117. incrementalencoder=IncrementalEncoder,
  118. incrementaldecoder=IncrementalDecoder,
  119. streamwriter=StreamWriter,
  120. streamreader=StreamReader,
  121. )
  122. codecs.register(search_function)