Rebrand the Rust client crate (coven/ → hh/, package+binary "hack-house"), README, CLI strings, and branch (coven → hack-house). Gitea repo renamed cmd-chat → hack-house to match. Crypto/server logic unchanged; selftest + golden-vector test still green, binary is now `hack-house`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
129 lines
4.0 KiB
Python
129 lines
4.0 KiB
Python
"""Command-line interface for the :mod:`idna` package.
|
|
|
|
Invoked via ``python -m idna``. See :func:`main` for the entry point.
|
|
"""
|
|
|
|
import argparse
|
|
import sys
|
|
from collections.abc import Iterable
|
|
from itertools import chain
|
|
from typing import IO, Optional
|
|
|
|
from . import IDNAError, decode, encode
|
|
from .core import _alabel_prefix, _unicode_dots_re
|
|
from .package_data import __version__
|
|
|
|
|
|
def _looks_like_alabel(s: str) -> bool:
|
|
"""Return True if any label in ``s`` carries the ``xn--`` ACE prefix."""
|
|
prefix = _alabel_prefix.decode("ascii")
|
|
return any(label.lower().startswith(prefix) for label in _unicode_dots_re.split(s))
|
|
|
|
|
|
def _build_parser() -> argparse.ArgumentParser:
|
|
parser = argparse.ArgumentParser(
|
|
prog="python -m idna",
|
|
description=(
|
|
"Convert a domain name between its Unicode (U-label) and "
|
|
"ASCII-compatible (A-label) forms. With no mode flag, the "
|
|
"direction is chosen from the first input — if it contains "
|
|
"an xn-- label the stream is decoded, otherwise it is "
|
|
"encoded — and the same mode is applied to every remaining "
|
|
"input. UTS #46 mapping is applied by default; pass "
|
|
"--strict to disable it. When no domains are given on the "
|
|
"command line and stdin is piped, one domain per line is "
|
|
"read from stdin."
|
|
),
|
|
)
|
|
mode = parser.add_mutually_exclusive_group()
|
|
mode.add_argument(
|
|
"-e",
|
|
"--encode",
|
|
dest="mode",
|
|
action="store_const",
|
|
const="encode",
|
|
help="Encode the input to its ASCII A-label form.",
|
|
)
|
|
mode.add_argument(
|
|
"-d",
|
|
"--decode",
|
|
dest="mode",
|
|
action="store_const",
|
|
const="decode",
|
|
help="Decode the input from its ASCII A-label form.",
|
|
)
|
|
parser.add_argument(
|
|
"--strict",
|
|
action="store_true",
|
|
help="Disable the default UTS #46 mapping and apply IDNA 2008 rules verbatim.",
|
|
)
|
|
parser.add_argument(
|
|
"--version",
|
|
action="version",
|
|
version=f"idna {__version__}",
|
|
)
|
|
parser.add_argument(
|
|
"domain",
|
|
nargs="*",
|
|
help="One or more domain names to convert. Omit to read from stdin.",
|
|
)
|
|
return parser
|
|
|
|
|
|
def _iter_stdin(stream: IO[str]) -> Iterable[str]:
|
|
"""Yield non-empty stripped lines from ``stream``, ignoring blanks."""
|
|
for line in stream:
|
|
stripped = line.strip()
|
|
if stripped:
|
|
yield stripped
|
|
|
|
|
|
def _convert_one(domain: str, mode: str, uts46: bool) -> bool:
|
|
"""Convert ``domain`` and write the result; return ``False`` on failure."""
|
|
try:
|
|
if mode == "decode":
|
|
print(decode(domain, uts46=uts46))
|
|
else:
|
|
print(encode(domain, uts46=uts46).decode("ascii"))
|
|
except IDNAError as err:
|
|
print(f"idna: {mode} failed for {domain!r}: {err}", file=sys.stderr)
|
|
return False
|
|
return True
|
|
|
|
|
|
def main(argv: Optional[list[str]] = None) -> int:
|
|
"""Entry point for ``python -m idna``.
|
|
|
|
When more than one domain is supplied (via positional arguments or
|
|
piped stdin) and no mode flag is given, the first input determines
|
|
the direction and that mode is applied uniformly to the rest.
|
|
|
|
:param argv: Argument list excluding the program name. Defaults to
|
|
:data:`sys.argv` when ``None``.
|
|
:returns: ``0`` on success, ``1`` if any conversion fails.
|
|
"""
|
|
parser = _build_parser()
|
|
args = parser.parse_args(argv)
|
|
uts46 = not args.strict
|
|
|
|
if args.domain:
|
|
domains: Iterable[str] = args.domain
|
|
elif not sys.stdin.isatty():
|
|
domains = _iter_stdin(sys.stdin)
|
|
else:
|
|
parser.error("a domain argument is required when stdin is a terminal")
|
|
|
|
iterator = iter(domains)
|
|
first = next(iterator, None)
|
|
if first is None:
|
|
return 0
|
|
|
|
mode = args.mode or ("decode" if _looks_like_alabel(first) else "encode")
|
|
|
|
results = [_convert_one(domain, mode, uts46) for domain in chain([first], iterator)]
|
|
return 0 if all(results) else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|