Exercise 3 solution import struct from collections import Counter class FormatError(ValueError): pass def tlv(t, v): return struct.pack(" 8: raise FormatError("nested too deeply") out, pos = [], 0 while pos < len(buf): if len(buf) - pos < 3: raise FormatError(f"truncated header at offset {pos}") t, n = struct.unpack_from(" len(buf): raise FormatError(f"record at {pos} overruns the data") v = buf[start:start + n] out.append((t, careful(v, depth + 1) if t == 0x10 else v)) pos = start + n return out def sloppy(buf, depth=0): out, pos = [], 0 while pos < len(buf): t = buf[pos] (n,) = struct.unpack_from(" {val:#04x}", seed[:i] + bytes([val]) + seed[i + 1:])) ok = rejected = 0 crashes = Counter() first = {} for label, case in cases: try: parser(case) ok += 1 except FormatError: rejected += 1 except Exception as e: kind = type(e).__name__ crashes[kind] += 1 first.setdefault(kind, label) return len(cases), ok, rejected, crashes, first seed = tlv(0x10, tlv(1, b"Bo") + tlv(0x10, tlv(1, b"Ada") + tlv(2, b"\x24"))) + tlv(1, b"top") print(len(seed), "seed bytes") for name, parser in (("careful", careful), ("sloppy", sloppy)): total, ok, rejected, crashes, first = fuzz(parser, seed) print(name, "cases", total, "accepted", ok, "rejected", rejected, "crashes", sum(crashes.values())) for kind, count in sorted(crashes.items()): print(" ", kind, count, "first:", first[kind]) deep = tlv(1, b"x") for _ in range(50): deep = tlv(0x10, deep) for name, parser in (("careful", careful), ("sloppy", sloppy)): try: parser(deep) print(name, "deep -> accepted") except FormatError as e: print(name, "deep ->", e) except Exception as e: print(name, "deep ->", type(e).__name__) # 27 seed bytes # careful cases 102 accepted 49 rejected 53 crashes 0 # sloppy cases 102 accepted 89 rejected 0 crashes 13 # error 13 first: cut at 1 # careful deep -> nested too deeply # sloppy deep -> accepted (Run as written; the output matched the comments.) Explanation: The fuzzer cuts the seed at every length and replaces every byte with three other values, then counts three outcomes: accepted, rejected with the expected error type, or crashed with something else. The careful parser has no crashes. The sloppy one never checks the length of what it reads, so a cut header raises struct.error, and it quietly accepts many damaged inputs, returning short or wrong data with no complaint. It also has no depth limit.