Skip to content

Disc images and other sources ​

Two crates let the viewer and the tests read the game without an extracted copy: disc-image reads a CD image in place, and game-source puts every place the files can come from behind one interface. Neither ships a game file or a download URL. Both are written for reuse: disc-image knows nothing of POD and is a candidate for GameFrame's shared crates/.

crates/disc-image ​

PartWhat it reads
CUE sheet (cue.rs)FILE, TRACK, INDEX, PREGAP; other commands are read past. Times are mm:ss:ff, 75 frames a second
Tracks (disc.rs)Sector sizes 2048 (cooked), 2336 and 2352 (raw). User data of a raw sector: Mode 1 at +16, Mode 2 Form 1 at +24 (after the 8-byte subheader), Mode 0 is zeros. A Form 2 sector (subheader byte 2, bit 5) is refused: its 2324 bytes are not part of an ISO 9660 file
A BIN or ISO alone (Disc::from_raw)The sync pattern (00 FF×10 00) at sector 16 or 0 means raw 2352 bytes a sector, with the mode byte at +15; CD001 at 16×2048+1 means a cooked ISO; at 16×2336+9, raw 2336
ISO 9660 (iso9660.rs)The descriptors from sector 16 to the terminator (type 255); the primary descriptor's identifiers, block count and dates; the directory tree, with the hidden flag (bit 0) and directory flag (bit 1); names without ;1 and a trailing dot. A supplementary descriptor with the escape %/@, %/C or %/E at +88 is Joliet: its tree gives the names (UCS-2, big-endian). Files in several extents are refused
Random access (Volume::read_range)Only the sectors that hold the range are read, a few hundred at a time

A track's start is its disc address: the sum of the frames of the files before it, the pregaps up to it and its INDEX 01. ISO 9660 counts in these addresses.

All reading goes through one trait, ReadAt (read_at(offset, buf) and len()), with three sources: FileSource (positioned reads of a file on disk), Memory, and ChunkCache. The chunk cache is for sources that cannot answer at once, such as an HTTP server or a file dropped into a browser. It holds fixed-size chunks, and a read that needs one it lacks fails with WouldBlock and a Missing { cache, chunks } naming them. The caller fetches those chunks and runs the read again, so the parsing code stays synchronous and is the same everywhere.

Tested ​

  • Measured, synthetic: a 28-sector volume built in the test (tests/synthetic.rs), as a cooked ISO, raw Mode 1, raw Mode 2 Form 1, a CUE sheet with an audio track and a pregap in one file, and a CUE sheet with one file per track; with and without a Joliet tree. Reads across a sector boundary, at the very end, and one byte past it. The chunk cache reports only the chunks a read needs.
  • Measured, the current POD image (tests/user_image.rs, skipped when target/pod/cd/POD2_0.CUE is absent): track 1 Mode 1/2352, 231,180 sectors; track 2 audio, 65,880 sectors after a 150-sector pregap; volume POD2_0, 231,028 blocks, created 2000-09-14 21:17:27, with a Joliet tree; 951 files (6 hidden: the AVI videos in DATA/BINARY/VIDEO) in 49 directories. These are the counts GameFrame's discinfo.py reports. Every one of the 951 files, 471,896,442 bytes in all, is byte-identical to the copy in target/pod/disc/.

crates/game-source ​

The Source trait (describe, list, read_range, read, read_all) has these sources:

SourceWhere
FolderA folder of files: a copy of the disc, an install folder
ImageThe files of a disc image's volume. A subfolder (the viewer uses DATA) can be the root, so the paths match a copy of that folder
download::fetch (native)A URL the user gives: a .bin, .iso or .cue (the files its sheet names are fetched from beside it). It downloads into a cache folder (the viewer's default is target/download/), resumes a broken download with Range: bytes=N-, keeps NAME.url so another URL is never mistaken for the same file, and computes each image file's MD5 (kept in NAME.md5). Every --md5 given must match one of them, or it stops and leaves the files there to look at
remote::RemoteImageAn image fetched on demand through a Fetch trait, with chunks kept in a ChunkStore. Runs of up to 16 consecutive 1 MiB chunks go in one request
web (wasm32)HttpFetch (range requests), FileFetch (slices of a dropped File), and the stores Opfs (the origin private file system, checked with a test write) and Idb (IndexedDB, used where OPFS cannot write)

MD5 is implemented in the crate (md5.rs, RFC 1321's test suite) to check downloads. It is for knowing the bytes are the expected ones, not for security.

Tested ​

  • Measured: a local test server that cuts its first answer off after 100,000 of 300,000 bytes; the download resumes, matches the MD5, refuses a wrong MD5, and refuses a second URL with the same file name (download.rs).
  • Measured: an image read on demand fetches the descriptors and directories in one request and a 1.5 MB file's two chunks in another; opened again, everything comes from the store with no fetch (remote.rs).
  • The browser range reader and dropped-file reader are covered by their source and cache tests. A full catalogue read of the current POD image in the browser has not been timed yet.

Fetching from archive.org in the browser ​

Measured on 2026-10-23 from a page on http://127.0.0.1:5184, with a public-domain item: a fetch of https://archive.org/download/ITEM/FILE fails. The download URL answers 302 to a data node (dnNNNNNN.ca.archive.org/0/items/...). Neither the redirect nor the data node sends Access-Control-Allow-Origin, so the browser blocks the response. The data node does answer range requests (206 with Content-Range, checked with curl), and it answers a CORS preflight with 405. archive.org/metadata/... does send Access-Control-Allow-Origin: *. A one-part Range is a CORS-safelisted header, so no preflight would be needed if the header were there.

So ?image= cannot read an archive.org download directly. Two ways work. Natively, pod-viewer --url URL --md5 HEX downloads it once. In the browser, a server on 127.0.0.1 that answers ranges and allows the page can serve it, and viewer/serve-image.sh is that server for a folder with the image in it. Whether to fetch the game from an archive is the user's choice (AGENTS.md, "The user decides"); the code takes the URL from the user.

Not done ​

  • A CUE sheet with WAVE, MP3 or MOTOROLA files: the tracks are listed, and the audio is not decoded or byte-swapped.
  • ISO 9660 interleaved files (file unit size), files in several extents, Rock Ridge names, and HFS on hybrid discs.
  • A ZIP or 7z archive at the URL: the user unpacks it first.
  • The viewer still reads every file before showing the list. Reading a file only when it is chosen would let the browser start sooner and use less memory.

Prose CC0 1.0, code Apache 2.0. POD: Planet of Death is © Ubi Soft; none of its files are on this site.