Exercise 1 solution import struct MAGIC = b"INV1" MAX_ITEMS = 100 MIN_ITEM = 1 + 4 + 2 MAX_FILE = 65536 class FormatError(ValueError): pass class Reader: def __init__(self, buf): self.buf = bytes(buf) self.pos = 0 def remaining(self): return len(self.buf) - self.pos def take(self, n): if n < 0 or n > self.remaining(): raise FormatError(f"need {n} bytes at offset {self.pos}, {self.remaining()} left") chunk = self.buf[self.pos:self.pos + n] self.pos += n return chunk def u8(self): return self.take(1)[0] def u16(self): return struct.unpack(" MAX_FILE: raise FormatError(f"file of {len(buf)} bytes is over the {MAX_FILE} limit") r = Reader(buf) if r.take(4) != MAGIC: raise FormatError("bad magic") version = r.u8() if version != 1: raise FormatError(f"unsupported version {version}") count = r.u16() if count > MAX_ITEMS: raise FormatError(f"{count} items is over the {MAX_ITEMS} limit") if count * MIN_ITEM > r.remaining(): raise FormatError(f"{count} items cannot fit in {r.remaining()} bytes") items = [] for i in range(count): n = r.u8() if n == 0: raise FormatError(f"item {i} has an empty name") start = r.pos try: name = r.take(n).decode("utf-8") except UnicodeDecodeError: raise FormatError(f"item {i}: bad UTF-8 at offset {start}") from None items.append((name, r.u32(), r.u16())) if r.remaining(): raise FormatError(f"{r.remaining()} trailing byte(s) after offset {r.pos}") return items good = build([("bolt", 120, 15), ("nut", 300, 9), ("washer", 0, 2)]) print(len(good), good[:11].hex(" ")) print(parse(good)) print(parse(build([]))) cases = [ ("wrong magic", b"XXXX" + good[4:]), ("bad version", build([("a", 1, 1)], version=2)), ("cut short", good[:20]), ("name overruns", MAGIC + struct.pack(" accepted") except FormatError as e: print(label, "->", e) # 41 49 4e 56 31 01 03 00 04 62 6f 6c # [('bolt', 120, 15), ('nut', 300, 9), ('washer', 0, 2)] # [] # wrong magic -> bad magic # bad version -> unsupported version 2 # cut short -> 3 items cannot fit in 13 bytes # name overruns -> need 200 bytes at offset 8, 9 left # trailing byte -> 1 trailing byte(s) after offset 41 # huge count -> 60000 items is over the 100 limit # count beyond data -> 50 items cannot fit in 8 bytes # empty name -> item 0 has an empty name # bad UTF-8 -> item 0: bad UTF-8 at offset 8 # oversize file -> file of 70041 bytes is over the 65536 limit # empty file -> need 4 bytes at offset 0, 0 left (Run as written; the output matched the comments.) Explanation: Every read goes through one helper that checks the request against the bytes that remain, so a lying length can only produce a FormatError, never a short slice or a crash. The count is checked twice: against a fixed limit, and against how many records could possibly fit in the data left. Text is decoded inside a try block so a bad byte sequence becomes the same error type, and bytes left over after the last record are refused.