Exercise 1 solution import struct, zlib MAGIC = b"TBX1" VERSION = 1 HEADER = struct.Struct("<4sBBH") # magic, version, flags, entry count ENTRY = struct.Struct(" 255: raise FormatError("entry name too long") def write_tbx(entries, compress=False): if len(entries) > MAX_ENTRIES: raise FormatError("too many entries") seen, body = set(), [] for name, data in entries: check_name(name) if name in seen: raise FormatError(f"duplicate name {name!r}") seen.add(name) if len(data) > MAX_ORIGINAL: raise FormatError(f"{name!r} is over {MAX_ORIGINAL} bytes") stored = zlib.compress(data, 9) if compress else data raw_name = name.encode("utf-8") body.append(ENTRY.pack(len(raw_name), len(stored), len(data), zlib.crc32(data))) body.append(raw_name) body.append(stored) flags = FLAG_ZLIB if compress else 0 return HEADER.pack(MAGIC, VERSION, flags, len(entries)) + b"".join(body) 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 unpack(self, st): return st.unpack(self.take(st.size)) def inflate(data, expected): d = zlib.decompressobj() try: out = d.decompress(data, expected + 1) except zlib.error as e: raise FormatError(f"corrupt zlib data: {e}") from e if len(out) != expected or not d.eof or d.unused_data: raise FormatError(f"zlib data does not match the declared size {expected}") return out def read_tbx(buf): if len(buf) > MAX_FILE: raise FormatError(f"file over {MAX_FILE} bytes") r = Reader(buf) magic, version, flags, count = r.unpack(HEADER) if magic != MAGIC: raise FormatError(f"bad magic {magic!r}") if version != VERSION: raise FormatError(f"unsupported version {version}") if flags & ~KNOWN_FLAGS: raise FormatError(f"unknown flag bits {flags & ~KNOWN_FLAGS:#04x}") if count > MAX_ENTRIES: raise FormatError(f"entry count {count} over limit {MAX_ENTRIES}") if count * (ENTRY.size + 1) > r.remaining(): raise FormatError(f"entry count {count} cannot fit in {r.remaining()} bytes") out, seen = [], set() for _ in range(count): start = r.pos name_len, stored, original, crc = r.unpack(ENTRY) try: name = r.take(name_len).decode("utf-8") except UnicodeDecodeError as e: raise FormatError(f"entry at {start}: name is not UTF-8") from e check_name(name) if name in seen: raise FormatError(f"duplicate name {name!r}") seen.add(name) if original > MAX_ORIGINAL: raise FormatError(f"{name!r}: declared size {original} over limit") data = r.take(stored) if flags & FLAG_ZLIB: data = inflate(data, original) elif stored != original: raise FormatError(f"{name!r}: stored size {stored} != original size {original}") if zlib.crc32(data) != crc: raise FormatError(f"{name!r}: checksum mismatch") out.append((name, data)) if r.remaining(): raise FormatError(f"{r.remaining()} trailing bytes at offset {r.pos}") return out import pathlib, tempfile def pack_dir(path, compress=False): entries = [(p.name, p.read_bytes()) for p in sorted(pathlib.Path(path).iterdir()) if p.is_file()] return write_tbx(entries, compress) def unpack_to(buf, dest): dest = pathlib.Path(dest) entries = read_tbx(buf) # validate everything before touching the disk for name, _ in entries: if (dest / name).exists(): raise FormatError(f"{name!r} already exists") for name, data in entries: (dest / name).write_bytes(data) return [name for name, _ in entries] with tempfile.TemporaryDirectory() as tmp: tmp = pathlib.Path(tmp) src, dst = tmp / "src", tmp / "dst" src.mkdir() dst.mkdir() (src / "notes.txt").write_bytes(b"remember the milk\n") (src / "pixels.bin").write_bytes(bytes(range(256)) * 4) (src / "sub").mkdir() # directories are skipped archive = pack_dir(src, compress=True) print(len(archive), "byte archive") print(unpack_to(archive, dst)) same = all((src / n).read_bytes() == (dst / n).read_bytes() for n in ("notes.txt", "pixels.bin")) print("identical:", same) try: unpack_to(archive, dst) except FormatError as e: print("second unpack ->", e) evil_name = b"../evil.txt" evil = (HEADER.pack(MAGIC, VERSION, 0, 1) + ENTRY.pack(len(evil_name), 4, 4, zlib.crc32(b"boom")) + evil_name + b"boom") try: unpack_to(evil, dst) except FormatError as e: print("evil archive ->", e) print(sorted(p.name for p in tmp.iterdir()), sorted(p.name for p in dst.iterdir())) # 365 byte archive # ['notes.txt', 'pixels.bin'] # identical: True # second unpack -> 'notes.txt' already exists # evil archive -> bad entry name '../evil.txt' # ['dst', 'src'] ['notes.txt', 'pixels.bin'] (Run as written; the output matched the comments.) Explanation: pack_dir turns the regular files of a folder into entries (sorted, so the archive is the same every time). unpack_to calls read_tbx first, so a bad archive is rejected before any file is created, then checks for existing files, then writes. Because check_name refuses "/", "\\" and "..", an archive cannot name a file outside the destination. The archive size depends on the zlib version (check).