Skip to content

Instantly share code, notes, and snippets.

@mrwonko
Created December 29, 2024 11:44
Show Gist options
  • Save mrwonko/8f4b7797f5ff328d059c86398331ae15 to your computer and use it in GitHub Desktop.
Save mrwonko/8f4b7797f5ff328d059c86398331ae15 to your computer and use it in GitHub Desktop.
proof of concept for accessing a local directory from JavaScript and parsing a zip file
<!DOCTYPE html>
<html lang="en-US">
<head>
<title>Directory Access Proof-of-Concept</title>
<script>
const trailerSignature = new Uint8Array([0x50, 0x4b, 0x05, 0x06]); // "End of Central Directory" record
const showPicker = async () => {
// find assets0.pk3
const selectedDirHandle = await showDirectoryPicker();
let baseDirHandle = selectedDirHandle;
let assets0Handle;
try {
assets0Handle = await selectedDirHandle.getFileHandle("assets0.pk3");
} catch (e) {
if (e.name === "NotFoundError") {
try {
baseDirHandle = await selectedDirHandle.getDirectoryHandle("base");
assets0Handle = await baseDirHandle.getFileHandle("assets0.pk3")
} catch (e) {
if (e.name === "NotFoundError") {
alert("found neither assets0.pk3 nor base/assets0.pk3");
return;
} else {
throw e;
}
}
} else {
throw e;
}
}
const assets0File = await assets0Handle.getFile();
console.log(`assets0.pk3 size: ${assets0File.size} bytes`);
// read End of Central Directory record
// the .zip trailer contains a variable-length comment, so we have to make sure we read enough to get all of it
const minTrailerSize = 22;
// comment length is stored in 2 bytes
const maxCommentSize = (1 << 16) - 1;
const maxTrailerSize = minTrailerSize + maxCommentSize;
const trailerSlice = assets0File.slice(-maxTrailerSize);
const trailerArrayBuffer = await trailerSlice.arrayBuffer();
let trailerUint8Array = new Uint8Array(trailerArrayBuffer);
let trailerStart; // relative to -maxTrailerSize
for (let candidate = trailerUint8Array.length - minTrailerSize; candidate >= 0; candidate--) {
if (trailerUint8Array[candidate + 0] === trailerSignature[0] &&
trailerUint8Array[candidate + 1] === trailerSignature[1] &&
trailerUint8Array[candidate + 2] === trailerSignature[2] &&
trailerUint8Array[candidate + 3] === trailerSignature[3]) {
trailerStart = candidate;
break;
}
}
if (trailerStart === undefined) {
alert("could not find a valid zip trailer in assets0.pk3");
return;
}
console.log(`found end of central directory record ${-maxTrailerSize + trailerStart} bytes from end of assets0.pk3`);
trailerUint8Array = trailerUint8Array.slice(trailerStart);
// read first Central Directory entry
const centralDirectorySize = (trailerUint8Array[0xC] << 0) | (trailerUint8Array[0xD] << 8) | (trailerUint8Array[0xE] << 16) | (trailerUint8Array[0xF] << 24);
const centralDirectoryStart = (trailerUint8Array[0x10] << 0) | (trailerUint8Array[0x11] << 8) | (trailerUint8Array[0x12] << 16) | (trailerUint8Array[0x13] << 24);
const centralDirectorySlice = assets0File.slice(centralDirectoryStart, centralDirectoryStart + centralDirectorySize);
const centralDirectoryArrayBuffer = await centralDirectorySlice.arrayBuffer();
const centralDirectoryUint8Array = new Uint8Array(centralDirectoryArrayBuffer);
const fileNameLength = (centralDirectoryUint8Array[0x1C] << 0) | (centralDirectoryUint8Array[0x1D] << 8);
const fileNameOffset = 0x2e;
let fileName = "";
for (let i = fileNameOffset; i < fileNameOffset + fileNameLength; i++) {
fileName += String.fromCharCode(centralDirectoryUint8Array[i]);
}
const externalAttributes = (centralDirectoryUint8Array[0x26] << 0) | (centralDirectoryUint8Array[0x27] << 8) | (centralDirectoryUint8Array[0x28] << 16) | (centralDirectoryUint8Array[0x29] << 24);
const isDir = Boolean(externalAttributes & 0x10);
alert(`the first entry in assets0.pk3 is ${fileName}, it's a ${isDir ? "directory" : "file"}`);
};
</script>
</head>
<body>
<button type="button" onclick="showPicker()">select Jedi Academy folder</button>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment