Coverage for /pythoncovmergedfiles/medio/medio/src/idna/idna/codec.py: 84%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from __future__ import annotations
3import codecs
4from typing import Any
6from .core import IDNAError, _max_domain_length, _unicode_dots_re, alabel, decode, encode, ulabel
9class Codec(codecs.Codec):
10 """Stateless IDNA 2008 codec.
12 Implements the :class:`codecs.Codec` protocol so that the whole-domain
13 encoder (:func:`idna.encode`) and decoder (:func:`idna.decode`) are
14 accessible through the standard codec machinery as ``"idna2008"``.
16 Only the ``"strict"`` error handler is supported; any other handler
17 raises :exc:`~idna.IDNAError`.
18 """
20 def encode(self, data: str, errors: str = "strict") -> tuple[bytes, int]: # ty: ignore[invalid-method-override]
21 if errors != "strict":
22 raise IDNAError(f'Unsupported error handling "{errors}"', code="unsupported_errors")
24 if not data:
25 return b"", 0
27 return encode(data), len(data)
29 def decode(self, data: bytes, errors: str = "strict") -> tuple[str, int]: # ty: ignore[invalid-method-override]
30 if errors != "strict":
31 raise IDNAError(f'Unsupported error handling "{errors}"', code="unsupported_errors")
33 if not data:
34 return "", 0
36 return decode(data), len(data)
39class IncrementalEncoder(codecs.BufferedIncrementalEncoder):
40 """Incremental IDNA 2008 encoder.
42 Buffers a partial trailing label across calls until either the next
43 label separator is seen or ``final=True``, so that streamed input is
44 encoded one whole label at a time. Any of the four Unicode label
45 separators (``U+002E``, ``U+3002``, ``U+FF0E``, ``U+FF61``) ends a
46 label; the result always uses ``U+002E`` as the separator.
48 The 253-octet domain length limit (254 with a trailing dot) that
49 :func:`idna.encode` enforces is applied to the accumulated output, so
50 that streaming a name and encoding it in one shot either both succeed
51 with the same result or both raise :exc:`~idna.IDNAError`.
53 Only the ``"strict"`` error handler is supported.
54 """
56 def __init__(self, errors: str = "strict") -> None:
57 super().__init__(errors)
58 self._emitted = 0 # octets returned so far
59 self._trailing_dot = False # whether the output so far ends with "."
61 def reset(self) -> None:
62 super().reset()
63 self._emitted = 0
64 self._trailing_dot = False
66 def getstate(self) -> Any:
67 if not self.buffer and not self._emitted:
68 return 0
69 return (self.buffer, self._emitted, self._trailing_dot)
71 def setstate(self, state: Any) -> None:
72 if state:
73 self.buffer, self._emitted, self._trailing_dot = state
74 else:
75 self.reset()
77 def _buffer_encode(self, data: str, errors: str, final: bool) -> tuple[bytes, int]: # ty: ignore[invalid-method-override]
78 if errors != "strict":
79 raise IDNAError(f'Unsupported error handling "{errors}"', code="unsupported_errors")
81 result_bytes = b""
82 size = 0
83 if data:
84 labels = _unicode_dots_re.split(data)
85 trailing_dot = b""
86 if labels:
87 if not labels[-1]:
88 trailing_dot = b"."
89 del labels[-1]
90 elif not final:
91 # Keep potentially unfinished label until the next call
92 del labels[-1]
93 if labels:
94 trailing_dot = b"."
96 result = []
97 for label in labels:
98 result.append(alabel(label))
99 if size:
100 size += 1
101 size += len(label)
103 result_bytes = b".".join(result) + trailing_dot
104 size += len(trailing_dot)
106 # Mirror encode(): the whole name may not exceed 253 octets, or 254
107 # when it ends with a dot. Until the input is final a trailing dot
108 # may still arrive, so only the 254-octet ceiling applies before then.
109 self._emitted += len(result_bytes)
110 if result_bytes:
111 self._trailing_dot = result_bytes.endswith(b".")
112 may_end_with_dot = self._trailing_dot or not final
113 if self._emitted > _max_domain_length + may_end_with_dot:
114 raise IDNAError("Domain too long", code="domain_too_long")
115 return result_bytes, size
118class IncrementalDecoder(codecs.BufferedIncrementalDecoder):
119 """Incremental IDNA 2008 decoder.
121 Buffers a partial trailing label across calls until either the next
122 label separator is seen or ``final=True``, so that streamed input is
123 decoded one whole label at a time.
125 The 254-octet input length limit that :func:`idna.decode` enforces is
126 applied to the accumulated input, so that streaming a name and decoding
127 it in one shot either both succeed with the same result or both raise
128 :exc:`~idna.IDNAError`.
130 Only the ``"strict"`` error handler is supported.
131 """
133 def __init__(self, errors: str = "strict") -> None:
134 super().__init__(errors)
135 self._consumed = 0 # input octets consumed so far
137 def reset(self) -> None:
138 super().reset()
139 self._consumed = 0
141 def getstate(self) -> tuple[bytes, int]:
142 return (self.buffer, self._consumed)
144 def setstate(self, state: tuple[bytes, int]) -> None:
145 self.buffer, self._consumed = state
147 def _buffer_decode(self, data: Any, errors: str, final: bool) -> tuple[str, int]: # ty: ignore[invalid-method-override]
148 if errors != "strict":
149 raise IDNAError(f'Unsupported error handling "{errors}"', code="unsupported_errors")
151 if not data:
152 return ("", 0)
154 if not isinstance(data, str):
155 try:
156 data = str(data, "ascii")
157 except UnicodeDecodeError as err:
158 raise IDNAError("Invalid ASCII in A-label", code="invalid_ascii") from err
160 # Mirror decode(), which rejects input longer than 254 characters
161 # (253 plus a possible trailing dot) before looking at any label.
162 # ``data`` is the unconsumed buffer plus the new input, so this is
163 # the total seen so far.
164 if self._consumed + len(data) > _max_domain_length + 1:
165 raise IDNAError("Domain too long", code="domain_too_long")
167 labels = _unicode_dots_re.split(data)
168 trailing_dot = ""
169 if labels:
170 if not labels[-1]:
171 trailing_dot = "."
172 del labels[-1]
173 elif not final:
174 # Keep potentially unfinished label until the next call
175 del labels[-1]
176 if labels:
177 trailing_dot = "."
179 result = []
180 size = 0
181 for label in labels:
182 result.append(ulabel(label))
183 if size:
184 size += 1
185 size += len(label)
187 result_str = ".".join(result) + trailing_dot
188 size += len(trailing_dot)
189 self._consumed += size
190 return (result_str, size)
193class StreamWriter(Codec, codecs.StreamWriter):
194 pass
197class StreamReader(Codec, codecs.StreamReader):
198 pass
201def search_function(name: str) -> codecs.CodecInfo | None:
202 """Codec search function registered with :mod:`codecs`.
204 Returns a :class:`codecs.CodecInfo` for the ``"idna2008"`` codec name
205 so that ``str.encode("idna2008")`` and ``bytes.decode("idna2008")``
206 invoke the IDNA 2008 codec defined in this module.
208 :param name: The codec name being looked up.
209 :returns: A :class:`codecs.CodecInfo` instance if ``name`` is
210 ``"idna2008"``, otherwise ``None``.
211 """
212 if name != "idna2008":
213 return None
214 return codecs.CodecInfo(
215 name=name,
216 encode=Codec().encode,
217 decode=Codec().decode, # type: ignore
218 incrementalencoder=IncrementalEncoder,
219 incrementaldecoder=IncrementalDecoder,
220 streamwriter=StreamWriter,
221 streamreader=StreamReader,
222 )
225codecs.register(search_function)