Files
J1coding 05d94ed82d iOS: download the RetroArch shader collection from inside the app
867 presets over 27 categories, one manifest and one zip each, and no third
request anywhere. A tester asked for what Manic EMU has: a button that
fetches the collection instead of making people find a zip and side-load it.
The closure resolution and the licence sign-off landed first; this is the
phone half.

Order is the whole safety argument, because a remote manifest is attacker-
controlled if the host is. The stated size is refused before the transfer
rather than after -- the manifest carries it, so the refusal costs nothing.
The received byte count and the SHA-256 are both compared before the
importer is called, and the hash is streamed rather than read whole. The
relative path is validated before it becomes a URL, because .. and / both
survive percent-encoding. Then the fenced extractor does the writing,
unchanged, so there is no second containment guard to get wrong.

The manifest is 8 MB raw and 312 KB gzipped, and 96% of those bytes are the
per-file array. The entry type does not declare that key, so it is skipped:
the zip carries its own hash and that covers every file inside it. What
lands in the cache is this build's own projection rather than the served
bytes, which is also what makes browsing work with no network -- a failed
refresh ages the list instead of emptying it.

Two things the import path never had. Staging files are swept at launch,
because defer does not run when iOS kills a backgrounded app mid-download,
which is the ordinary outcome and not an edge case. And cancelling removes
the pack if the extract already began, which is not the same as stopping it;
the comment says so rather than implying otherwise.

The importer returns the name it installed instead of only publishing it.
One property on a shared object is fine for one caller and wrong for a
screen with 867 rows and no reason to install them one at a time: two
installs overwrite each other's answer, which would write one entry's marker
into the other's folder and make cancelling one delete the other.

Three fixes in the code around it, from the same review. A loaded chain
owned a render target and a pipeline per pass and nothing freed any of it
when the player turned shaders off, because DestroyShaderChain had exactly
two callers, a preset change and device teardown. The Metal frame path
flushed on success and returned on failure, though a chain that failed
partway has already encoded passes into the same command buffer and needs
the submit for the same reason the success path does. And the pack extractor
held every file's bytes resident to the 32 MB cap, because the autoreleased
data was never drained inside the loop.

The catalogue is not published yet. The base URL is one constant, and an INI
key no UI writes can repoint it, accepting only https and file -- which is
how a simulator reads a local emit, since ATS refuses plain HTTP and there
is no reason to weaken it for a test.

Eleven checks in the new fence, six mutations run against the real source
and all six caught, each restored byte for byte. Still open and written down
rather than left to be rediscovered: the extractor's per-entry decompressed
cap is applied after the entry is fully inflated, so a crafted zip can spend
up to that cap before the refusal. Bounding it earlier needs a streaming
inflate.

The extractor's own fence gains an ordering claim. It asserted that a
canonical resolve appears somewhere in the method, which passes for a
resolve whose answer is discarded; deleting the entire containment refusal
left it green. It anchors on resolvedParent now, because the body carries
several refusal sites naming the same constants and anything looser is
satisfied by a neighbouring refusal that has nothing to do with containment
-- which is the same trap the first attempt at this fix fell into.
2026-08-18 23:41:53 +02:00
..