A Weird Imagination

Extracting Tametsi puzzles in the browser

Posted in

The problem#

Previously, I figured out how to extract Tametsi's puzzles, but I wanted to make something user-friendly that made use of those puzzles, so I didn't want to require people to install something or run console commands. I had also figured out how to get Java programs running in a browser, so I figured it would be straightforward to combine the two. As you may have guessed from this paragraph being in the "the problem" section, it was not. Specifically, while the Java command-line will read tametsi.exe as a JAR file, Doppio gives the error

Invalid Zip file: Central directory record has invalid signature

The solution#

As a workaround, use a different library to unzip tametsi.exe. There's no need to present it as a JAR file instead of a directory, so no need to rezip it:

var fs = BrowserFS.BFSRequire("fs");

async function unzipToDirectory(zipfile, dir) {
  const z = new zip.fs.FS();
  await z.importBlob(zipfile);

  async function extract(z, dir) {
    fs.mkdir(dir, true)
    if (z.directory) {
      const childDir = z.name
        ? `${dir}/${z.name}`
        : dir;
      for (const child of z.children) {
        await extract(child, childDir);
      }
    } else {
      fs.writeFileSync(
        `${dir}/${z.name}`,
        new buffer(await (await z.getBlob()).arrayBuffer()));
    }
  }

  await extract(z.root, dir);
}

To call that, in the uploadFile() function, replace the reader.onload with

await unzipToDirectory(f, process.cwd() + '/tametsi');

The details#

ZIP file format#

JAR files are ZIP files that follow a specific directory layout, which is why they can be handled by libraries that read ZIP files. But the file is named tametsi.exe, taking advantage of a ZIP design decision to allow the start and end of ZIP files to be arbitrary data so a ZIP file can also be a valid file of many other file types. This is commonly used to make ZIP files executable by putting a valid executable at the start that reads itself to load the zipped data. In this case, it's used to start the Java runtime to actually play the game.

In order to find the actual starting point for interpreting the ZIP file, the library has to look for the signature byte string identifying the end of central directory record, which may be up to 64 KiB before the end of the file. From there, it can find all of the central directory file headers describing where to find the files stored in the ZIP file.

The error message#

The message is a very generic failure to interpret the metadata describing the files stored in the ZIP file. I found some similar errors reported and the recommendation was to either confirm the file is not corrupted or try to read the ZIP file with different software.

I never managed to debug in enough detail to figure out the precise issue, but apparently the ZIP library in BrowserFS used by Doppio is not as forgiving of the extra data at the start of the file as it is supposed to be, causing it to try to read a central directory record from the wrong location. So I went with the suggestion of trying different software.

Newer BrowserFS?#

The first thing to try when a library doesn't work is to try updating to the latest version. But since Doppio is pretty tightly integrated with BrowserFS that seemed unlikely to be a simple change. And there's a several-year-old issue about using the latest BrowserFS, which is not encouraging. Additionally, the BrowserFS GitHub page points to a newer fork called ZenFS (which does have its own ZIP module), which would likely be even harder to get Doppio to use.

Doppio's unzip#

Doppio actually also has an Unzip.class which uses java.util.zip.* to unzip files. Or does it? Actually running it gives a familiar error:

Error: EINVAL: Invalid Zip file: Central directory record has invalid signature: 128979713

But, furthermore, the stacktrace points stright into BrowserFS, suggesting that the Doppio implementation of java.util.zip.* is actually backed by BrowserFS. In other words, this is just another entrypoint to the exact same code.

Different library#

As mentioned above, this led me to trying to just use a completely separate JavaScript ZIP library that hopefully doesn't have the same bug. I had needed to handle ZIP files in the browser for an older blog post, so I searched for that and adapted the code, which uses the zip.js library. I just had to modify it a bit to write into BrowserFS instead of OPFS, which resulted in the unzipToDirectory() function above.

Adding to classpath#

From Java's perspective, a JAR file in a classpath is treated the same as a directory, so unzipping the JAR file works just as well as using the JAR file directly.

Next steps#

While I got this working in a prototype, I still need to package it up into a nice web page and actually get around to writing the page that will make use of the Tametsi puzzles.

Comments

Have something to add? Post a comment by sending an email to comments@aweirdimagination.net. You may use Markdown for formatting.

There are no comments yet.