diff --git a/cache/pom.xml b/cache/pom.xml index 9b4eefa2d0..c81033c4b3 100644 --- a/cache/pom.xml +++ b/cache/pom.xml @@ -75,9 +75,9 @@ 2.4 - org.gnu - gnu-crypto - 2.0.1 + org.bouncycastle + bcprov-ext-jdk14 + 1.54 diff --git a/cache/src/main/java/net/runelite/cache/fs/Archive.java b/cache/src/main/java/net/runelite/cache/fs/Archive.java index e4c8ddbed1..d2eae752a3 100644 --- a/cache/src/main/java/net/runelite/cache/fs/Archive.java +++ b/cache/src/main/java/net/runelite/cache/fs/Archive.java @@ -31,10 +31,10 @@ package net.runelite.cache.fs; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Objects; import net.runelite.cache.io.InputStream; +import net.runelite.cache.io.OutputStream; public class Archive { @@ -44,6 +44,7 @@ public class Archive private byte[] whirlpool; private int crc; private int revision; + private int compression; private List files = new ArrayList<>(); public Archive(Index index, int id) @@ -83,7 +84,6 @@ public class Archive { return false; } - // crc is of the file data, we always rewrite in one loop, so iti is different if (this.revision != other.revision) { return false; @@ -115,6 +115,106 @@ public class Archive } } + public void loadContents(byte[] data) + { + if (this.getFiles().size() == 1) + { + this.getFiles().get(0).setContents(data); + return; + } + + int filesCount = this.getFiles().size(); + + InputStream stream = new InputStream(data); + stream.setOffset(stream.getLength() - 1); + int chunks = stream.readUnsignedByte(); + + // -1 for chunks count + one int per file slot per chunk + stream.setOffset(stream.getLength() - 1 - chunks * filesCount * 4); + int[][] chunkSizes = new int[filesCount][chunks]; + int[] filesSize = new int[filesCount]; + + for (int chunk = 0; chunk < chunks; ++chunk) + { + int chunkSize = 0; + + for (int id = 0; id < filesCount; ++id) + { + int delta = stream.readInt(); + chunkSize += delta; // size of this chunk + + chunkSizes[id][chunk] = chunkSize; // store size of chunk + + filesSize[id] += chunkSize; // add chunk size to file size + } + } + + byte[][] fileContents = new byte[filesCount][]; + int[] fileOffsets = new int[filesCount]; + + for (int i = 0; i < filesCount; ++i) + { + fileContents[i] = new byte[filesSize[i]]; + } + + // the file data is at the beginning of the stream + stream.setOffset(0); + + for (int chunk = 0; chunk < chunks; ++chunk) + { + for (int id = 0; id < filesCount; ++id) + { + int chunkSize = chunkSizes[id][chunk]; + + stream.readBytes(fileContents[id], fileOffsets[id], chunkSize); + + fileOffsets[id] += chunkSize; + } + } + + for (int i = 0; i < filesCount; ++i) + { + File f = this.getFiles().get(i); + f.setContents(fileContents[i]); + } + } + + public byte[] saveContents() + { + OutputStream stream = new OutputStream(); + + int filesCount = this.getFiles().size(); + + if (filesCount == 1) + { + File file = this.getFiles().get(0); + stream.writeBytes(file.getContents()); + } + else + { + for (File file : this.getFiles()) + { + stream.writeBytes(file.getContents()); + } + + int offset = 0; + + for (File file : this.getFiles()) + { + int chunkSize = file.getSize(); + + int sz = chunkSize - offset; + offset = chunkSize; + stream.writeInt(sz); + } + + stream.writeByte(1); // chunks + } + + byte[] fileData = stream.flip(); + return fileData; + } + public void loadNames(InputStream stream, int numberOfFiles) { for (int i = 0; i < numberOfFiles; ++i) @@ -170,6 +270,16 @@ public class Archive this.revision = revision; } + public int getCompression() + { + return compression; + } + + public void setCompression(int compression) + { + this.compression = compression; + } + public List getFiles() { return files; diff --git a/cache/src/main/java/net/runelite/cache/fs/DataFile.java b/cache/src/main/java/net/runelite/cache/fs/DataFile.java index a292a34049..7b355e2822 100644 --- a/cache/src/main/java/net/runelite/cache/fs/DataFile.java +++ b/cache/src/main/java/net/runelite/cache/fs/DataFile.java @@ -80,7 +80,7 @@ public class DataFile implements Closeable */ public synchronized DataFileReadResult read(int indexId, int archiveId, int sector, int size) throws IOException { - if (sector <= 0L || dat.length() / 520L < (long) sector) + if (sector <= 0L || dat.length() / SECTOR_SIZE < (long) sector) { logger.warn("bad read, dat length {}, requested sector {}", dat.length(), sector); return null; @@ -171,7 +171,7 @@ public class DataFile implements Closeable //XTEA decrypt here? - return this.decompress(buffer.array()); + return decompress(buffer.array()); } public synchronized DataFileWriteResult write(int indexId, int archiveId, ByteBuffer data, int compression, int revision) throws IOException @@ -270,12 +270,13 @@ public class DataFile implements Closeable DataFileWriteResult res = new DataFileWriteResult(); res.sector = startSector; res.compressedLength = compressedData.length; - res.crc = CRC32HGenerator.getHash(compressedData, compressedData.length - 2); - res.whirlpool = Whirlpool.getHash(compressedData, compressedData.length - 2); + int length = revision != -1 ? compressedData.length - 2 : compressedData.length; + res.crc = CRC32HGenerator.getHash(compressedData, length); + res.whirlpool = Whirlpool.getHash(compressedData, length); return res; } - private DataFileReadResult decompress(byte[] b) + public static DataFileReadResult decompress(byte[] b) { InputStream stream = new InputStream(b); @@ -290,22 +291,22 @@ public class DataFile implements Closeable { case CompressionType.NONE: data = new byte[compressedLength]; - revision = this.checkRevision(stream, compressedLength); + revision = checkRevision(stream, compressedLength); stream.readBytes(data, 0, compressedLength); break; case CompressionType.BZ2: { int length = stream.readInt(); - revision = this.checkRevision(stream, compressedLength); - data = BZip2.decompress(stream.getRemaining()); + revision = checkRevision(stream, compressedLength); + data = BZip2.decompress(stream.getRemaining(), compressedLength); assert data.length == length; break; } case CompressionType.GZ: { int length = stream.readInt(); - revision = this.checkRevision(stream, compressedLength); - data = GZip.decompress(stream.getRemaining()); + revision = checkRevision(stream, compressedLength); + data = GZip.decompress(stream.getRemaining(), compressedLength); assert data.length == length; break; } @@ -316,8 +317,10 @@ public class DataFile implements Closeable DataFileReadResult res = new DataFileReadResult(); res.data = data; res.revision = revision; - res.crc = CRC32HGenerator.getHash(b, b.length - 2); - res.whirlpool = Whirlpool.getHash(b, b.length - 2); + int length = revision != -1 ? b.length - 2 : b.length; + res.crc = CRC32HGenerator.getHash(b, length); + res.whirlpool = Whirlpool.getHash(b, length); + res.compression = compression; return res; } @@ -349,12 +352,13 @@ public class DataFile implements Closeable } stream.writeBytes(compressedData); - stream.writeShort(revision); + if (revision != -1) + stream.writeShort(revision); return stream.flip(); } - private int checkRevision(InputStream stream, int compressedLength) + private static int checkRevision(InputStream stream, int compressedLength) { int offset = stream.getOffset(); int revision; @@ -362,6 +366,7 @@ public class DataFile implements Closeable { stream.setOffset(stream.getLength() - 2); revision = stream.readUnsignedShort(); + assert revision != -1; stream.setOffset(offset); } else diff --git a/cache/src/main/java/net/runelite/cache/fs/DataFileReadResult.java b/cache/src/main/java/net/runelite/cache/fs/DataFileReadResult.java index 0a2d7e47b3..b4cef1f49f 100644 --- a/cache/src/main/java/net/runelite/cache/fs/DataFileReadResult.java +++ b/cache/src/main/java/net/runelite/cache/fs/DataFileReadResult.java @@ -36,4 +36,5 @@ public class DataFileReadResult public int revision; public int crc; // crc of compressed data public byte[] whirlpool; + public int compression; // compression method data was compressed with } diff --git a/cache/src/main/java/net/runelite/cache/fs/Index.java b/cache/src/main/java/net/runelite/cache/fs/Index.java index fbe6948736..55063bf821 100644 --- a/cache/src/main/java/net/runelite/cache/fs/Index.java +++ b/cache/src/main/java/net/runelite/cache/fs/Index.java @@ -50,7 +50,12 @@ public class Index implements Closeable private final Store store; private final IndexFile index; private final int id; + private int protocol = 7; + private boolean named = true, usesWhirpool; private int revision; + private int crc; + private byte[] whirlpool; + private int compression; // compression method of this index's data in 255 private final List archives = new ArrayList<>(); public Index(Store store, IndexFile index, int id) @@ -108,6 +113,21 @@ public class Index implements Closeable return id; } + public int getRevision() + { + return revision; + } + + public int getCrc() + { + return crc; + } + + public byte[] getWhirlpool() + { + return whirlpool; + } + public IndexFile getIndex() { return index; @@ -154,6 +174,11 @@ public class Index implements Closeable archives.clear(); readIndexData(data); + + this.crc = res.crc; + this.whirlpool = res.whirlpool; + this.compression = res.compression; + assert res.revision == -1; this.loadFiles(); } @@ -167,14 +192,17 @@ public class Index implements Closeable DataFile dataFile = store.getData(); IndexFile index255 = store.getIndex255(); - DataFileWriteResult res = dataFile.write(index255.getIndexFileId(), this.id, ByteBuffer.wrap(data), 0, this.revision); + DataFileWriteResult res = dataFile.write(index255.getIndexFileId(), this.id, ByteBuffer.wrap(data), this.compression, -1); // index data revision is always -1 index255.write(new IndexEntry(index255, id, res.sector, res.compressedLength)); + + this.crc = res.crc; + this.whirlpool = res.whirlpool; } - private void readIndexData(byte[] data) + public void readIndexData(byte[] data) { InputStream stream = new InputStream(data); - int protocol = stream.readUnsignedByte(); + protocol = stream.readUnsignedByte(); if (protocol >= 5 && protocol <= 7) { if (protocol >= 6) @@ -183,8 +211,9 @@ public class Index implements Closeable } int hash = stream.readUnsignedByte(); - boolean named = (1 & hash) != 0; - boolean usesWhirpool = (2 & hash) != 0; + named = (1 & hash) != 0; + usesWhirpool = (2 & hash) != 0; + assert (hash & ~3) == 0; int validArchivesCount = protocol >= 7 ? stream.readBigSmart() : stream.readUnsignedShort(); int lastArchiveId = 0; @@ -288,65 +317,14 @@ public class Index implements Closeable logger.warn("whirlpool mismatch for archive {}", a); } - if (a.getFiles().size() == 1) + if (a.getRevision() != res.revision) { - a.getFiles().get(0).setContents(data); - continue; + logger.warn("revision mismatch for archive {}", a); } + + a.setCompression(res.compression); - final int filesCount = a.getFiles().size(); - - int readPosition = data.length; - --readPosition; - int amtOfLoops = data[readPosition] & 255; - readPosition -= amtOfLoops * filesCount * 4; - InputStream stream = new InputStream(data); - stream.setOffset(readPosition); - int[] filesSize = new int[filesCount]; - - int sourceOffset; - int count; - for (int filesData = 0; filesData < amtOfLoops; ++filesData) - { - sourceOffset = 0; - - for (count = 0; count < filesCount; ++count) - { - filesSize[count] += sourceOffset += stream.readInt(); - } - } - - byte[][] var18 = new byte[filesCount][]; - - for (sourceOffset = 0; sourceOffset < filesCount; ++sourceOffset) - { - var18[sourceOffset] = new byte[filesSize[sourceOffset]]; - filesSize[sourceOffset] = 0; - } - - stream.setOffset(readPosition); - sourceOffset = 0; - - int fileId; - int i; - for (count = 0; count < amtOfLoops; ++count) - { - fileId = 0; - - for (i = 0; i < filesCount; ++i) - { - fileId += stream.readInt(); - System.arraycopy(data, sourceOffset, var18[i], filesSize[i], fileId); - sourceOffset += fileId; - filesSize[i] += fileId; - } - } - - for (i = 0; i < filesCount; ++i) - { - File f = a.getFiles().get(i); - f.setContents(var18[i]); - } + a.loadContents(data); } } @@ -354,43 +332,12 @@ public class Index implements Closeable { for (Archive a : archives) { - OutputStream stream = new OutputStream(); - - int sourceOffset = 0; - final int filesCount = a.getFiles().size(); - - if (filesCount == 1) - { - File file = a.getFiles().get(0); - stream.writeBytes(file.getContents()); - } - else - { - for (int i = 0; i < filesCount; ++i) - { - File file = a.getFiles().get(i); - stream.writeBytes(file.getContents()); - } - - for (int count = 0; count < filesCount; ++count) - { - File file = a.getFiles().get(count); - - int sz = file.getSize() - sourceOffset; - sourceOffset = file.getSize(); - stream.writeInt(sz); - } - - stream.writeByte(1); // number of loops - } - - byte[] fileData = stream.flip(); + byte[] fileData = a.saveContents(); assert this.index.getIndexFileId() == this.id; DataFile data = store.getData(); - // XXX old data is just left there in the file? - DataFileWriteResult res = data.write(this.id, a.getArchiveId(), ByteBuffer.wrap(fileData), 0, this.revision); + DataFileWriteResult res = data.write(this.id, a.getArchiveId(), ByteBuffer.wrap(fileData), a.getCompression(), a.getRevision()); this.index.write(new IndexEntry(this.index, a.getArchiveId(), res.sector, res.compressedLength)); a.setCrc(res.crc); @@ -401,14 +348,12 @@ public class Index implements Closeable public byte[] writeIndexData() { OutputStream stream = new OutputStream(); - int protocol = 7;//this.getProtocol(); stream.writeByte(protocol); if (protocol >= 6) { stream.writeInt(this.revision); } - boolean named = true, usesWhirpool = false; stream.writeByte((named ? 1 : 0) | (usesWhirpool ? 2 : 0)); if (protocol >= 7) { diff --git a/cache/src/main/java/net/runelite/cache/fs/Store.java b/cache/src/main/java/net/runelite/cache/fs/Store.java index 399a600986..798ed89fea 100644 --- a/cache/src/main/java/net/runelite/cache/fs/Store.java +++ b/cache/src/main/java/net/runelite/cache/fs/Store.java @@ -98,7 +98,7 @@ public class Store implements Closeable return true; } - public final Index addIndex(int id) throws FileNotFoundException + public Index addIndex(int id) throws FileNotFoundException { for (Index i : indexes) if (i.getIndex().getIndexFileId() == id) @@ -111,6 +111,12 @@ public class Store implements Closeable return index; } + + public void removeIndex(Index index) + { + assert indexes.contains(index); + indexes.remove(index); + } public void load() throws IOException { @@ -150,4 +156,12 @@ public class Store implements Closeable { return indexes.get(type.getNumber()); } + + public Index findIndex(int id) + { + for (Index i : indexes) + if (i.getId() == id) + return i; + return null; + } } diff --git a/cache/src/main/java/net/runelite/cache/fs/util/BZip2.java b/cache/src/main/java/net/runelite/cache/fs/util/BZip2.java index 2cca0b6f56..318e71eaa3 100644 --- a/cache/src/main/java/net/runelite/cache/fs/util/BZip2.java +++ b/cache/src/main/java/net/runelite/cache/fs/util/BZip2.java @@ -58,12 +58,18 @@ public class BZip2 { InputStream is = new ByteArrayInputStream(bytes); ByteArrayOutputStream bout = new ByteArrayOutputStream(); - try (OutputStream os = new BZip2CompressorOutputStream(bout)) + try (OutputStream os = new BZip2CompressorOutputStream(bout, 1)) { IOUtils.copy(is, os); } byte[] out = bout.toByteArray(); + + assert BZIP_HEADER[0] == out[0]; + assert BZIP_HEADER[1] == out[1]; + assert BZIP_HEADER[2] == out[2]; + assert BZIP_HEADER[3] == out[3]; + return Arrays.copyOfRange(out, BZIP_HEADER.length, out.length); // remove header.. } catch (IOException ex) @@ -73,15 +79,15 @@ public class BZip2 } } - public static byte[] decompress(byte[] bytes) + public static byte[] decompress(byte[] bytes, int len) { try { - byte[] data = new byte[bytes.length + BZIP_HEADER.length]; + byte[] data = new byte[len + BZIP_HEADER.length]; // add header System.arraycopy(BZIP_HEADER, 0, data, 0, BZIP_HEADER.length); - System.arraycopy(bytes, 0, data, BZIP_HEADER.length, bytes.length); + System.arraycopy(bytes, 0, data, BZIP_HEADER.length, len); ByteArrayOutputStream os = new ByteArrayOutputStream(); diff --git a/cache/src/main/java/net/runelite/cache/fs/util/GZip.java b/cache/src/main/java/net/runelite/cache/fs/util/GZip.java index 9b6adbb76c..834fcb6fcb 100644 --- a/cache/src/main/java/net/runelite/cache/fs/util/GZip.java +++ b/cache/src/main/java/net/runelite/cache/fs/util/GZip.java @@ -63,11 +63,11 @@ public class GZip return bout.toByteArray(); } - public static byte[] decompress(byte[] bytes) + public static byte[] decompress(byte[] bytes, int len) { ByteArrayOutputStream os = new ByteArrayOutputStream(); - try (InputStream is = new GZIPInputStream(new ByteArrayInputStream(bytes))) + try (InputStream is = new GZIPInputStream(new ByteArrayInputStream(bytes, 0, len))) { IOUtils.copy(is, os); } diff --git a/cache/src/main/java/net/runelite/cache/fs/util/Whirlpool.java b/cache/src/main/java/net/runelite/cache/fs/util/Whirlpool.java index a380819276..ebfba7233a 100644 --- a/cache/src/main/java/net/runelite/cache/fs/util/Whirlpool.java +++ b/cache/src/main/java/net/runelite/cache/fs/util/Whirlpool.java @@ -27,22 +27,34 @@ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ - package net.runelite.cache.fs.util; -public class Whirlpool { - private static gnu.crypto.hash.Whirlpool whirlpool = new gnu.crypto.hash.Whirlpool(); - - public static synchronized byte[] getHash(byte[] data, int len) +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.Security; +import org.bouncycastle.jce.provider.BouncyCastleProvider; + +public class Whirlpool +{ + private static MessageDigest messageDigest; + + static { - whirlpool.update(data, 0, len); + Security.addProvider(new BouncyCastleProvider()); + try { - return whirlpool.digest(); + messageDigest = MessageDigest.getInstance("Whirlpool"); } - finally + catch (NoSuchAlgorithmException ex) { - whirlpool.reset(); + throw new RuntimeException(ex); } } + + public static synchronized byte[] getHash(byte[] data, int len) + { + messageDigest.update(data, 0, len); + return messageDigest.digest(); + } } diff --git a/cache/src/main/java/net/runelite/cache/io/OutputStream.java b/cache/src/main/java/net/runelite/cache/io/OutputStream.java index ee6e41a2ea..d4ebb05585 100644 --- a/cache/src/main/java/net/runelite/cache/io/OutputStream.java +++ b/cache/src/main/java/net/runelite/cache/io/OutputStream.java @@ -75,8 +75,13 @@ public final class OutputStream extends java.io.OutputStream public void writeBytes(byte[] b) { - ensureRemaining(b.length); - buffer.put(b); + writeBytes(b, 0, b.length); + } + + public void writeBytes(byte[] b, int offset, int length) + { + ensureRemaining(length); + buffer.put(b, offset, length); } public void writeByte(int i) diff --git a/cache/src/test/java/net/runelite/cache/fs/DataFileTest.java b/cache/src/test/java/net/runelite/cache/fs/DataFileTest.java index ea5ac06d91..da0db62e57 100644 --- a/cache/src/test/java/net/runelite/cache/fs/DataFileTest.java +++ b/cache/src/test/java/net/runelite/cache/fs/DataFileTest.java @@ -68,7 +68,7 @@ public class DataFileTest File file = folder.newFile(); Store store = new Store(folder.getRoot()); DataFile df = new DataFile(store, file); - DataFileWriteResult res = df.write(42, 0x1FFFF, ByteBuffer.wrap(b), CompressionType.NONE, 0); + DataFileWriteResult res = df.write(42, 0x1FFFF, ByteBuffer.wrap(b), CompressionType.BZ2, 42); DataFileReadResult res2 = df.read(42, 0x1FFFF, res.sector, res.compressedLength); byte[] buf = res2.data; Assert.assertArrayEquals(b, buf); @@ -95,11 +95,27 @@ public class DataFileTest try (Store store = new Store(folder.getRoot())) { DataFile df = new DataFile(store, folder.newFile()); - DataFileWriteResult res = df.write(41, 4, ByteBuffer.wrap("test".getBytes()), CompressionType.BZ2, 0); + DataFileWriteResult res = df.write(41, 4, ByteBuffer.wrap("test".getBytes()), CompressionType.BZ2, 5); DataFileReadResult res2 = df.read(41, 4, res.sector, res.compressedLength); byte[] buf = res2.data; String str = new String(buf); Assert.assertEquals("test", str); } } + + @Test + public void testCrc() throws IOException + { + File file = folder.newFile(); + Store store = new Store(folder.getRoot()); + DataFile df = new DataFile(store, file); + DataFileWriteResult res = df.write(42, 3, ByteBuffer.wrap("test".getBytes()), CompressionType.NONE, 42); + DataFileReadResult res2 = df.read(42, 3, res.sector, res.compressedLength); + byte[] buf = res2.data; + String str = new String(buf); + Assert.assertEquals("test", str); + Assert.assertEquals(res.crc, res2.crc); + Assert.assertEquals(42, res2.revision); + file.delete(); + } } diff --git a/cache/src/test/java/net/runelite/cache/fs/util/WhirlpoolTest.java b/cache/src/test/java/net/runelite/cache/fs/util/WhirlpoolTest.java new file mode 100644 index 0000000000..96d8fd369f --- /dev/null +++ b/cache/src/test/java/net/runelite/cache/fs/util/WhirlpoolTest.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2016, Adam + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. All advertising materials mentioning features or use of this software + * must display the following acknowledgement: + * This product includes software developed by Adam + * 4. Neither the name of the Adam nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY Adam ''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 Adam 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. + */ +package net.runelite.cache.fs.util; + +import org.junit.Assert; +import org.junit.Test; + +public class WhirlpoolTest +{ + private static final byte[] result = + { + 92, -33, 60, 4, -28, 24, 54, -39, + -11, -85, -123, -74, 6, -107, 32, 36, + 108, 104, -82, 108, 36, -53, -95, 123, + -84, -86, -13, 107, -110, 27, 35, -78, + -60, -122, 36, 56, 86, 73, -9, -70, + -35, 58, -43, 82, -36, -53, -107, -9, + -21, 6, -43, 14, 109, -26, -115, 67, + 64, 116, 107, 18, 12, 46, -64, 63 + }; + + @Test + public void testGetHash() + { + byte[] data = "runelite".getBytes(); + byte[] out = Whirlpool.getHash(data, data.length); + + Assert.assertArrayEquals(out, result); + } + +}