"""Torrent metainfo — .torrent file parsing. Parses BitTorrent .torrent files (bencoded metainfo dictionaries) to extract tracker URL, file list, piece hashes, and info hash. Ported from org.klomp.snark.MetaInfo. """ from __future__ import annotations import hashlib import math from dataclasses import dataclass, field from i2p_apps.snark.bencode import bdecode, bencode @dataclass class FileEntry: """A file within a torrent.""" path: str length: int class MetaInfo: """Parsed torrent metainfo.""" def __init__( self, announce: str, name: str, piece_length: int, pieces: bytes, total_size: int, files: list[FileEntry], info_hash: bytes, ) -> None: self.announce = announce self.name = name self.piece_length = piece_length self.pieces = pieces self.total_size = total_size self.files = files self.info_hash = info_hash @property def piece_count(self) -> int: return max(1, math.ceil(self.total_size / self.piece_length)) @property def is_i2p(self) -> bool: """Check if this torrent uses an I2P tracker.""" return ".i2p" in self.announce @classmethod def from_bytes(cls, data: bytes) -> MetaInfo: """Parse a .torrent file from raw bytes.""" torrent = bdecode(data) if not isinstance(torrent, dict): raise ValueError("Invalid torrent: not a dictionary") announce = torrent.get(b"announce", b"").decode("utf-8", errors="replace") info = torrent[b"info"] name = info.get(b"name", b"unknown").decode("utf-8", errors="replace") piece_length = info[b"piece length"] pieces = info[b"pieces"] # Calculate info hash info_hash = hashlib.sha1(bencode(info)).digest() # nosec B324 — BitTorrent protocol requires SHA-1 info hash # Parse files if b"files" in info: # Multi-file torrent files = [] total_size = 0 for f in info[b"files"]: path_parts = [p.decode("utf-8", errors="replace") for p in f[b"path"]] path = "/".join(path_parts) length = f[b"length"] files.append(FileEntry(path=path, length=length)) total_size += length else: # Single-file torrent total_size = info[b"length"] files = [FileEntry(path=name, length=total_size)] return cls( announce=announce, name=name, piece_length=piece_length, pieces=pieces, total_size=total_size, files=files, info_hash=info_hash, )