package net.lax1dude.eaglercraft.sp; import java.io.ByteArrayInputStream; import java.io.DataInputStream; import java.io.IOException; import java.io.InputStream; import java.util.Arrays; import com.jcraft.jzlib.CRC32; import com.jcraft.jzlib.GZIPInputStream; import com.jcraft.jzlib.InflaterInputStream; public class EPKDecompiler { 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 SHA1Digest dg; private CRC32 crc32; private int numFiles; private boolean isFinished = false; private boolean isOldFormat = false; public EPKDecompiler(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]; zis.read(typeBytes); String type = readASCII(typeBytes); if(numFiles == 0) { if(!"END$".equals(type)) { throw new IOException("EPK file is missing END code (END$)"); } 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]; zis.read(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]; zis.read(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; dg = new SHA1Digest(); 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; return null; }else if(!s.equals("")) { throw new IOException("invalid epk file"); } String path = in.readUTF(); byte[] digest = new byte[20]; byte[] digest2 = new byte[20]; in.read(digest); int len = in.readInt(); byte[] file = new byte[len]; in.read(file); dg.update(file, 0, len); dg.doFinal(digest2, 0); if(!Arrays.equals(digest, digest2)) throw new IOException("invalid file hash for "+path); if(!"".equals(in.readUTF())) throw new IOException("invalid epk file"); return new FileEntry("FILE", path, file); } }