Coverage for /pythoncovmergedfiles/medio/medio/src/idna/idna/core.py: 96%
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 bisect
4import re
5import unicodedata
6import warnings
7from typing import Literal
9from . import idnadata
10from .intranges import intranges_contain
12_virama_combining_class = 9
13_alabel_prefix = b"xn--"
14_max_input_length = 1024
15_max_domain_length = 253 # RFC 1035 octets, excluding any trailing dot
16_STATUS_VALID, _STATUS_MAPPED, _STATUS_DEVIATION, _STATUS_IGNORED = b"VMDI"
17_unicode_dots_re = re.compile("[\u002e\u3002\uff0e\uff61]")
18_std3_disallowed_re = re.compile("[\x00-\x2c\x2f\x3a-\x40A-Z\x5b-\x60\x7b-\x7f]")
19_ldh_label_re = re.compile(r"(?!..--)[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\Z")
20_bidi_rtl_first = frozenset({"R", "AL"})
21_bidi_rtl_categories = frozenset({"R", "AL", "AN"})
22_bidi_rtl_allowed = frozenset({"R", "AL", "AN", "EN", "ES", "CS", "ET", "ON", "BN", "NSM"})
23_bidi_rtl_valid_ending = frozenset({"R", "AL", "EN", "AN"})
24_bidi_rtl_numeric = frozenset({"AN", "EN"})
25_bidi_ltr_allowed = frozenset({"L", "EN", "ES", "CS", "ET", "ON", "BN", "NSM"})
26_bidi_ltr_valid_ending = frozenset({"L", "EN"})
27_bidi_joiner_l_or_d = frozenset({"L", "D"})
28_bidi_joiner_r_or_d = frozenset({"R", "D"})
31def _joining_type(cp: int) -> str | None:
32 for jt, ranges in idnadata.joining_types.items():
33 if intranges_contain(cp, ranges):
34 return jt
35 return None
38# Machine-readable identifiers for the rule an :class:`IDNAError` reports.
39# These strings are stable and documented; exception message wording is not.
40_ErrorCode = Literal[
41 "input_too_long",
42 "label_too_long",
43 "domain_too_long",
44 "empty_label",
45 "empty_domain",
46 "not_nfc",
47 "hyphen_3_4",
48 "hyphen_start_end",
49 "leading_combiner",
50 "disallowed_codepoint",
51 "contextj",
52 "contexto",
53 "unknown_codepoint",
54 "bidi_rule_1",
55 "bidi_rule_2",
56 "bidi_rule_3",
57 "bidi_rule_4",
58 "bidi_rule_5",
59 "bidi_rule_6",
60 "bidi_unknown_direction",
61 "invalid_alabel",
62 "non_canonical_alabel",
63 "invalid_ascii",
64 "invalid_utf8",
65 "uts46_disallowed",
66 "uts46_std3",
67 "unsupported_errors",
68]
71class IDNAError(UnicodeError):
72 """Base exception for all IDNA-encoding related problems.
74 ``str(err)`` is a human-readable description of the failure. The
75 exception also carries machine-readable attributes so callers do not
76 need to parse the message:
78 * ``code`` -- a short, stable identifier for the rule that failed, such
79 as ``"disallowed_codepoint"`` or ``"bidi_rule_2"``. Message wording,
80 by contrast, may change between releases.
81 * ``text`` -- the label (or, for UTS #46 processing, the domain) that
82 was being validated;
83 * ``codepoint`` -- the offending codepoint, as an ``int``;
84 * ``position`` -- the 1-based index of the offending character within
85 ``text``, matching the position quoted in the message.
87 Each is ``None`` when it does not apply.
88 """
90 code: str | None
91 text: str | None
92 codepoint: int | None
93 position: int | None
95 def __init__(
96 self,
97 *args: object,
98 code: _ErrorCode | None = None,
99 text: str | None = None,
100 codepoint: int | None = None,
101 position: int | None = None,
102 ) -> None:
103 super().__init__(*args)
104 self.code = code
105 self.text = text
106 self.codepoint = codepoint
107 self.position = position
110class IDNABidiError(IDNAError):
111 """Exception when bidirectional requirements are not satisfied"""
114class InvalidCodepoint(IDNAError):
115 """Exception when a disallowed or unallocated codepoint is used"""
118class InvalidCodepointContext(IDNAError):
119 """Exception when the codepoint is not valid in the context it is used"""
122def _combining_class(cp: int) -> int:
123 v = unicodedata.combining(chr(cp))
124 if v == 0 and not unicodedata.name(chr(cp)):
125 raise ValueError("Unknown character in unicodedata")
126 return v
129def _is_script(cp: str, script: str) -> bool:
130 return intranges_contain(ord(cp), idnadata.scripts[script])
133def _punycode(s: str) -> bytes:
134 return s.encode("punycode")
137def _unot(s: int) -> str:
138 return f"U+{s:04X}"
141def valid_label_length(label: bytes | str) -> bool:
142 """Check that a label does not exceed the maximum permitted length.
144 Per :rfc:`1035` (and :rfc:`5891` §4.2.4) a DNS label must not exceed
145 63 octets. The argument may be either a :class:`str` (a U-label, where
146 length is measured in characters) or :class:`bytes` (an A-label, where
147 length is measured in octets).
149 :param label: The label to check.
150 :returns: ``True`` if the label is within the length limit, otherwise
151 ``False``.
152 """
153 return len(label) <= 63
156def valid_string_length(domain: bytes | str, trailing_dot: bool) -> bool:
157 """Check that a full domain name does not exceed the maximum length.
159 Per :rfc:`1035`, a domain name is limited to 253 octets when no trailing
160 dot is present, or 254 octets when one is included.
162 :param domain: The full (possibly multi-label) domain name.
163 :param trailing_dot: ``True`` if ``domain`` includes a trailing ``.``.
164 :returns: ``True`` if the domain is within the length limit, otherwise
165 ``False``.
166 """
167 return len(domain) <= _max_domain_length + trailing_dot
170def check_bidi(label: str, check_ltr: bool = False) -> bool:
171 """Validate the Bidi Rule from :rfc:`5893` for a single label.
173 The Bidi Rule constrains how bidirectional characters (Hebrew, Arabic,
174 etc.) may appear within a label. By default the check is only applied
175 when the label contains at least one right-to-left character (Unicode
176 bidirectional categories ``R``, ``AL``, or ``AN``); set ``check_ltr``
177 to ``True`` to apply it to LTR-only labels as well.
179 :param label: The label to validate, as a Unicode string.
180 :param check_ltr: If ``True``, apply the rules even when the label
181 contains no RTL characters.
182 :returns: ``True`` if the label satisfies the Bidi Rule.
183 :raises IDNABidiError: If any of Bidi Rule conditions 1-6 are violated,
184 or if the directional category of a codepoint cannot be determined.
185 """
186 if len(label) > _max_input_length:
187 raise IDNAError("Label too long", code="input_too_long")
188 # Bidi rules should only be applied if string contains RTL characters
189 bidi_label = False
190 for idx, cp in enumerate(label, 1):
191 direction = unicodedata.bidirectional(cp)
192 if direction == "":
193 # String likely comes from a newer version of Unicode
194 raise IDNABidiError(
195 f"Unknown directionality in label {label!r} at position {idx}",
196 code="bidi_unknown_direction",
197 text=label,
198 codepoint=ord(cp),
199 position=idx,
200 )
201 if direction in _bidi_rtl_categories:
202 bidi_label = True
203 if not bidi_label and not check_ltr:
204 return True
206 # Bidi rule 1
207 direction = unicodedata.bidirectional(label[0])
208 if direction in _bidi_rtl_first:
209 rtl = True
210 elif direction == "L":
211 rtl = False
212 else:
213 raise IDNABidiError(
214 f"First codepoint in label {label!r} must be directionality L, R or AL",
215 code="bidi_rule_1",
216 text=label,
217 codepoint=ord(label[0]),
218 position=1,
219 )
221 valid_ending = False
222 ending_idx = 1
223 number_type: str | None = None
224 for idx, cp in enumerate(label, 1):
225 direction = unicodedata.bidirectional(cp)
227 if rtl:
228 # Bidi rule 2
229 if direction not in _bidi_rtl_allowed:
230 raise IDNABidiError(
231 f"Invalid direction for codepoint at position {idx} in a right-to-left label",
232 code="bidi_rule_2",
233 text=label,
234 codepoint=ord(cp),
235 position=idx,
236 )
237 # Bidi rule 3
238 if direction in _bidi_rtl_valid_ending:
239 valid_ending = True
240 ending_idx = idx
241 elif direction != "NSM":
242 valid_ending = False
243 ending_idx = idx
244 # Bidi rule 4
245 if direction in _bidi_rtl_numeric:
246 if not number_type:
247 number_type = direction
248 elif number_type != direction:
249 raise IDNABidiError(
250 "Can not mix numeral types in a right-to-left label",
251 code="bidi_rule_4",
252 text=label,
253 codepoint=ord(cp),
254 position=idx,
255 )
256 else:
257 # Bidi rule 5
258 if direction not in _bidi_ltr_allowed:
259 raise IDNABidiError(
260 f"Invalid direction for codepoint at position {idx} in a left-to-right label",
261 code="bidi_rule_5",
262 text=label,
263 codepoint=ord(cp),
264 position=idx,
265 )
266 # Bidi rule 6
267 if direction in _bidi_ltr_valid_ending:
268 valid_ending = True
269 ending_idx = idx
270 elif direction != "NSM":
271 valid_ending = False
272 ending_idx = idx
274 if not valid_ending:
275 # Rules 3 and 6 concern the last character that is not a
276 # non-spacing mark, which is what ``ending_idx`` tracks.
277 raise IDNABidiError(
278 "Label ends with illegal codepoint directionality",
279 code="bidi_rule_3" if rtl else "bidi_rule_6",
280 text=label,
281 codepoint=ord(label[ending_idx - 1]),
282 position=ending_idx,
283 )
285 return True
288def check_initial_combiner(label: str) -> bool:
289 """Reject labels that begin with a combining mark.
291 Per :rfc:`5891` §4.2.3.2 a label must not start with a character of
292 Unicode general category ``M`` (Mark).
294 :param label: The label to check.
295 :returns: ``True`` if the first character is not a combining mark.
296 :raises IDNAError: If the label begins with a combining character.
297 """
298 if label and unicodedata.category(label[0])[0] == "M":
299 raise IDNAError(
300 "Label begins with an illegal combining character",
301 code="leading_combiner",
302 text=label,
303 codepoint=ord(label[0]),
304 position=1,
305 )
306 return True
309def check_hyphen_ok(label: str) -> bool:
310 """Validate the hyphen restrictions for a label.
312 Per :rfc:`5891` §4.2.3.1 a label must not start or end with a hyphen
313 (``U+002D``), and must not have hyphens in both the third and fourth
314 positions (the prefix reserved for A-labels).
316 :param label: The label to check.
317 :returns: ``True`` if the hyphen restrictions are satisfied.
318 :raises IDNAError: If any of the hyphen restrictions are violated.
319 """
320 if label[2:4] == "--":
321 raise IDNAError("Label has disallowed hyphens in 3rd and 4th position", code="hyphen_3_4")
322 if label.startswith("-") or label.endswith("-"):
323 raise IDNAError("Label must not start or end with a hyphen", code="hyphen_start_end")
324 return True
327def check_nfc(label: str) -> None:
328 """Require that a label is in Unicode Normalization Form C.
330 :param label: The label to check.
331 :raises IDNAError: If ``label`` differs from its NFC normalisation.
332 """
333 if len(label) > _max_input_length:
334 raise IDNAError("Label too long", code="input_too_long")
335 if unicodedata.normalize("NFC", label) != label:
336 raise IDNAError("Label must be in Normalization Form C", code="not_nfc")
339def valid_contextj(label: str, pos: int) -> bool:
340 """Validate the CONTEXTJ rules from :rfc:`5892` Appendix A.
342 These rules govern the contextual use of the joiner codepoints
343 ``U+200C`` (ZERO WIDTH NON-JOINER, Appendix A.1) and ``U+200D``
344 (ZERO WIDTH JOINER, Appendix A.2) within a label.
346 :param label: The label containing the codepoint.
347 :param pos: Index of the joiner codepoint within ``label``.
348 :returns: ``True`` if the codepoint at ``pos`` satisfies its CONTEXTJ
349 rule, ``False`` otherwise (including when the codepoint at
350 ``pos`` is not a recognised joiner).
351 :raises ValueError: If an adjacent codepoint has no Unicode name when
352 determining its combining class.
353 :raises IDNAError: If ``label`` exceeds the defensive input length limit.
354 """
355 if len(label) > _max_input_length:
356 raise IDNAError("Label too long", code="input_too_long")
357 cp_value = ord(label[pos])
359 if cp_value == 0x200C:
360 if pos > 0 and _combining_class(ord(label[pos - 1])) == _virama_combining_class:
361 return True
363 ok = False
364 for i in range(pos - 1, -1, -1):
365 joining_type = _joining_type(ord(label[i]))
366 if joining_type == "T":
367 continue
368 if joining_type in _bidi_joiner_l_or_d:
369 ok = True
370 break
371 break
373 if not ok:
374 return False
376 ok = False
377 for i in range(pos + 1, len(label)):
378 joining_type = _joining_type(ord(label[i]))
379 if joining_type == "T":
380 continue
381 if joining_type in _bidi_joiner_r_or_d:
382 ok = True
383 break
384 break
385 return ok
387 if cp_value == 0x200D:
388 return pos > 0 and _combining_class(ord(label[pos - 1])) == _virama_combining_class
390 return False
393def valid_contexto(label: str, pos: int, exception: bool = False) -> bool:
394 """Validate the CONTEXTO rules from :rfc:`5892` Appendix A.
396 Covers the contextual rules for codepoints such as MIDDLE DOT
397 (``U+00B7``), Greek lower numeral sign, Hebrew punctuation, Katakana
398 middle dot, and the Arabic-Indic / Extended Arabic-Indic digit ranges.
400 :param label: The label containing the codepoint.
401 :param pos: Index of the codepoint within ``label``.
402 :param exception: Reserved for forward compatibility; currently unused.
403 :returns: ``True`` if the codepoint at ``pos`` satisfies its CONTEXTO
404 rule, ``False`` otherwise (including when the codepoint is not a
405 recognised CONTEXTO codepoint).
406 :raises IDNAError: If ``label`` exceeds the defensive input length limit.
407 """
408 if len(label) > _max_input_length:
409 raise IDNAError("Label too long", code="input_too_long")
410 cp_value = ord(label[pos])
412 if cp_value == 0x00B7:
413 return 0 < pos < len(label) - 1 and ord(label[pos - 1]) == 0x006C and ord(label[pos + 1]) == 0x006C
415 if cp_value == 0x0375:
416 if pos < len(label) - 1 and len(label) > 1:
417 return _is_script(label[pos + 1], "Greek")
418 return False
420 if cp_value in {0x05F3, 0x05F4}:
421 if pos > 0:
422 return _is_script(label[pos - 1], "Hebrew")
423 return False
425 if cp_value == 0x30FB:
426 for cp in label:
427 if cp == "\u30fb":
428 continue
429 if _is_script(cp, "Hiragana") or _is_script(cp, "Katakana") or _is_script(cp, "Han"):
430 return True
431 return False
433 if 0x660 <= cp_value <= 0x669:
434 return not any(0x6F0 <= ord(cp) <= 0x06F9 for cp in label)
436 if 0x6F0 <= cp_value <= 0x6F9:
437 return not any(0x660 <= ord(cp) <= 0x0669 for cp in label)
439 return False
442def check_label(label: str | bytes | bytearray) -> None:
443 """Run the full set of IDNA 2008 validity checks on a single label.
445 Applies, in order: NFC normalisation (:func:`check_nfc`), hyphen
446 restrictions (:func:`check_hyphen_ok`), the no-leading-combiner rule
447 (:func:`check_initial_combiner`), per-codepoint validity (PVALID,
448 CONTEXTJ, CONTEXTO classes from :rfc:`5892`), and the Bidi Rule
449 (:func:`check_bidi`).
451 :param label: The label to validate. ``bytes`` or ``bytearray`` input
452 is decoded as UTF-8 first.
453 :raises IDNAError: If the label is empty or fails a structural rule.
454 :raises InvalidCodepoint: If the label contains a DISALLOWED or
455 UNASSIGNED codepoint.
456 :raises InvalidCodepointContext: If a CONTEXTJ or CONTEXTO codepoint
457 is not valid in its context.
458 :raises IDNABidiError: If the Bidi Rule is violated.
459 """
460 if len(label) > _max_input_length:
461 raise IDNAError("Label too long", code="input_too_long")
462 if isinstance(label, (bytes, bytearray)):
463 try:
464 label = label.decode("utf-8")
465 except UnicodeDecodeError as err:
466 raise IDNAError("Invalid UTF-8 in label", code="invalid_utf8") from err
467 if len(label) == 0:
468 raise IDNAError("Empty Label", code="empty_label")
469 if _ldh_label_re.match(label):
470 return
472 # Check against the domain length rather than the label length to
473 # support some UTS #46 use cases, while still bounding the work done
474 # by the label contextual rules below.
475 if not valid_string_length(label, trailing_dot=True):
476 raise IDNAError("Label too long", code="label_too_long")
478 check_nfc(label)
479 check_hyphen_ok(label)
480 check_initial_combiner(label)
482 for pos, cp in enumerate(label):
483 cp_value = ord(cp)
484 if intranges_contain(cp_value, idnadata.codepoint_classes["PVALID"]):
485 continue
486 if intranges_contain(cp_value, idnadata.codepoint_classes["CONTEXTJ"]):
487 try:
488 contextj_ok = valid_contextj(label, pos)
489 except ValueError as err:
490 raise IDNAError(
491 f"Unknown codepoint adjacent to joiner {_unot(cp_value)} at position {pos + 1} in {label!r}",
492 code="unknown_codepoint",
493 text=label,
494 codepoint=cp_value,
495 position=pos + 1,
496 ) from err
497 if not contextj_ok:
498 raise InvalidCodepointContext(
499 f"Joiner {_unot(cp_value)} not allowed at position {pos + 1} in {label!r}",
500 code="contextj",
501 text=label,
502 codepoint=cp_value,
503 position=pos + 1,
504 )
505 elif intranges_contain(cp_value, idnadata.codepoint_classes["CONTEXTO"]):
506 if not valid_contexto(label, pos):
507 raise InvalidCodepointContext(
508 f"Codepoint {_unot(cp_value)} not allowed at position {pos + 1} in {label!r}",
509 code="contexto",
510 text=label,
511 codepoint=cp_value,
512 position=pos + 1,
513 )
514 else:
515 raise InvalidCodepoint(
516 f"Codepoint {_unot(cp_value)} at position {pos + 1} of {label!r} not allowed",
517 code="disallowed_codepoint",
518 text=label,
519 codepoint=cp_value,
520 position=pos + 1,
521 )
523 check_bidi(label)
526def alabel(label: str) -> bytes:
527 """Convert a single U-label into its A-label form.
529 The result is the ASCII-Compatible Encoding (ACE) form per :rfc:`5891`
530 §4: the label is validated, Punycode-encoded, and prefixed with
531 ``xn--``. Pure ASCII labels that are already valid IDNA labels are
532 returned unchanged (as :class:`bytes`).
534 :param label: The label to convert, as a Unicode string.
535 :returns: The A-label as ASCII-encoded :class:`bytes`.
536 :raises IDNAError: If the label is invalid or the resulting A-label
537 exceeds 63 octets.
538 """
539 if len(label) > _max_input_length:
540 raise IDNAError("Label too long", code="input_too_long")
541 try:
542 label_bytes = label.encode("ascii")
543 except UnicodeEncodeError:
544 pass
545 else:
546 ulabel(label_bytes)
547 if not valid_label_length(label_bytes):
548 raise IDNAError("Label too long", code="label_too_long")
549 return label_bytes
551 check_label(label)
552 label_bytes = _alabel_prefix + _punycode(label)
554 if not valid_label_length(label_bytes):
555 raise IDNAError("Label too long", code="label_too_long")
557 return label_bytes
560def ulabel(label: str | bytes | bytearray) -> str:
561 """Convert a single A-label into its U-label form.
563 Performs the inverse of :func:`alabel`: an ``xn--``-prefixed label is
564 Punycode-decoded and validated, and is rejected unless it is the
565 canonical A-label for the decoded U-label (:rfc:`5891` §5.3). Labels
566 that are already Unicode (or plain ASCII without the ACE prefix) are
567 validated and returned as a Unicode string.
569 :param label: The label to convert. ``bytes`` or ``bytearray`` input
570 is treated as ASCII.
571 :returns: The U-label as a Unicode string.
572 :raises IDNAError: If the label is malformed or fails validation.
573 """
574 if len(label) > _max_input_length:
575 raise IDNAError("Label too long", code="input_too_long")
576 if not isinstance(label, (bytes, bytearray)):
577 try:
578 label_bytes = label.encode("ascii")
579 except UnicodeEncodeError:
580 check_label(label)
581 return label
582 else:
583 label_bytes = bytes(label)
584 if not label_bytes.isascii():
585 raise IDNAError("Invalid ASCII in A-label", code="invalid_ascii")
587 label_bytes = label_bytes.lower()
588 if label_bytes.startswith(_alabel_prefix):
589 label_bytes = label_bytes[len(_alabel_prefix) :]
590 if not label_bytes:
591 raise IDNAError("Malformed A-label, no Punycode eligible content found", code="invalid_alabel")
592 if label_bytes.endswith(b"-"):
593 raise IDNAError("A-label must not end with a hyphen", code="invalid_alabel")
594 else:
595 check_label(label_bytes)
596 return label_bytes.decode("ascii")
598 try:
599 label = label_bytes.decode("punycode")
600 except UnicodeError as err:
601 raise IDNAError("Invalid A-label", code="invalid_alabel") from err
602 # RFC 5891 §5.3: the label is rejected unless re-encoding the decoded
603 # form reproduces the (lowercased) input. This catches "fake A-labels"
604 # (RFC 5890 §2.3.2.1) such as ``xn---bbk``, a non-canonical Punycode
605 # spelling of ``xn--bbk`` that would otherwise decode to the same
606 # U-label and so display identically to a different wire-format name.
607 if _punycode(label) != label_bytes:
608 raise IDNAError("A-label is not the canonical Punycode encoding of its U-label", code="non_canonical_alabel")
609 check_label(label)
610 return label
613def _check_std3(text: str, domain: str, offset: int) -> None:
614 """Raise if ``text``, a slice of ``domain`` starting at ``offset`` that
615 UTS #46 mapping left unchanged, contains an ASCII character disallowed
616 under ``UseSTD3ASCIIRules``."""
617 match = _std3_disallowed_re.search(text)
618 if match:
619 codepoint = ord(match.group())
620 position = offset + match.start() + 1
621 raise InvalidCodepoint(
622 f"Codepoint {_unot(codepoint)} not allowed at position {position} in {domain!r}",
623 code="uts46_std3",
624 text=domain,
625 codepoint=codepoint,
626 position=position,
627 )
630def _warn_transitional() -> None:
631 warnings.warn(
632 "Transitional processing is deprecated in UTS #46 and has no effect. "
633 "The transitional argument will be removed in a future version.",
634 DeprecationWarning,
635 stacklevel=3,
636 )
639def uts46_remap(domain: str, std3_rules: bool = True, transitional: bool = False) -> str:
640 """Apply the UTS #46 character mapping to a domain string.
642 Implements the mapping table from `UTS #46 §4
643 <https://www.unicode.org/reports/tr46/>`_: each character is kept,
644 replaced, or rejected based on its status (``V``, ``M``, ``D``,
645 ``I``, ``X``). The result is returned in Normalisation Form C.
647 :param domain: The full domain name to remap.
648 :param std3_rules: If ``True``, apply UTS #46's ``UseSTD3ASCIIRules``:
649 after mapping, any ASCII character other than a lowercase letter,
650 digit, hyphen or the label separator ``.`` is rejected, whether it
651 appeared in the input or was produced by a mapping (e.g. U+FF01
652 FULLWIDTH EXCLAMATION MARK maps to ``!``). If ``False``, such
653 characters are passed through.
654 :param transitional: Deprecated and ignored. UTS #46 deprecated
655 transitional processing in Unicode 15.1 and deviation (status
656 ``D``) codepoints are now always kept, so this has no effect
657 beyond emitting a :class:`DeprecationWarning`. It will be removed
658 in a future version.
659 :returns: The remapped domain, in Normalisation Form C.
660 :raises InvalidCodepoint: If the domain contains a disallowed
661 codepoint under the chosen rules.
662 :raises IDNAError: If ``domain`` exceeds the defensive input length limit.
663 """
664 if transitional:
665 _warn_transitional()
666 if len(domain) > _max_input_length:
667 raise IDNAError("Domain too long", code="input_too_long")
668 if domain.isascii():
669 # The only ASCII mapping in UTS #46 is upper- to lowercase, and
670 # ASCII is invariant under NFC, so lowercasing is the whole job.
671 result = domain.lower()
672 if std3_rules:
673 _check_std3(result, domain, 0)
674 return result
676 from .uts46data import uts46_replacements, uts46_starts, uts46_statuses
678 # ``start`` marks the run of unchanged input not yet copied; a run is
679 # only sliced out when a character must be replaced or dropped, so the
680 # common no-change case makes no copy. STD3 is checked per output piece
681 # to report a violation at its input position.
682 output: list[str] = []
683 start = 0
684 for pos, char in enumerate(domain):
685 code_point = ord(char)
686 i = code_point if code_point < 256 else bisect.bisect_right(uts46_starts, code_point) - 1
687 status = uts46_statuses[i]
688 # UTS #46 §4: V valid, D deviation (kept), M mapped, I ignored,
689 # anything else disallowed.
690 if status == _STATUS_VALID:
691 continue
692 if status == _STATUS_MAPPED:
693 replacement = uts46_replacements[i]
694 elif status == _STATUS_DEVIATION:
695 continue
696 elif status == _STATUS_IGNORED:
697 replacement = None
698 else:
699 raise InvalidCodepoint(
700 f"Codepoint {_unot(code_point)} not allowed at position {pos + 1} in {domain!r}",
701 code="uts46_disallowed",
702 text=domain,
703 codepoint=code_point,
704 position=pos + 1,
705 )
706 if start < pos:
707 run = domain[start:pos]
708 if std3_rules:
709 _check_std3(run, domain, start)
710 output.append(run)
711 if replacement:
712 if std3_rules and _std3_disallowed_re.search(replacement):
713 raise InvalidCodepoint(
714 f"Codepoint {_unot(code_point)} not allowed at position {pos + 1} in {domain!r}",
715 code="uts46_std3",
716 text=domain,
717 codepoint=code_point,
718 position=pos + 1,
719 )
720 output.append(replacement)
721 start = pos + 1
723 if start == 0:
724 if std3_rules:
725 _check_std3(domain, domain, 0)
726 return unicodedata.normalize("NFC", domain)
727 tail = domain[start:]
728 if std3_rules:
729 _check_std3(tail, domain, start)
730 output.append(tail)
731 return unicodedata.normalize("NFC", "".join(output))
734def encode(
735 s: str | bytes | bytearray,
736 strict: bool = False,
737 uts46: bool = False,
738 std3_rules: bool = False,
739 transitional: bool = False,
740) -> bytes:
741 """Encode a Unicode domain name into its ASCII (A-label) form.
743 Splits the input on label separators (only ``U+002E`` if ``strict`` is
744 set; otherwise also IDEOGRAPHIC FULL STOP ``U+3002``, FULLWIDTH FULL
745 STOP ``U+FF0E``, and HALFWIDTH IDEOGRAPHIC FULL STOP ``U+FF61``),
746 encodes each label with :func:`alabel`, and rejoins them with ``.``.
747 Optionally pre-processes the input through :func:`uts46_remap`.
749 :param s: The domain name to encode.
750 :param strict: If ``True``, only ``U+002E`` is recognised as a label
751 separator.
752 :param uts46: If ``True``, apply UTS #46 mapping before encoding.
753 :param std3_rules: Forwarded to :func:`uts46_remap` when ``uts46`` is
754 ``True``.
755 :param transitional: Deprecated and ignored (see :func:`uts46_remap`):
756 emits a :class:`DeprecationWarning` and will be removed in a
757 future version.
758 :returns: The encoded domain as ASCII :class:`bytes`.
759 :raises IDNAError: If the domain is empty, contains an invalid label,
760 or exceeds the maximum domain length.
761 """
762 if transitional:
763 _warn_transitional()
764 if not isinstance(s, str):
765 try:
766 s = str(s, "ascii")
767 except (UnicodeDecodeError, TypeError) as err:
768 raise IDNAError(
769 "should pass a unicode string to the function rather than a byte string.", code="invalid_ascii"
770 ) from err
771 if len(s) > _max_input_length:
772 raise IDNAError("Domain too long", code="input_too_long")
773 if uts46:
774 s = uts46_remap(s, std3_rules)
776 if not valid_string_length(s, trailing_dot=True):
777 raise IDNAError("Domain too long", code="domain_too_long")
779 trailing_dot = False
780 result = []
781 labels = s.split(".") if strict else _unicode_dots_re.split(s)
782 if not labels or labels == [""]:
783 raise IDNAError("Empty domain", code="empty_domain")
784 if labels[-1] == "":
785 del labels[-1]
786 trailing_dot = True
787 for label in labels:
788 s = alabel(label)
789 if s:
790 result.append(s)
791 else:
792 raise IDNAError("Empty label", code="empty_label")
793 if trailing_dot:
794 result.append(b"")
795 s = b".".join(result)
796 if not valid_string_length(s, trailing_dot):
797 raise IDNAError("Domain too long", code="domain_too_long")
798 return s
801def decode(
802 s: str | bytes | bytearray,
803 strict: bool = False,
804 uts46: bool = False,
805 std3_rules: bool = False,
806 display: bool = False,
807) -> str:
808 """Decode an A-label-encoded domain name back to Unicode.
810 Splits the input on label separators (see :func:`encode` for the
811 rules), decodes each label with :func:`ulabel`, and rejoins them
812 with ``.``. Optionally pre-processes the input through
813 :func:`uts46_remap`.
815 :param s: The domain name to decode.
816 :param strict: If ``True``, only ``U+002E`` is recognised as a label
817 separator.
818 :param uts46: If ``True``, apply UTS #46 mapping before decoding.
819 :param std3_rules: Forwarded to :func:`uts46_remap` when ``uts46`` is
820 ``True``.
821 :param display: If ``True``, any ``xn--`` label that fails IDNA
822 validation is passed through unchanged (lowercased) rather than
823 aborting the whole call. Intended for "decode for display"
824 consumers (e.g. URL libraries, HTTP clients) that want to show
825 the user the label as it appears on the wire when it cannot be
826 rendered as Unicode. Matches the per-label recovery prescribed
827 by UTS #46 §4 and the WHATWG URL "domain to Unicode" algorithm.
828 :returns: The decoded domain as a Unicode string.
829 :raises IDNAError: If the input is not valid ASCII, contains an
830 invalid label, or is empty.
831 """
832 if not isinstance(s, str):
833 try:
834 s = str(s, "ascii")
835 except (UnicodeDecodeError, TypeError) as err:
836 raise IDNAError("Invalid ASCII in A-label", code="invalid_ascii") from err
837 if len(s) > _max_input_length:
838 raise IDNAError("Domain too long", code="input_too_long")
839 if uts46:
840 s = uts46_remap(s, std3_rules, False)
841 if not valid_string_length(s, trailing_dot=True):
842 raise IDNAError("Domain too long", code="domain_too_long")
843 trailing_dot = False
844 result = []
845 labels = s.split(".") if strict else _unicode_dots_re.split(s)
846 if not labels or labels == [""]:
847 raise IDNAError("Empty domain", code="empty_domain")
848 if not labels[-1]:
849 del labels[-1]
850 trailing_dot = True
851 for label in labels:
852 try:
853 u = ulabel(label)
854 except IDNAError:
855 if display and label[:4].lower() == "xn--":
856 u = label.lower()
857 else:
858 raise
859 if u:
860 result.append(u)
861 else:
862 raise IDNAError("Empty label", code="empty_label")
863 if trailing_dot:
864 result.append("")
865 return ".".join(result)