package net.scapeemulator.cache; import java.io.IOException; import java.nio.ByteBuffer; import net.scapeemulator.cache.util.CompressionUtils; import net.scapeemulator.cache.util.crypto.Xtea; /** * A {@link Container} holds an optionally compressed file. This class can be * used to decompress and compress containers. A container can also have a two * byte trailer which specifies the version of the file within it. * @author Graham * @author `Discardedx2 */ public final class Container { /** * This type indicates that no compression is used. */ public static final int COMPRESSION_NONE = 0; /** * This type indicates that BZIP2 compression is used. */ public static final int COMPRESSION_BZIP2 = 1; /** * This type indicates that GZIP compression is used. */ public static final int COMPRESSION_GZIP = 2; private static final int[] NULL_KEY = new int[4]; /** * Decodes and decompresses the container. * @param buffer The buffer. * @return The decompressed container. * @throws IOException if an I/O error occurs. */ public static Container decode(ByteBuffer buffer) throws IOException { return Container.decode(buffer, NULL_KEY); } public static Container decode(ByteBuffer buffer, int[] key) throws IOException { /* decode the type and length */ int type = buffer.get() & 0xFF; int length = buffer.getInt(); /* decrypt (TODO what to do about version number trailer?) */ if (key[0] != 0 || key[1] != 0 || key[2] != 0 || key[3] != 0) { Xtea.decipher(buffer, 5, length + (type == COMPRESSION_NONE ? 5 : 9), key); } /* check if we should decompress the data or not */ if (type == COMPRESSION_NONE) { /* simply grab the data and wrap it in a buffer */ byte[] temp = new byte[length]; buffer.get(temp); ByteBuffer data = ByteBuffer.wrap(temp); /* decode the version if present */ int version = -1; if (buffer.remaining() >= 2) { version = buffer.getShort(); } /* and return the decoded container */ return new Container(type, data, version); } else { /* grab the length of the uncompressed data */ int uncompressedLength = buffer.getInt(); /* grab the data */ byte[] compressed = new byte[length]; buffer.get(compressed); /* uncompress it */ byte[] uncompressed; if (type == COMPRESSION_BZIP2) { uncompressed = CompressionUtils.bunzip2(compressed); } else if (type == COMPRESSION_GZIP) { uncompressed = CompressionUtils.gunzip(compressed); } else { throw new IOException("Invalid compression type"); } /* check if the lengths are equal */ if (uncompressed.length != uncompressedLength) { throw new IOException("Length mismatch"); } /* decode the version if present */ int version = -1; if (buffer.remaining() >= 2) { version = buffer.getShort(); } /* and return the decoded container */ return new Container(type, ByteBuffer.wrap(uncompressed), version); } } /** * The type of compression this container uses. */ private int type; /** * The decompressed data. */ private ByteBuffer data; /** * The version of the file within this container. */ private int version; /** * Creates a new unversioned container. * @param type The type of compression. * @param data The decompressed data. */ public Container(int type, ByteBuffer data) { this(type, data, -1); } /** * Creates a new versioned container. * @param type The type of compression. * @param data The decompressed data. * @param version The version of the file within this container. */ public Container(int type, ByteBuffer data, int version) { this.type = type; this.data = data; this.version = version; } /** * Checks if this container is versioned. * @return {@code true} if so, {@code false} if not. */ public boolean isVersioned() { return version != -1; } /** * Gets the version of the file in this container. * @return The version of the file. * @throws IllegalArgumentException if this container is not versioned. */ public int getVersion() { if (!isVersioned()) throw new IllegalStateException(); return version; } /** * Sets the version of this container. * @param version The version. */ public void setVersion(int version) { this.version = version; } /** * Removes the version on this container so it becomes unversioned. */ public void removeVersion() { this.version = -1; } /** * Sets the type of this container. * @param type The compression type. */ public void setType(int type) { this.type = type; } /** * Gets the type of this container. * @return The compression type. */ public int getType() { return type; } /** * Gets the decompressed data. * @return The decompressed data. */ public ByteBuffer getData() { return data.asReadOnlyBuffer(); } /** * Encodes and compresses this container. * @return The buffer. * @throws IOException if an I/O error occurs. */ public ByteBuffer encode() throws IOException { ByteBuffer data = getData(); // so we have a read only view, making this method thread safe /* grab the data as a byte array for compression */ byte[] bytes = new byte[data.limit()]; data.mark(); data.get(bytes); data.reset(); /* compress the data */ byte[] compressed; if (type == COMPRESSION_NONE) { compressed = bytes; } else if (type == COMPRESSION_GZIP) { compressed = CompressionUtils.gzip(bytes); } else if (type == COMPRESSION_BZIP2) { compressed = CompressionUtils.bzip2(bytes); } else { throw new IOException("Invalid compression type"); } /* calculate the size of the header and trailer and allocate a buffer */ int header = 5 + (type == COMPRESSION_NONE ? 0 : 4) + (isVersioned() ? 2 : 0); ByteBuffer buf = ByteBuffer.allocate(header + compressed.length); /* write the header, with the optional uncompressed length */ buf.put((byte) type); buf.putInt(compressed.length); /* write the compressed length */ if (type != COMPRESSION_NONE) { buf.putInt(data.limit()); } /* write the compressed data */ buf.put(compressed); /* write the trailer with the optional version */ if (isVersioned()) { buf.putShort((short) version); } /* flip the buffer and return it */ return (ByteBuffer) buf.flip(); } }