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