Created
December 29, 2024 11:44
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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