mirror of
https://github.com/lax1dude/eagler-binary-tools.git
synced 2024-12-03 05:54:14 -08:00
Implement EPK compiler/decompiler
This commit is contained in:
commit
9a9e50e223
7
.gitattributes
vendored
Normal file
7
.gitattributes
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
#
|
||||
# https://help.github.com/articles/dealing-with-line-endings/
|
||||
#
|
||||
|
||||
*.bat text eol=crlf
|
||||
*.sh text eol=lf
|
||||
gradlew text eol=lf
|
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
.classpath
|
||||
.project
|
||||
bin
|
194
src/main/java/net/lax1dude/eaglercraft/bintools/EPKCompiler.java
Normal file
194
src/main/java/net/lax1dude/eaglercraft/bintools/EPKCompiler.java
Normal file
|
@ -0,0 +1,194 @@
|
|||
package net.lax1dude.eaglercraft.bintools;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.zip.CRC32;
|
||||
import java.util.zip.Deflater;
|
||||
import java.util.zip.DeflaterOutputStream;
|
||||
|
||||
import net.lax1dude.eaglercraft.bintools.utils.GZIPOutputStream2;
|
||||
|
||||
/**
|
||||
* Copyright (c) 2022-2024 lax1dude. All Rights Reserved.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
||||
* IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
|
||||
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
||||
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
* POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
*/
|
||||
public class EPKCompiler {
|
||||
|
||||
public static void _main(String[] args) throws IOException {
|
||||
if (args.length < 2 || args.length > 4) {
|
||||
System.out.println("Usage: epkcompiler <input directory> <output file> [gzip|zlib|none] [file-type]");
|
||||
return;
|
||||
}
|
||||
|
||||
File root = new File(args[0]);
|
||||
File output = new File(args[1]);
|
||||
char compressionType;
|
||||
|
||||
if (args.length > 2) {
|
||||
if (args[2].equalsIgnoreCase("gzip")) {
|
||||
compressionType = 'G';
|
||||
} else if (args[2].equalsIgnoreCase("zlib")) {
|
||||
compressionType = 'Z';
|
||||
} else if (args[2].equalsIgnoreCase("none")) {
|
||||
compressionType = '0';
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unknown compression method: " + args[2]);
|
||||
}
|
||||
} else {
|
||||
compressionType = 'G';
|
||||
}
|
||||
|
||||
System.out.println("Scanning input directory...");
|
||||
|
||||
ArrayList<File> files = new ArrayList();
|
||||
listDirectory(root, files);
|
||||
ByteArrayOutputStream osb = new ByteArrayOutputStream();
|
||||
String start = root.getAbsolutePath();
|
||||
|
||||
System.out.println("Compiling: " + output.getAbsolutePath());
|
||||
|
||||
osb.write("EAGPKG$$".getBytes(StandardCharsets.US_ASCII));
|
||||
|
||||
String chars = "ver2.0";
|
||||
osb.write(chars.length());
|
||||
osb.write(chars.getBytes(StandardCharsets.US_ASCII));
|
||||
|
||||
Date d = new Date();
|
||||
|
||||
String comment = "\n\n # Eagler EPK v2.0 - Generated by EaglerBinaryTools\n" + " # update: on "
|
||||
+ (new SimpleDateFormat("MM/dd/yyyy")).format(d) + " at "
|
||||
+ (new SimpleDateFormat("hh:mm:ss aa")).format(d) + "\n\n";
|
||||
|
||||
String nm = output.getName();
|
||||
osb.write(nm.length());
|
||||
osb.write(nm.getBytes(StandardCharsets.US_ASCII));
|
||||
|
||||
writeShort(comment.length(), osb);
|
||||
osb.write(comment.getBytes(StandardCharsets.US_ASCII));
|
||||
|
||||
writeLong(d.getTime(), osb);
|
||||
writeInt(files.size() + 1, osb);
|
||||
|
||||
osb.write(compressionType);
|
||||
|
||||
OutputStream os;
|
||||
|
||||
if (compressionType == 'G') {
|
||||
System.out.println("Using GZIP compression");
|
||||
os = new GZIPOutputStream2(osb, 9, 16384, true);
|
||||
} else if (compressionType == 'Z') {
|
||||
System.out.println("Using ZLIB (DEFLATE) compression");
|
||||
os = new DeflaterOutputStream(osb, new Deflater(9), 16384, true);
|
||||
} else {
|
||||
System.out.println("Using no compression");
|
||||
os = osb;
|
||||
}
|
||||
|
||||
os.write("HEAD".getBytes(StandardCharsets.US_ASCII));
|
||||
String key = "file-type";
|
||||
os.write(key.length());
|
||||
os.write(key.getBytes(StandardCharsets.US_ASCII));
|
||||
String value;
|
||||
if (args.length > 3) {
|
||||
value = args[3];
|
||||
} else {
|
||||
value = "epk/resources";
|
||||
}
|
||||
writeInt(value.length(), os);
|
||||
os.write(value.getBytes(StandardCharsets.US_ASCII));
|
||||
os.write('>');
|
||||
|
||||
CRC32 checkSum = new CRC32();
|
||||
for (File f : files) {
|
||||
InputStream stream = new FileInputStream(f);
|
||||
byte[] targetArray = new byte[(int) f.length()];
|
||||
stream.read(targetArray);
|
||||
stream.close();
|
||||
|
||||
checkSum.reset();
|
||||
checkSum.update(targetArray, 0, targetArray.length);
|
||||
int ch = (int) checkSum.getValue();
|
||||
|
||||
os.write("FILE".getBytes(StandardCharsets.US_ASCII));
|
||||
|
||||
String p = f.getAbsolutePath().replace(start, "").replace('\\', '/');
|
||||
if (p.startsWith("/")) {
|
||||
p = p.substring(1);
|
||||
}
|
||||
os.write(p.length());
|
||||
os.write(p.getBytes(StandardCharsets.US_ASCII));
|
||||
writeInt(targetArray.length + 5, os);
|
||||
writeInt(ch, os);
|
||||
|
||||
os.write(targetArray);
|
||||
os.write(':');
|
||||
os.write('>');
|
||||
}
|
||||
|
||||
os.write("END$".getBytes(StandardCharsets.US_ASCII));
|
||||
os.close();
|
||||
|
||||
osb.write(":::YEE:>".getBytes(StandardCharsets.US_ASCII));
|
||||
|
||||
System.out.println("Compiled " + files.size() + " files into the EPK");
|
||||
System.out.println("Writing to disk...");
|
||||
|
||||
FileOutputStream out = new FileOutputStream(output);
|
||||
out.write(osb.toByteArray());
|
||||
out.close();
|
||||
}
|
||||
|
||||
public static void writeShort(int i, OutputStream os) throws IOException {
|
||||
os.write((i >> 8) & 0xFF);
|
||||
os.write(i & 0xFF);
|
||||
}
|
||||
|
||||
public static void writeInt(int i, OutputStream os) throws IOException {
|
||||
os.write((i >> 24) & 0xFF);
|
||||
os.write((i >> 16) & 0xFF);
|
||||
os.write((i >> 8) & 0xFF);
|
||||
os.write(i & 0xFF);
|
||||
}
|
||||
|
||||
public static void writeLong(long i, OutputStream os) throws IOException {
|
||||
os.write((int) ((i >> 56) & 0xFF));
|
||||
os.write((int) ((i >> 48) & 0xFF));
|
||||
os.write((int) ((i >> 40) & 0xFF));
|
||||
os.write((int) ((i >> 32) & 0xFF));
|
||||
os.write((int) ((i >> 24) & 0xFF));
|
||||
os.write((int) ((i >> 16) & 0xFF));
|
||||
os.write((int) ((i >> 8) & 0xFF));
|
||||
os.write((int) (i & 0xFF));
|
||||
}
|
||||
|
||||
public static void listDirectory(File dir, ArrayList<File> files) {
|
||||
for (File f : dir.listFiles()) {
|
||||
if (f.isDirectory()) {
|
||||
listDirectory(f, files);
|
||||
} else {
|
||||
files.add(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
package net.lax1dude.eaglercraft.bintools;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.zip.Deflater;
|
||||
import java.util.zip.DeflaterOutputStream;
|
||||
|
||||
/**
|
||||
* Copyright (c) 2022-2024 lax1dude. All Rights Reserved.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
||||
* IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
|
||||
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
||||
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
* POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
*/
|
||||
public class EPKCompilerLegacy {
|
||||
|
||||
public static void _main(String[] args) throws IOException, NoSuchAlgorithmException {
|
||||
if (args.length != 2) {
|
||||
System.out.print("Usage: legacy-epkcompiler <input directory> <output file>");
|
||||
return;
|
||||
}
|
||||
File root = new File(args[0]);
|
||||
ArrayList<File> files = new ArrayList();
|
||||
System.out.println("Scanning input directory...");
|
||||
listDirectory(root, files);
|
||||
ByteArrayOutputStream osb = new ByteArrayOutputStream();
|
||||
DataOutputStream os = new DataOutputStream(osb);
|
||||
String start = root.getAbsolutePath();
|
||||
File output = new File(args[1]);
|
||||
System.out.println("Compiling: " + output.getAbsolutePath());
|
||||
os.write("EAGPKG!!".getBytes(StandardCharsets.UTF_8));
|
||||
os.writeUTF(
|
||||
"\n\n # eaglercraft package file - assets copyright mojang ab\n # eagler eagler eagler eagler eagler eagler eagler\n\n");
|
||||
Deflater d = new Deflater(9);
|
||||
os = new DataOutputStream(new DeflaterOutputStream(osb, d));
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-1");
|
||||
for (File f : files) {
|
||||
os.writeUTF("<file>");
|
||||
String p = f.getAbsolutePath().replace(start, "").replace('\\', '/');
|
||||
if (p.startsWith("/"))
|
||||
p = p.substring(1);
|
||||
os.writeUTF(p);
|
||||
|
||||
InputStream stream = new FileInputStream(f);
|
||||
byte[] targetArray = new byte[stream.available()];
|
||||
stream.read(targetArray);
|
||||
stream.close();
|
||||
|
||||
os.write(md.digest(targetArray));
|
||||
os.writeInt(targetArray.length);
|
||||
os.write(targetArray);
|
||||
os.writeUTF("</file>");
|
||||
}
|
||||
os.writeUTF(" end");
|
||||
os.flush();
|
||||
os.close();
|
||||
|
||||
System.out.println("Compiled " + files.size() + " files into the EPK");
|
||||
System.out.println("Writing to disk...");
|
||||
|
||||
FileOutputStream out = new FileOutputStream(output);
|
||||
out.write(osb.toByteArray());
|
||||
out.close();
|
||||
}
|
||||
|
||||
public static void listDirectory(File dir, ArrayList<File> files) {
|
||||
for (File f : dir.listFiles()) {
|
||||
if (f.isDirectory()) {
|
||||
listDirectory(f, files);
|
||||
} else {
|
||||
files.add(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
package net.lax1dude.eaglercraft.bintools;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import net.lax1dude.eaglercraft.bintools.utils.EPKDecompilerSP;
|
||||
|
||||
/**
|
||||
* Copyright (c) 2022-2024 lax1dude. All Rights Reserved.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
||||
* IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
|
||||
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
||||
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
* POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
*/
|
||||
public class EPKDecompiler {
|
||||
|
||||
public static void _main(String[] args) throws IOException {
|
||||
if (args.length != 2) {
|
||||
System.out.print("Usage: epkdecompiler <input epk> <output folder>");
|
||||
return;
|
||||
}
|
||||
File input = new File(args[0]);
|
||||
if(!input.isFile()) {
|
||||
System.err.println("Input file does not exist!");
|
||||
return;
|
||||
}
|
||||
System.out.println("Decompiling: " + input.getAbsolutePath());
|
||||
File output = new File(args[1]);
|
||||
byte[] inputBytes = new byte[(int)input.length()];
|
||||
try(FileInputStream fis = new FileInputStream(input)) {
|
||||
fis.read(inputBytes);
|
||||
}
|
||||
EPKDecompilerSP epkDecompiler = new EPKDecompilerSP(inputBytes);
|
||||
if(epkDecompiler.isOld()) {
|
||||
System.out.println("Detected legacy EPK format!");
|
||||
}
|
||||
int filesWritten = 0;
|
||||
try {
|
||||
EPKDecompilerSP.FileEntry f = null;
|
||||
while((f = epkDecompiler.readFile()) != null) {
|
||||
if(f.type.equals("HEAD")) {
|
||||
System.out.println("Skipping HEAD: \"" + f.name + "\": \"" + (new String(f.data, StandardCharsets.US_ASCII)) + "\"");
|
||||
}else if(f.type.equals("FILE")) {
|
||||
String safeName = f.name.replace('\\', '/');
|
||||
if(safeName.startsWith("../") || safeName.contains("/../") || safeName.endsWith("/..") || safeName.equals("..")) {
|
||||
System.out.println("Skipping unsafe relative path: \"" + f.name + "\"");
|
||||
}else {
|
||||
File destFile = new File(output, safeName);
|
||||
File parent = destFile.getParentFile();
|
||||
if(!parent.isDirectory()) {
|
||||
if(!parent.mkdirs()) {
|
||||
throw new IOException("Could not create directory: " + parent.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
try(FileOutputStream fos = new FileOutputStream(destFile)) {
|
||||
fos.write(f.data);
|
||||
++filesWritten;
|
||||
}
|
||||
}
|
||||
}else {
|
||||
System.err.println("Skipping unknown entry type \"" + f.type + "\" name \"" + f.name + "\", data is " + f.data.length + " bytes");
|
||||
}
|
||||
}
|
||||
}finally {
|
||||
epkDecompiler.close();
|
||||
}
|
||||
System.out.println("Extracted " + filesWritten + " from the EPK");
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
package net.lax1dude.eaglercraft.bintools;
|
||||
|
||||
/**
|
||||
* Copyright (c) 2024 lax1dude. All Rights Reserved.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
||||
* IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
|
||||
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
||||
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
* POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
*/
|
||||
public class EaglerBinaryTools {
|
||||
|
||||
public static void main(String[] args) throws Throwable {
|
||||
if(args.length < 1) {
|
||||
usage();
|
||||
return;
|
||||
}
|
||||
String[] argz = new String[args.length - 1];
|
||||
System.arraycopy(args, 1, argz, 0, argz.length);
|
||||
switch(args[0].toLowerCase()) {
|
||||
case "epkcompiler":
|
||||
EPKCompiler._main(argz);
|
||||
return;
|
||||
case "legacy-epkcompiler":
|
||||
case "legacyepkcompiler":
|
||||
EPKCompilerLegacy._main(argz);
|
||||
return;
|
||||
case "epkdecompiler":
|
||||
EPKDecompiler._main(argz);
|
||||
return;
|
||||
case "obj2mdl-1.5":
|
||||
case "obj2mdl1.5":
|
||||
return;
|
||||
case "obj2mdl-1.8":
|
||||
case "obj2mdl1.8":
|
||||
return;
|
||||
case "ebp-encode":
|
||||
case "ebpencode":
|
||||
return;
|
||||
case "ebp-decode":
|
||||
case "ebpdecode":
|
||||
return;
|
||||
case "skybox-gen":
|
||||
case "skyboxgen":
|
||||
return;
|
||||
case "eagler-moon-gen":
|
||||
case "eaglermoongen":
|
||||
return;
|
||||
case "lens-flare-gen":
|
||||
case "lensflaregen":
|
||||
return;
|
||||
default:
|
||||
usage();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private static void usage() {
|
||||
System.out.println("Usage: java -jar EaglerBinaryTools.jar <epkcompiler|legacy-epkcompiler|epkdecompiler|obj2mdl-1.5|obj2mdl-1.8|ebp-encode|ebp-decode|skybox-gen|eagler-moon-gen|lens-flare-gen> [args...]");
|
||||
System.out.println(" - 'epkcompiler': Compile an EPK file from a folder");
|
||||
System.out.println(" - 'legacy-epkcompiler': Compile an EPK file in legacy format");
|
||||
System.out.println(" - 'epkdecompiler': Decompile an EPK file into a folder");
|
||||
System.out.println(" - 'obj2mdl-1.5': Compile FNAW skin MDL file for 1.5");
|
||||
System.out.println(" - 'obj2mdl-1.8': Compile FNAW skin MDL file for 1.8");
|
||||
System.out.println(" - 'ebp-encode': Encode EBP file from PNG");
|
||||
System.out.println(" - 'ebp-decode': Decode EBP file to PNG");
|
||||
System.out.println(" - 'skybox-gen': Generate skybox.dat from OBJ for shader packs");
|
||||
System.out.println(" - 'eagler-moon-gen': Generate eagler_moon.bmp from PNG for shader packs");
|
||||
System.out.println(" - 'lens-flare-gen': Generate lens_streaks.bmp, lens_ghosts.bmp from PNG for shader packs");
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,248 @@
|
|||
package net.lax1dude.eaglercraft.bintools.utils;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
import java.util.zip.CRC32;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
import java.util.zip.InflaterInputStream;
|
||||
|
||||
/**
|
||||
* Copyright (c) 2022-2024 lax1dude. All Rights Reserved.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
||||
* IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
|
||||
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
||||
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
* POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
*/
|
||||
public class EPKDecompilerSP {
|
||||
|
||||
public static class FileEntry {
|
||||
public final String type;
|
||||
public final String name;
|
||||
public final byte[] data;
|
||||
protected FileEntry(String type, String name, byte[] data) {
|
||||
this.type = type;
|
||||
this.name = name;
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
private ByteArrayInputStream in2;
|
||||
private DataInputStream in;
|
||||
private InputStream zis;
|
||||
private MessageDigest dg;
|
||||
private CRC32 crc32;
|
||||
private int numFiles;
|
||||
private boolean isFinished = false;
|
||||
private boolean isOldFormat = false;
|
||||
|
||||
public EPKDecompilerSP(byte[] data) throws IOException {
|
||||
in2 = new ByteArrayInputStream(data);
|
||||
|
||||
byte[] header = new byte[8];
|
||||
in2.read(header);
|
||||
|
||||
if(Arrays.equals(header, new byte[]{(byte)69,(byte)65,(byte)71,(byte)80,(byte)75,(byte)71,(byte)36,(byte)36})) {
|
||||
byte[] endCode = new byte[] { (byte)':', (byte)':', (byte)':', (byte)'Y',
|
||||
(byte)'E', (byte)'E', (byte)':', (byte)'>' };
|
||||
for(int i = 0; i < 8; ++i) {
|
||||
if(data[data.length - 8 + i] != endCode[i]) {
|
||||
throw new IOException("EPK file is missing EOF code (:::YEE:>)");
|
||||
}
|
||||
}
|
||||
in2 = new ByteArrayInputStream(data, 8, data.length - 16);
|
||||
initNew();
|
||||
}else if(Arrays.equals(header, new byte[]{(byte)69,(byte)65,(byte)71,(byte)80,(byte)75,(byte)71,(byte)33,(byte)33})) {
|
||||
initOld();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public boolean isOld() {
|
||||
return isOldFormat;
|
||||
}
|
||||
|
||||
public FileEntry readFile() throws IOException {
|
||||
if(!isOldFormat) {
|
||||
return readFileNew();
|
||||
}else {
|
||||
return readFileOld();
|
||||
}
|
||||
}
|
||||
|
||||
private void initNew() throws IOException {
|
||||
InputStream is = in2;
|
||||
|
||||
String vers = readASCII(is);
|
||||
if(!vers.startsWith("ver2.")) {
|
||||
throw new IOException("Unknown or invalid EPK version: " + vers);
|
||||
}
|
||||
|
||||
is.skip(is.read()); // skip filename
|
||||
is.skip(loadShort(is)); // skip comment
|
||||
is.skip(8); // skip millis date
|
||||
|
||||
numFiles = loadInt(is);
|
||||
|
||||
char compressionType = (char)is.read();
|
||||
|
||||
switch(compressionType) {
|
||||
case 'G':
|
||||
zis = new GZIPInputStream(is);
|
||||
break;
|
||||
case 'Z':
|
||||
zis = new InflaterInputStream(is);
|
||||
break;
|
||||
case '0':
|
||||
zis = is;
|
||||
break;
|
||||
default:
|
||||
throw new IOException("Invalid or unsupported EPK compression: " + compressionType);
|
||||
}
|
||||
|
||||
crc32 = new CRC32();
|
||||
|
||||
}
|
||||
|
||||
private FileEntry readFileNew() throws IOException {
|
||||
if(isFinished) {
|
||||
return null;
|
||||
}
|
||||
|
||||
byte[] typeBytes = new byte[4];
|
||||
IOUtils.readFully(zis, typeBytes);
|
||||
String type = readASCII(typeBytes);
|
||||
|
||||
if(numFiles == 0) {
|
||||
if(!"END$".equals(type)) {
|
||||
throw new IOException("EPK file is missing END code (END$)");
|
||||
}
|
||||
zis.close();
|
||||
isFinished = true;
|
||||
return null;
|
||||
}else {
|
||||
if("END$".equals(type)) {
|
||||
throw new IOException("Unexpected END when there are still " + numFiles + " files remaining");
|
||||
}else {
|
||||
String name = readASCII(zis);
|
||||
int len = loadInt(zis);
|
||||
byte[] data;
|
||||
|
||||
if("FILE".equals(type)) {
|
||||
if(len < 5) {
|
||||
throw new IOException("File '" + name + "' is incomplete (no crc)");
|
||||
}
|
||||
|
||||
int loadedCrc = loadInt(zis);
|
||||
|
||||
data = new byte[len - 5];
|
||||
IOUtils.readFully(zis, data);
|
||||
|
||||
crc32.reset();
|
||||
crc32.update(data, 0, data.length);
|
||||
if((int)crc32.getValue() != loadedCrc) {
|
||||
throw new IOException("File '" + name + "' has an invalid checksum");
|
||||
}
|
||||
|
||||
if(zis.read() != ':') {
|
||||
throw new IOException("File '" + name + "' is incomplete");
|
||||
}
|
||||
}else {
|
||||
data = new byte[len];
|
||||
IOUtils.readFully(zis, data);
|
||||
}
|
||||
|
||||
if(zis.read() != '>') {
|
||||
throw new IOException("Object '" + name + "' is incomplete");
|
||||
}
|
||||
|
||||
--numFiles;
|
||||
return new FileEntry(type, name, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final int loadShort(InputStream is) throws IOException {
|
||||
return (is.read() << 8) | is.read();
|
||||
}
|
||||
|
||||
private static final int loadInt(InputStream is) throws IOException {
|
||||
return (is.read() << 24) | (is.read() << 16) | (is.read() << 8) | is.read();
|
||||
}
|
||||
|
||||
public static final String readASCII(byte[] bytesIn) throws IOException {
|
||||
char[] charIn = new char[bytesIn.length];
|
||||
for(int i = 0; i < bytesIn.length; ++i) {
|
||||
charIn[i] = (char)((int)bytesIn[i] & 0xFF);
|
||||
}
|
||||
return new String(charIn);
|
||||
}
|
||||
|
||||
private static final String readASCII(InputStream bytesIn) throws IOException {
|
||||
int len = bytesIn.read();
|
||||
char[] charIn = new char[len];
|
||||
for(int i = 0; i < len; ++i) {
|
||||
charIn[i] = (char)(bytesIn.read() & 0xFF);
|
||||
}
|
||||
return new String(charIn);
|
||||
}
|
||||
|
||||
private void initOld() throws IOException {
|
||||
isOldFormat = true;
|
||||
try {
|
||||
dg = MessageDigest.getInstance("SHA-1");
|
||||
}catch(NoSuchAlgorithmException ex) {
|
||||
throw new RuntimeException("SHA-1 is not supported in this JRE!", ex);
|
||||
}
|
||||
in = new DataInputStream(in2);
|
||||
in.readUTF();
|
||||
in = new DataInputStream(new InflaterInputStream(in2));
|
||||
}
|
||||
|
||||
private FileEntry readFileOld() throws IOException {
|
||||
if(isFinished) {
|
||||
return null;
|
||||
}
|
||||
String s = in.readUTF();
|
||||
if(s.equals(" end")) {
|
||||
isFinished = true;
|
||||
in.close();
|
||||
return null;
|
||||
}else if(!s.equals("<file>")) {
|
||||
throw new IOException("invalid epk file");
|
||||
}
|
||||
String path = in.readUTF();
|
||||
byte[] digest = new byte[20];
|
||||
IOUtils.readFully(in, digest);
|
||||
int len = in.readInt();
|
||||
byte[] file = new byte[len];
|
||||
IOUtils.readFully(in, file);
|
||||
byte[] digest2 = dg.digest(file);
|
||||
if(!Arrays.equals(digest, digest2)) throw new IOException("invalid file hash for "+path);
|
||||
if(!"</file>".equals(in.readUTF())) throw new IOException("invalid epk file");
|
||||
return new FileEntry("FILE", path, file);
|
||||
}
|
||||
|
||||
// Avoid Inflater memleak
|
||||
public void close() throws IOException {
|
||||
if(zis != null) {
|
||||
zis.close();
|
||||
}
|
||||
if(in != null) {
|
||||
in.close();
|
||||
}
|
||||
isFinished = true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* Copyright (c) 1996, 2022, Oracle and/or its affiliates. All rights reserved.
|
||||
* ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
package net.lax1dude.eaglercraft.bintools.utils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.zip.CRC32;
|
||||
import java.util.zip.Deflater;
|
||||
import java.util.zip.DeflaterOutputStream;
|
||||
|
||||
/**
|
||||
* GZIP output stream class that allows you to set the compression level
|
||||
*/
|
||||
public class GZIPOutputStream2 extends DeflaterOutputStream {
|
||||
|
||||
protected CRC32 crc = new CRC32();
|
||||
|
||||
private final static int GZIP_MAGIC = 0x8b1f;
|
||||
|
||||
private final static int TRAILER_SIZE = 8;
|
||||
|
||||
public GZIPOutputStream2(OutputStream out, int compressionLevel, int size, boolean syncFlush) throws IOException {
|
||||
super(out, new Deflater(compressionLevel, true), size, syncFlush);
|
||||
writeHeader();
|
||||
crc.reset();
|
||||
}
|
||||
|
||||
public synchronized void write(byte[] buf, int off, int len) throws IOException {
|
||||
super.write(buf, off, len);
|
||||
crc.update(buf, off, len);
|
||||
}
|
||||
|
||||
public void finish() throws IOException {
|
||||
if (!def.finished()) {
|
||||
try {
|
||||
def.finish();
|
||||
while (!def.finished()) {
|
||||
int len = def.deflate(buf, 0, buf.length);
|
||||
if (def.finished() && len <= buf.length - TRAILER_SIZE) {
|
||||
// last deflater buffer. Fit trailer at the end
|
||||
writeTrailer(buf, len);
|
||||
len = len + TRAILER_SIZE;
|
||||
out.write(buf, 0, len);
|
||||
return;
|
||||
}
|
||||
if (len > 0)
|
||||
out.write(buf, 0, len);
|
||||
}
|
||||
// if we can't fit the trailer at the end of the last
|
||||
// deflater buffer, we write it separately
|
||||
byte[] trailer = new byte[TRAILER_SIZE];
|
||||
writeTrailer(trailer, 0);
|
||||
out.write(trailer);
|
||||
} catch (IOException e) {
|
||||
def.end();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void writeHeader() throws IOException {
|
||||
out.write(new byte[] { (byte) GZIP_MAGIC, // Magic number (short)
|
||||
(byte) (GZIP_MAGIC >> 8), // Magic number (short)
|
||||
Deflater.DEFLATED, // Compression method (CM)
|
||||
0, // Flags (FLG)
|
||||
0, // Modification time MTIME (int)
|
||||
0, // Modification time MTIME (int)
|
||||
0, // Modification time MTIME (int)
|
||||
0, // Modification time MTIME (int)
|
||||
0, // Extra flags (XFLG)
|
||||
0 // Operating system (OS)
|
||||
});
|
||||
}
|
||||
|
||||
private void writeTrailer(byte[] buf, int offset) throws IOException {
|
||||
writeInt((int) crc.getValue(), buf, offset); // CRC-32 of uncompr. data
|
||||
writeInt(def.getTotalIn(), buf, offset + 4); // Number of uncompr. bytes
|
||||
}
|
||||
|
||||
private void writeInt(int i, byte[] buf, int offset) throws IOException {
|
||||
writeShort(i & 0xffff, buf, offset);
|
||||
writeShort((i >> 16) & 0xffff, buf, offset + 2);
|
||||
}
|
||||
|
||||
private void writeShort(int s, byte[] buf, int offset) throws IOException {
|
||||
buf[offset] = (byte) (s & 0xff);
|
||||
buf[offset + 1] = (byte) ((s >> 8) & 0xff);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package net.lax1dude.eaglercraft.bintools.utils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* Copyright (c) 2024 lax1dude. All Rights Reserved.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
||||
* IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
|
||||
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
||||
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
* POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
*/
|
||||
public class IOUtils {
|
||||
|
||||
public static int readFully(InputStream is, byte[] out) throws IOException {
|
||||
int i = 0, j;
|
||||
while(i < out.length && (j = is.read(out, i, out.length - i)) != -1) {
|
||||
i += j;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
public static long skipFully(InputStream is, long skip) throws IOException {
|
||||
long i = 0, j;
|
||||
while(i < skip && (j = is.skip(skip - i)) != 0) {
|
||||
i += j;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user