From 9a9e50e2231bc38d9c47cbffe4a315dc13a1fe6f Mon Sep 17 00:00:00 2001 From: lax1dude Date: Sun, 16 Jun 2024 22:42:28 -0700 Subject: [PATCH] Implement EPK compiler/decompiler --- .gitattributes | 7 + .gitignore | 3 + .../eaglercraft/bintools/EPKCompiler.java | 194 ++++++++++++++ .../bintools/EPKCompilerLegacy.java | 93 +++++++ .../eaglercraft/bintools/EPKDecompiler.java | 81 ++++++ .../bintools/EaglerBinaryTools.java | 79 ++++++ .../bintools/utils/EPKDecompilerSP.java | 248 ++++++++++++++++++ .../bintools/utils/GZIPOutputStream2.java | 112 ++++++++ .../eaglercraft/bintools/utils/IOUtils.java | 39 +++ 9 files changed, 856 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 src/main/java/net/lax1dude/eaglercraft/bintools/EPKCompiler.java create mode 100644 src/main/java/net/lax1dude/eaglercraft/bintools/EPKCompilerLegacy.java create mode 100644 src/main/java/net/lax1dude/eaglercraft/bintools/EPKDecompiler.java create mode 100644 src/main/java/net/lax1dude/eaglercraft/bintools/EaglerBinaryTools.java create mode 100644 src/main/java/net/lax1dude/eaglercraft/bintools/utils/EPKDecompilerSP.java create mode 100644 src/main/java/net/lax1dude/eaglercraft/bintools/utils/GZIPOutputStream2.java create mode 100644 src/main/java/net/lax1dude/eaglercraft/bintools/utils/IOUtils.java diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..e3813a4 --- /dev/null +++ b/.gitattributes @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..916b75b --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.classpath +.project +bin \ No newline at end of file diff --git a/src/main/java/net/lax1dude/eaglercraft/bintools/EPKCompiler.java b/src/main/java/net/lax1dude/eaglercraft/bintools/EPKCompiler.java new file mode 100644 index 0000000..867a66f --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/bintools/EPKCompiler.java @@ -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 [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 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 files) { + for (File f : dir.listFiles()) { + if (f.isDirectory()) { + listDirectory(f, files); + } else { + files.add(f); + } + } + } + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/bintools/EPKCompilerLegacy.java b/src/main/java/net/lax1dude/eaglercraft/bintools/EPKCompilerLegacy.java new file mode 100644 index 0000000..527227a --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/bintools/EPKCompilerLegacy.java @@ -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 "); + return; + } + File root = new File(args[0]); + ArrayList 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(""); + 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(""); + } + 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 files) { + for (File f : dir.listFiles()) { + if (f.isDirectory()) { + listDirectory(f, files); + } else { + files.add(f); + } + } + } + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/bintools/EPKDecompiler.java b/src/main/java/net/lax1dude/eaglercraft/bintools/EPKDecompiler.java new file mode 100644 index 0000000..a6e9775 --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/bintools/EPKDecompiler.java @@ -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 "); + 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"); + } + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/bintools/EaglerBinaryTools.java b/src/main/java/net/lax1dude/eaglercraft/bintools/EaglerBinaryTools.java new file mode 100644 index 0000000..3009b99 --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/bintools/EaglerBinaryTools.java @@ -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 [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"); + } + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/bintools/utils/EPKDecompilerSP.java b/src/main/java/net/lax1dude/eaglercraft/bintools/utils/EPKDecompilerSP.java new file mode 100644 index 0000000..e57e34e --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/bintools/utils/EPKDecompilerSP.java @@ -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("")) { + 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(!"".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; + } +} diff --git a/src/main/java/net/lax1dude/eaglercraft/bintools/utils/GZIPOutputStream2.java b/src/main/java/net/lax1dude/eaglercraft/bintools/utils/GZIPOutputStream2.java new file mode 100644 index 0000000..459e514 --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/bintools/utils/GZIPOutputStream2.java @@ -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); + } +} diff --git a/src/main/java/net/lax1dude/eaglercraft/bintools/utils/IOUtils.java b/src/main/java/net/lax1dude/eaglercraft/bintools/utils/IOUtils.java new file mode 100644 index 0000000..fdde31a --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/bintools/utils/IOUtils.java @@ -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; + } + +}