Python Challenge - Level 6

Problem

The very first line of the HTML source is the hint:

<html>
  <!-- <-- zip -->
</html>

The URL path is channel — change it to channel.zip to download a ZIP archive.

Inside the ZIP, each entry's comment field contains the name of the next entry to visit — a linked list hidden in ZIP metadata. Follow the chain from entry 90052, collecting each comment along the way. The concatenated comments spell the answer.

Solution

Python

zipfile.ZipFile gives direct access to per-entry comments via ZipInfo.comment:

import urllib.request, zipfile, io

data = urllib.request.urlopen(
    "http://www.pythonchallenge.com/pc/def/channel.zip"
).read()

with zipfile.ZipFile(io.BytesIO(data)) as zf:
    name = "90052"
    comments = []
    while True:
        info = zf.getinfo(name + ".txt")
        next_name = zf.read(name + ".txt").decode().strip()
        comments.append(info.comment.decode())
        if not next_name.isdigit():
            print("Final content:", next_name)
            break
        name = next_name

print("".join(comments))  # hockey

Go

Go's archive/zip exposes zip.File.Comment per entry:

package main

import (
    "archive/zip"
    "bytes"
    "fmt"
    "io"
    "net/http"
    "strings"
)

func main() {
    resp, _ := http.Get("http://www.pythonchallenge.com/pc/def/channel.zip")
    data, _ := io.ReadAll(resp.Body)
    resp.Body.Close()

    r, _ := zip.NewReader(bytes.NewReader(data), int64(len(data)))

    // Build a lookup map: filename → *zip.File
    files := make(map[string]*zip.File)
    for _, f := range r.File {
        files[f.Name] = f
    }

    name := "90052.txt"
    var comments strings.Builder
    for {
        f := files[name]
        comments.WriteString(f.Comment)

        rc, _ := f.Open()
        body, _ := io.ReadAll(rc)
        rc.Close()

        next := strings.TrimSpace(string(body))
        if next == "" || !isDigit(next) {
            fmt.Println("Final:", next)
            break
        }
        name = next + ".txt"
    }
    fmt.Println(comments.String()) // hockey
}

func isDigit(s string) bool {
    for _, c := range s {
        if c < '0' || c > '9' {
            return false
        }
    }
    return true
}

Rust

use std::io::{Cursor, Read};
use zip::ZipArchive;

fn main() {
    let bytes = reqwest::blocking::get(
        "http://www.pythonchallenge.com/pc/def/channel.zip"
    ).unwrap().bytes().unwrap();

    let mut archive = ZipArchive::new(Cursor::new(bytes)).unwrap();
    let mut name = "90052.txt".to_string();
    let mut comments = String::new();

    loop {
        let mut file = archive.by_name(&name).unwrap();
        comments.push_str(file.comment());

        let mut content = String::new();
        file.read_to_string(&mut content).unwrap();
        let next = content.trim().to_string();

        if next.is_empty() || !next.chars().all(|c| c.is_ascii_digit()) {
            println!("Final: {}", next);
            break;
        }
        name = format!("{}.txt", next);
    }
    println!("{}", comments); // hockey
}

Language Comparison

Feature Python Go Rust
ZIP reading zipfile.ZipFile — stdlib archive/zip — stdlib zip crate (external)
Per-entry comment ZipInfo.comment (bytes) zip.File.Comment (string) ZipFile::comment() (string)
In-memory ZIP from bytes io.BytesIO(data) bytes.NewReader(data) Cursor::new(bytes)
Entry lookup by name zf.getinfo(name) — O(n) scan Build a map[string]*zip.File archive.by_name(name) — O(n) scan

Key insight: All three languages can parse ZIP files from memory without writing to disk — using Python's BytesIO, Go's bytes.Reader, or Rust's Cursor<T>. The ZIP spec has a per-entry comment field (separate from the file content) that is rarely used in practice — this puzzle is a great reminder it exists.

Next Level

http://www.pythonchallenge.com/pc/def/hockey.html → redirects to ../return/good.html