package net.scapeemulator.cache; import java.io.Closeable; import java.io.FileNotFoundException; import java.io.IOException; import java.nio.ByteBuffer; import java.util.zip.CRC32; import net.scapeemulator.cache.util.ByteBufferUtils; import net.scapeemulator.cache.util.crypto.Whirlpool; /** * The {@link Cache} class provides a unified, high-level API for modifying * the cache of a Jagex game. * @author Graham * @author `Discardedx2 */ public final class Cache implements Closeable { /** * The file store that backs this cache. */ private final FileStore store; /** * Creates a new {@link Cache} backed by the specified {@link FileStore}. * @param store The {@link FileStore} that backs this {@link Cache}. */ public Cache(FileStore store) { this.store = store; } /** * Gets the number of index files, not including the meta index file. * @return The number of index files. * @throws IOException if an I/O error occurs. */ public int getTypeCount() throws IOException { return store.getTypeCount(); } /** * Gets the number of files of the specified type. * @param type The type. * @return The number of files. * @throws IOException if an I/O error occurs. */ public int getFileCount(int type) throws IOException { return store.getFileCount(type); } /** * Gets the {@link FileStore} that backs this {@link Cache}. * @return The underlying file store. */ public FileStore getStore() { return store; } /** * Computes the {@link ChecksumTable} for this cache. The checksum table * forms part of the so-called "update keys". * @return The {@link ChecksumTable}. * @throws IOException if an I/O error occurs. */ public ChecksumTable createChecksumTable() throws IOException { /* create the checksum table */ int size = store.getTypeCount(); ChecksumTable table = new ChecksumTable(size); /* loop through all the reference tables and get their CRC and versions */ for (int i = 0; i < size; i++) { ByteBuffer buf = store.read(255, i); int crc = 0; int version = 0; byte[] whirlpool = new byte[64]; /* * if there is actually a reference table, calculate the CRC, * version and whirlpool hash */ if (buf.limit() > 0) { // some indices are not used, is this appropriate? ReferenceTable ref = ReferenceTable.decode(Container.decode(buf).getData()); crc = ByteBufferUtils.getCrcChecksum(buf); version = ref.getVersion(); buf.position(0); whirlpool = ByteBufferUtils.getWhirlpoolDigest(buf); } table.setEntry(i, new ChecksumTable.Entry(crc, version, whirlpool)); } /* return the table */ return table; } /** * Reads a file from the cache. * @param type The type of file. * @param file The file id. * @return The file. * @throws IOException if an I/O error occurred. */ public Container read(int type, int file) throws IOException { /* we don't want people reading/manipulating these manually */ if (type == 255) throw new IOException("Reference tables can only be read with the low level FileStore API!"); /* delegate the call to the file store then decode the container */ return Container.decode(store.read(type, file)); } /** * Writes a file to the cache and updates the {@link ReferenceTable} that * it is associated with. * @param type The type of file. * @param file The file id. * @param container The {@link Container} to write. * @throws IOException if an I/O error occurs. */ public void write(int type, int file, Container container) throws IOException { /* we don't want people reading/manipulating these manually */ if (type == 255) throw new IOException("Reference tables can only be modified with the low level FileStore API!"); /* increment the container's version */ container.setVersion(container.getVersion() + 1); /* decode the reference table for this index */ Container tableContainer = Container.decode(store.read(255, type)); ReferenceTable table = ReferenceTable.decode(tableContainer.getData()); /* grab the bytes we need for the checksum */ ByteBuffer buffer = container.encode(); byte[] bytes = new byte[buffer.limit() - 2]; // last two bytes are the version and shouldn't be included buffer.mark(); try { buffer.position(0); buffer.get(bytes, 0, bytes.length); } finally { buffer.reset(); } /* calculate the new CRC checksum */ CRC32 crc = new CRC32(); crc.update(bytes, 0, bytes.length); /* update the version and checksum for this file */ ReferenceTable.Entry entry = table.getEntry(file); if (entry == null) { /* create a new entry for the file */ entry = new ReferenceTable.Entry(); table.putEntry(file, entry); } entry.setVersion(container.getVersion()); entry.setCrc((int) crc.getValue()); /* calculate and update the whirlpool digest if we need to */ if ((table.getFlags() & ReferenceTable.FLAG_WHIRLPOOL) != 0) { byte[] whirlpool = Whirlpool.whirlpool(bytes, 0, bytes.length); entry.setWhirlpool(whirlpool); } /* update the reference table version */ table.setVersion(table.getVersion() + 1); /* save the reference table */ tableContainer = new Container(tableContainer.getType(), table.encode()); store.write(255, type, tableContainer.encode()); /* save the file itself */ store.write(type, file, buffer); } /** * Reads a file contained in an archive in the cache. * @param type The type of the file. * @param file The archive id. * @param member The file within the archive. * @return The file. * @throws IOException if an I/O error occurred. */ public ByteBuffer read(int type, int file, int member, boolean archived) throws IOException { /* grab the container and the reference table */ Container container = read(type, file); if(!archived) { return container.getData(); } Container tableContainer = Container.decode(store.read(255, type)); ReferenceTable table = ReferenceTable.decode(tableContainer.getData()); /* check if the file/member are valid */ ReferenceTable.Entry entry = table.getEntry(file); if (entry == null || member < 0 || member >= entry.capacity()) throw new FileNotFoundException(); /* convert member id */ int nonSparseMember = 0; for (int i = 0; i < member; i++) { if (entry.getEntry(i) != null) nonSparseMember++; } /* extract the entry from the archive */ Archive archive = Archive.decode(container.getData(), entry.size()); return archive.getEntry(nonSparseMember); } /** * Writes a file contained in an archive to the cache. * @param type The type of file. * @param file The id of the archive. * @param member The file within the archive. * @param data The data to write. * @throws IOException if an I/O error occurs. */ public void write(int type, int file, int member, ByteBuffer data) throws IOException { /* grab the reference table */ Container tableContainer = Container.decode(store.read(255, type)); ReferenceTable table = ReferenceTable.decode(tableContainer.getData()); /* create a new entry if necessary */ ReferenceTable.Entry entry = table.getEntry(file); int oldArchiveSize = -1; if (entry == null) { entry = new ReferenceTable.Entry(); table.putEntry(file, entry); } else { oldArchiveSize = entry.capacity(); } /* add a child entry if one does not exist */ ReferenceTable.ChildEntry child = entry.getEntry(member); if (child == null) { child = new ReferenceTable.ChildEntry(); entry.putEntry(member, child); } /* extract the current archive into memory so we can modify it */ Archive archive; int containerType, containerVersion; if (file < store.getFileCount(type) && oldArchiveSize != -1) { Container container = read(type, file); containerType = container.getType(); containerVersion = container.getVersion(); archive = Archive.decode(container.getData(), oldArchiveSize); } else { containerType = Container.COMPRESSION_GZIP; containerVersion = 1; archive = new Archive(member + 1); } /* expand the archive if it is not large enough */ if (member >= archive.size()) { Archive newArchive = new Archive(member + 1); for (int id = 0; id < archive.size(); id++) { newArchive.putEntry(id, archive.getEntry(id)); } archive = newArchive; } /* put the member into the archive */ archive.putEntry(member, data); /* create 'dummy' entries */ for (int id = 0; id < archive.size(); id++) { if (archive.getEntry(id) == null) { entry.putEntry(id, new ReferenceTable.ChildEntry()); archive.putEntry(id, ByteBuffer.allocate(1)); } } /* write the reference table out again */ tableContainer = new Container(tableContainer.getType(), table.encode()); store.write(255, type, tableContainer.encode()); /* and write the archive back to memory */ Container container = new Container(containerType, archive.encode(), containerVersion); write(type, file, container); } @Override public void close() throws IOException { store.close(); } }