import re from typing import List from .net_port import is_port NUMBER = re.compile(r"[0-9]+") def _trim_blank(s: str) -> str: """Spaces and tabs only: a newline inside a port list is a mistake, not padding.""" return s.strip(" \t") def _is_number(s: str) -> bool: """Whole ASCII digits with no leading zero: "80", not "080", "+80" or "٨٠".""" return NUMBER.fullmatch(s) is not None and (len(s) == 1 or s[0] != "0") def _port(token: str) -> int: # More than five digits cannot be a port. value = 0 if len(token) > 5 else int(token) if not is_port(value): raise ValueError('port "%s" is out of range 1-65535' % token) return value def parse_port_list(text: str) -> List[int]: """Parse a port list such as "22,80,8000-8010" into port numbers: each port once, in the order first given. Ranges are inclusive. """ if not isinstance(text, str): raise TypeError("text must be a string") if _trim_blank(text) == "": raise ValueError("port list is empty") out: List[int] = [] seen = set() def add(p: int) -> None: if p not in seen: seen.add(p) out.append(p) for raw in text.split(","): item = _trim_blank(raw) if item == "": raise ValueError("empty item in port list") if "-" in item: ends = item.split("-") if len(ends) != 2 or not _is_number(ends[0]) or not _is_number(ends[1]): raise ValueError('invalid port range "%s"' % item) low = _port(ends[0]) high = _port(ends[1]) if low > high: raise ValueError('port range "%s" runs backwards' % item) for p in range(low, high + 1): add(p) else: if not _is_number(item): raise ValueError('invalid port "%s"' % item) add(_port(item)) return out