#!/usr/bin/env python3 try: from contextlib import nullcontext except ImportError: # Python 3.6 and below # One-liner because I can. nullcontext = type("nc", (object,), {"__init__": (lambda self, v: setattr(self, "v", v)), "__enter__": (lambda self: self.v), "__exit__": (lambda self, *args, **kwargs: None)}) import json import sys def parse_dict(f): d = {} while True: c = f.read(1) if c == b'e': return d elif b'0' <= c <= b'9': key = parse_str(f, c) value = parse_any(f) d[key] = value else: raise ValueError(f'invalid bencode value: {c} instead of expected e or digit') def parse_list(f): l = [] while True: c = f.read(1) if c == b'e': return l l.append(parse_any(f, c)) def _parse_int_bytes(f, stopChar): # Read until encountering a non-decimal character; if it isn't stopChar, raise a ValueError v = b'' # Python 3.8+ (walrus) #while b'0' <= (c := f.read(1)) <= b'9': # v += c while True: c = f.read(1) if b'0' <= c <= b'9': v += c else: break if c == stopChar: return v raise ValueError(f'invalid bencode value: {c} instead of expected {stopChar}') def parse_int(f): return int(_parse_int_bytes(f, b'e')) def parse_str(f, c): v = _parse_int_bytes(f, b':') length = int(c + v) read = 0 buf = [] while read < length: d = f.read(length - read) buf.append(d) read += len(d) buf = b''.join(buf) try: return buf.decode('utf-8', 'strict') except UnicodeDecodeError: return buf.decode('iso-8859-1') def parse_any(f, c = None): if c is None: c = f.read(1) if c == b'd': return parse_dict(f) elif c == b'l': return parse_list(f) elif c == b'i': return parse_int(f) elif b'0' <= c <= b'9': return parse_str(f, c) else: raise ValueError(f'invalid bencode value: {c} instead of expected d, l, i, or digit') if sys.argv[1:]: c = open(sys.argv[1], 'rb') else: c = nullcontext(sys.stdin.buffer) with c as fp: print(json.dumps(parse_any(fp)))