"""Bencode decoder for tracker announce responses. Adapted from newTrackon (MIT). Rewritten for this architecture. Peers are validated structurally and discarded by callers; this module never persists peer addresses. """ from __future__ import annotations from collections import OrderedDict from socket import AF_INET, AF_INET6, inet_ntop from struct import unpack_from from typing import Any TOK_DICT = b"d" TOK_LIST = b"l" TOK_INT = b"i" TOK_END = b"e" TOK_STR_SEP = b":" BDecoded = OrderedDict[bytes, Any] | list[Any] | int | bytes | None class BencodeError(ValueError): pass def bdecode(data: bytes) -> dict[str, Any]: """Decode a bencoded tracker response into a string-keyed dict. Binary `peers` / `peers6` are converted to length-checked peer lists for validation, then callers must discard addresses immediately. """ if not data: raise BencodeError("empty bencode payload") decoded = Decoder(data).decode() if not isinstance(decoded, OrderedDict): raise BencodeError("tracker response must be a dict") response: dict[str, Any] = {} for key, value in decoded.items(): response[key.decode("utf-8", errors="replace")] = value if "peers" in response and isinstance(response["peers"], bytes): response["peers"] = decode_binary_peers_list(response["peers"], 0, AF_INET) if "peers6" in response and isinstance(response["peers6"], bytes): response["peers6"] = decode_binary_peers_list(response["peers6"], 0, AF_INET6) for key, value in list(response.items()): if isinstance(value, bytes): response[key] = value.decode("utf-8", errors="replace") return response def decode_binary_peers_list(buf: bytes, offset: int, ip_family: int) -> list[dict[str, Any]]: """Validate compact peer binary layout; return ephemeral peer dicts.""" peers: list[dict[str, Any]] = [] peer_length = 6 if ip_family == AF_INET else 18 view = memoryview(buf) while offset < len(buf): if len(buf) < offset + peer_length: break ip_bytes = bytes(view[offset : offset + peer_length - 2]) try: ip_str = inet_ntop(ip_family, ip_bytes) except OSError as exc: raise BencodeError("invalid compact peer address") from exc offset += peer_length - 2 port = unpack_from("!H", buf, offset)[0] offset += 2 peers.append({"ip": ip_str, "port": port}) return peers def validate_peers_then_discard(response: dict[str, Any]) -> bool: """Return True if peers/peers6 field exists and is structurally valid, then clear it.""" has_peers = False for key in ("peers", "peers6"): if key not in response: continue value = response[key] if isinstance(value, list): has_peers = True elif isinstance(value, (bytes, str)): # dictionary-model peers — accept presence without storing has_peers = True response[key] = None return has_peers class Decoder: def __init__(self, data: bytes) -> None: self.index = 0 self.data = data def decode(self) -> BDecoded: c = self.peek() if c is None: raise BencodeError("unexpected EOF") if c == TOK_DICT: self.read(1) return self.decode_dict() if c == TOK_LIST: self.read(1) return self.decode_list() if c == TOK_INT: self.read(1) return self.decode_int() if c in b"0123456789": return self.decode_str() raise BencodeError("invalid bencode token") def peek(self) -> bytes | None: if self.index >= len(self.data): return None return self.data[self.index : self.index + 1] def read(self, length: int) -> bytes: if self.index + length > len(self.data): raise BencodeError("unexpected EOF while reading") result = self.data[self.index : self.index + length] self.index += length return result def read_until(self, token: bytes) -> bytes: loc = self.data.find(token, self.index) if loc == -1: raise BencodeError("token not found") result = self.data[self.index : loc] self.index = loc + 1 return result def decode_dict(self) -> OrderedDict[bytes, BDecoded]: result: OrderedDict[bytes, BDecoded] = OrderedDict() while self.data[self.index : self.index + 1] != TOK_END: key = self.decode() if not isinstance(key, bytes): raise BencodeError("dict key must be bytes") result[key] = self.decode() self.read(1) return result def decode_list(self) -> list[BDecoded]: result: list[BDecoded] = [] while self.data[self.index : self.index + 1] != TOK_END: result.append(self.decode()) self.read(1) return result def decode_int(self) -> int: return int(self.read_until(TOK_END)) def decode_str(self) -> bytes: length = int(self.read_until(TOK_STR_SEP)) if length < 0: raise BencodeError("negative string length") return self.read(length)