Skip to main content

Indie game storeFree gamesFun gamesHorror games
Game developmentAssetsComics
SalesBundles
Jobs
TagsGame Engines

[Tool] BuildHarbor: raw ZIP path checks before itch.io HTML5 uploads

A topic by P4ODEBATATA created 13 days ago Views: 130 Replies: 2
Viewing posts 1 to 3

HTML5 uploads can fail for boring ZIP reasons: index.html nested in an extra folder, case-sensitive asset paths, absolute URLs, or archive entries written with backslashes.


I built BuildHarbor as a small offline checker for itch.io creators. It reads the ZIP structure locally, flags root entry and asset-path issues, can apply conservative repairs, and exports TXT/JSON reports plus a repaired ZIP.


Version 1.1.0 now inspects raw central-directory filename bytes before library normalization, so it can detect and normalize backslash separators without changing the original archive.


Project and screenshots: https://p4odebatata.itch.io/buildharbor-html5-zip-repair


If you publish HTML5 builds, which ZIP or asset-path failure has caused you the most trouble?

The backslash case cost us the most, and the part worth warning people about is that the usual ways of checking it all lie. We had packages we suspected were written with backslash separators. Python's zipfile and .NET's ZipArchive both reported them clean, and those two normalise in opposite directions, so the fact that they agreed meant nothing at all. Only scanning the raw central directory bytes settled it, and the answer was yes, the archives really were backslashed. So reading filename bytes before library normalisation isn't a small detail, it's the only thing that answers the question. Any checker built on a zip library inherits that library's opinion about separators and will quietly certify the exact thing it exists to catch. Case sensitivity was second for us, and it only ever showed up after upload, because the local filesystem didn't care.

Case sensitivity, and it is not close — mostly because the machine you build on physically cannot reproduce it. macOS and Windows are both case-insensitive by default, so textures/Ball.png requested as textures/ball.png works locally, works under a local http server, works in every check you can run before upload, and 404s the instant it is served off Linux. Same shape as your backslash finding, really: the layer that could tell you is the layer that normalises.

Which is why I would push on the tool’s scope rather than its checks 🔍 — a ZIP checker validates the archive against itself, and a case mismatch is not a property of the archive. The archive is fine. The code is asking for a name that is not in it. Catching it means cross-referencing two different things: the entry names in the central directory, and the strings your HTML/JS/CSS actually request. Structure alone cannot see it, however carefully you read the bytes.

And that drags in the part I would design around rather than bolt on. A static reference scan has to cover at least three syntaxes — src and href attributes, string literals in JS, url() in CSS — and it will still miss everything that is not a literal. A good half of our texture loads look like loader.load(“textures/” + name + “.ktx2”), and nothing resolves that statically.

So: report coverage, not a verdict. “Resolved 214 of 230 references; 16 constructed at runtime, not checkable” is useful. A green tick on a codebase made of template literals is actively worse than no check, because it certifies clean on exactly the references it never looked at — which is the same failure mode you just fixed at the byte level, one layer up.

The complement that costs nothing to ship: after upload, open the published embed and read the network panel for non-200s. No session, no credentials, no tooling — a browser and one look. It catches case mismatches, absolute URLs, mixed content and genuinely missing files in a single pass, because it asks the only question that finally matters: what did the browser request, and did it get it. Pre-upload structural checks and a post-upload 404 sweep catch nearly disjoint sets of failures, which to me is an argument for the second one living inside the first rather than in a README.

One question, since you are already down at the central directory: how often do you see general purpose bit 11 wrong — a non-ASCII entry name written as UTF-8 with the flag left unset, so it is spec’d as CP437? That is the only path failure I know of where a human reading the ZIP listing will also read it as correct, which seems like precisely your tool’s territory.