Add EPK decompiler

todo: SHA1 checks for decompiling old format EPK
This commit is contained in:
ayunami2000 2022-10-02 20:10:53 -04:00
parent 84d25525b1
commit 5d2f7ba203
4 changed files with 357 additions and 1 deletions

244
ayunEPKDecompiler.js Normal file
View File

@ -0,0 +1,244 @@
window.decompileEPK = function () {
let currentOffset = 0;
let numFiles = 0;
function detectOldHeader(epkData) {
const oldHeader = "EAGPKG!!";
for (let i = 0; i < oldHeader.length; i++) {
if (epkData[i] != oldHeader.charCodeAt(i)) return false;
}
return true;
}
function readASCII(epkData) {
const len = read(epkData);
let str = "";
for (let i = 0; i < len; i++) {
str += String.fromCharCode(read(epkData));
}
return str;
}
function read(epkData) {
return epkData[currentOffset++];
}
function skip(num) {
currentOffset += num;
}
function loadShort(epkData) {
return (read(epkData) << 8) | read(epkData);
}
function loadInt(epkData) {
return (read(epkData) << 24) | (read(epkData) << 16) | (read(epkData) << 8) | read(epkData);
}
function readUTF(epkData) {
const len = loadShort(epkData);
let str = "";
for (let i = 0; i < len; i++) {
str += String.fromCharCode(read(epkData));
}
return str;
}
// https://stackoverflow.com/a/18639903/6917520
const crc32 = (function() {
let table = new Uint32Array(256);
for (let i = 256; i--;) {
let tmp = i;
for (let k = 8; k--;) {
tmp = tmp & 1 ? 3988292384 ^ tmp >>> 1 : tmp >>> 1;
}
table[i] = tmp;
}
return function(data) {
let crc = -1;
for (let i = 0, l = data.length; i < l; i++) {
crc = crc >>> 8 ^ table[crc & 255 ^ data[i]];
}
return (crc ^ -1) >>> 0;
};
})();
function decompileOld(epkData) {
readUTF(epkData);
try {
let zData = pako.inflate(epkData.slice(currentOffset));
currentOffset = 0;
return readFilesOld(zData);
} catch (err) {
return null;
}
}
function decompileNew(epkData) {
const vers = readASCII(epkData);
if (!vers.startsWith("ver2.")) {
return null;
}
skip(read(epkData));
skip(loadShort(epkData));
skip(8);
numFiles = loadInt(epkData);
const compressionType = String.fromCharCode(read(epkData));
let zData = epkData.slice(currentOffset);
if (compressionType == "Z" || compressionType == "G") {
try {
zData = pako.inflate(zData);
} catch (err) {
return null;
}
} else if (compressionType == "0") {
// do nothing
} else {
return null;
}
currentOffset = 0;
return readFilesNew(zData);
}
function readFilesOld(data) {
let files = [];
let file;
while ((file = readFileOld(data)) != null) {
if (file == -1) return null;
files.push(file);
}
return files;
}
function readFileOld(data) {
const s = readUTF(data);
if (s == " end") {
return null;
} else if (s != "<file>") {
return -1;
}
const path = readUTF(data);
skip(20);
const len = loadInt(data);
const blob = new Blob([data.slice(currentOffset, currentOffset + len)]);
skip(len);
if (readUTF(data) != "</file>") {
return -1;
}
return {
type: "FILE",
name: path,
data: blob
};
}
function readFilesNew(data) {
let files = [];
let file;
while ((file = readFileNew(data)) != null) {
if (file == -1) return null;
files.push(file);
}
return files;
}
function readFileNew(data) {
const type = String.fromCharCode(read(data), read(data), read(data), read(data));
if (numFiles == 0) {
if (type != "END$") {
return -1;
}
return null;
}
if (type == "END$") {
return -1;
}
const name = readASCII(data);
const len = loadInt(data);
let blob = null;
if (type == "FILE") {
if (len < 5) {
return -1;
}
const crc = loadInt(data);
const blobBuffer = data.slice(currentOffset, currentOffset + len - 5);
skip(len - 5);
blob = new Blob([blobBuffer]);
if (crc != (crc32(blobBuffer) | 0)) {
return -1;
}
if (read(data) != 58) {
return -1;
}
} else {
blob = new Blob([data.slice(currentOffset, currentOffset + len)]);
skip(len);
}
if (read(data) != 62) {
return -1;
}
numFiles--;
return {
type: type,
name: name,
data: blob
};
}
return async function(rawBuffer) {
let epkData = new Uint8Array(rawBuffer);
if (detectOldHeader(epkData)) {
epkData = epkData.slice(8);
return decompileOld(epkData);
}
const header = "EAGPKG$$";
for (let i = 0; i < header.length; i++) {
if (epkData[i] != header.charCodeAt(i)) return null;
}
const endCode = ":::YEE:>";
for (let i = 0; i < endCode.length; i++) {
if (epkData[epkData.length - 8 + i] != endCode.charCodeAt(i)) return null;
}
epkData = epkData.slice(8, -8);
return decompileNew(epkData);
};
};

109
decompile.html Normal file
View File

@ -0,0 +1,109 @@
<!DOCTYPE html>
<html>
<head>
<script async src="https://arc.io/widget.min.js#HEPCFa3K"></script>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>ayunWebEPK</title>
<script type="text/javascript" src="jszip.min.js"></script>
<script type="text/javascript" src="pako_inflate.min.js"></script>
<script type="text/javascript" src="ayunEPKDecompiler.js"></script>
<link rel="preconnect" href="https://fonts.googleapis.com"/>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/>
<link href="https://fonts.googleapis.com/css2?family=Nunito&display=swap" rel="stylesheet"/>
<style>
a:not([href]) {
display: none;
}
*, ::file-selector-button {
font-family: 'Nunito', sans-serif;
}
body {
background-color: #111111;
color: #dddddd;
}
input[disabled] {
opacity: 0.8;
}
::file-selector-button, progress {
background-color: #343434;
color: #eeeeee;
border: 1px solid #eeeeee;
border-radius: 4px;
}
a, input[type=file] {
color: #dddddd;
}
progress {
height: 1em;
}
progress, ::-webkit-file-upload-button {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}
progress[value="0"][max="1"] {
display: none;
}
::-moz-progress-bar {
background-color: #eeeeee;
}
::-webkit-progress-value {
background-color: #eeeeee;
}
::-webkit-progress-bar {
background-color: #343434;
border-radius: 4px;
}
</style>
</head>
<body>
<sub><a href="..">Want to compile one instead?</a></sub>
<h1>ayunWebEPK</h1>
<p>Decompile EPK files in your browser!</p>
Select .EPK file: <input type="file" onchange="selectFile(this);" accept=".epk"/>
<br/>
<progress value="0" max="1"></progress>
<a download="my-cool.zip">Download as a ZIP!</a>
<script>
const downloadLink = document.querySelectorAll('a')[1];
const progressBar = document.querySelector('progress');
function selectFile(fileElem) {
downloadLink.removeAttribute('href');
fileElem.disabled = true;
if (fileElem.files.length > 0) {
const epkFile = fileElem.files[0];
const reader = new FileReader();
reader.onload = function(e) {
window.decompileEPK()(e.target.result).then(function(fileList) {
if (fileList == null) {
alert("Invalid EPK!");
fileElem.removeAttribute('disabled');
return;
}
const zip = new JSZip();
progressBar.max = fileList.length;
for (const file of fileList) {
progressBar.value++;
if (file.type != 'FILE') continue;
zip.file(file.name, file.data); // todo: check if it'll accept a blob as input
}
zip.generateAsync({type: 'blob'}).then(function(content) {
downloadLink.href = window.URL.createObjectURL(content);
fileElem.value = '';
progressBar.value = 0;
progressBar.max = 1;
fileElem.removeAttribute('disabled')
});
});
};
reader.readAsArrayBuffer(epkFile);
} else {
fileElem.removeAttribute('disabled');
}
}
</script>
</body>
</html>

View File

@ -102,6 +102,7 @@
</style>
</head>
<body>
<sub><a href="decompile.html">Want to decompile one instead?</a></sub>
<h1>ayunWebEPK</h1>
<p>Compile EPK files in your browser!</p>
Use legacy format: <input type="checkbox"/>
@ -142,7 +143,7 @@
};
})();
const downloadLink = document.querySelector('a');
const downloadLink = document.querySelectorAll('a')[1];
const progressBar = document.querySelector('progress');
const fileElems = document.querySelectorAll('input[type=file]');
const oldBox = document.querySelector('input[type=checkbox]');

2
pako_inflate.min.js vendored Normal file

File diff suppressed because one or more lines are too long