package net.scapeemulator.cache;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import net.scapeemulator.cache.util.StringUtils;
/**
* A {@link ReferenceTable} holds details for all the files with a single
* type, such as checksums, versions and archive members. There are also
* optional fields for identifier hashes and whirlpool digests.
* @author Graham
* @author `Discardedx2
*/
public final class ReferenceTable {
/**
* A flag which indicates this {@link ReferenceTable} contains
* {@link StringUtils} hashed identifiers.
*/
public static final int FLAG_IDENTIFIERS = 0x01;
/**
* A flag which indicates this {@link ReferenceTable} contains
* whirlpool digests for its entries.
*/
public static final int FLAG_WHIRLPOOL = 0x02;
/**
* Represents a child entry within an {@link Entry} in the
* {@link ReferenceTable}.
* @author Graham Edgecombe
*/
public static class ChildEntry {
/**
* This entry's identifier.
*/
private int identifier = -1;
/**
* Gets the identifier of this entry.
* @return The identifier.
*/
public int getIdentifier() {
return identifier;
}
/**
* Sets the identifier of this entry.
* @param identifier The identifier.
*/
public void setIdentifier(int identifier) {
this.identifier = identifier;
}
}
/**
* Represents a single entry within a {@link ReferenceTable}.
* @author Graham Edgecombe
*/
public static class Entry {
/**
* The identifier of this entry.
*/
private int identifier = -1;
/**
* The CRC32 checksum of this entry.
*/
private int crc;
/**
* The whirlpool digest of this entry.
*/
private byte[] whirlpool = new byte[64];
/**
* The version of this entry.
*/
private int version;
/**
* The children in this entry.
*/
private SortedMap<Integer, ChildEntry> entries = new TreeMap<>();
/**
* Gets the identifier of this entry.
* @return The identifier.
*/
public int getIdentifier() {
return identifier;
}
/**
* Sets the identifier of this entry.
* @param identifier The identifier.
*/
public void setIdentifier(int identifier) {
this.identifier = identifier;
}
/**
* Gets the CRC32 checksum of this entry.
* @return The CRC32 checksum.
*/
public int getCrc() {
return crc;
}
/**
* Sets the CRC32 checksum of this entry.
* @param crc The CRC32 checksum.
*/
public void setCrc(int crc) {
this.crc = crc;
}
/**
* Gets the whirlpool digest of this entry.
* @return The whirlpool digest.
*/
public byte[] getWhirlpool() {
return whirlpool;
}
/**
* Sets the whirlpool digest of this entry.
* @param whirlpool The whirlpool digest.
* @throws IllegalArgumentException if the digest is not 64 bytes long.
*/
public void setWhirlpool(byte[] whirlpool) {
if (whirlpool.length != 64)
throw new IllegalArgumentException();
System.arraycopy(whirlpool, 0, this.whirlpool, 0, whirlpool.length);
}
/**
* Gets the version of this entry.
* @return The version.
*/
public int getVersion() {
return version;
}
/**
* Sets the version of this entry.
* @param version The version.
*/
public void setVersion(int version) {
this.version = version;
}
/**
* Gets the number of actual child entries.
* @return The number of actual child entries.
*/
public int size() {
return entries.size();
}
/**
* Gets the maximum number of child entries.
* @return The maximum number of child entries.
*/
public int capacity() {
if (entries.isEmpty())
return 0;
return entries.lastKey() + 1;
}
/**
* Gets the child entry with the specified id.
* @param id The id.
* @return The entry, or {@code null} if it does not exist.
*/
public ChildEntry getEntry(int id) {
return entries.get(id);
}
/**
* Replaces or inserts the child entry with the specified id.
* @param id The id.
* @param entry The entry.
*/
public void putEntry(int id, ChildEntry entry) {
entries.put(id, entry);
}
/**
* Removes the entry with the specified id.
* @param id The id.
* @param entry The entry.
*/
public void removeEntry(int id, ChildEntry entry) {
entries.remove(id);
}
}
/**
* Decodes the slave checksum table contained in the specified
* {@link ByteBuffer}.
* @param buffer The buffer.
* @return The slave checksum table.
*/
public static ReferenceTable decode(ByteBuffer buffer) {
/* create a new table */
ReferenceTable table = new ReferenceTable();
/* read header */
table.format = buffer.get() & 0xFF;
if (table.format >= 6) {
table.version = buffer.getInt();
}
table.flags = buffer.get() & 0xFF;
/* read the ids */
int[] ids = new int[buffer.getShort() & 0xFFFF];
int accumulator = 0, size = -1;
for (int i = 0; i < ids.length; i++) {
int delta = buffer.getShort() & 0xFFFF;
ids[i] = accumulator += delta;
if (ids[i] > size) {
size = ids[i];
}
}
size++;
/* and allocate specific entries within that array */
for (int id : ids) {
table.entries.put(id, new Entry());
}
/* read the identifiers if present */
if ((table.flags & FLAG_IDENTIFIERS) != 0) {
for (int id : ids) {
int identifier = table.entries.get(id).identifier = buffer.getInt();
table.namedEntries.put(identifier, id);
}
}
/* read the CRC32 checksums */
for (int id : ids) {
table.entries.get(id).crc = buffer.getInt();
}
/* read the whirlpool digests if present */
if ((table.flags & FLAG_WHIRLPOOL) != 0) {
for (int id : ids) {
buffer.get(table.entries.get(id).whirlpool);
}
}
/* read the version numbers */
for (int id : ids) {
table.entries.get(id).version = buffer.getInt();
}
/* read the child sizes */
int[][] members = new int[size][];
for (int id : ids) {
members[id] = new int[buffer.getShort() & 0xFFFF];
}
/* read the child ids */
for (int id : ids) {
/* reset the accumulator and size */
accumulator = 0;
size = -1;
/* loop through the array of ids */
for (int i = 0; i < members[id].length; i++) {
int delta = buffer.getShort() & 0xFFFF;
members[id][i] = accumulator += delta;
if (members[id][i] > size) {
size = members[id][i];
}
}
size++;
/* and allocate specific entries within the array */
for (int child : members[id]) {
table.entries.get(id).entries.put(child, new ChildEntry());
}
}
/* read the child identifiers if present */
if ((table.flags & FLAG_IDENTIFIERS) != 0) {
for (int id : ids) {
for (int child : members[id]) {
table.entries.get(id).entries.get(child).identifier = buffer.getInt();
}
}
}
/* return the table we constructed */
return table;
}
/**
* The format of this table.
*/
private int format;
/**
* The version of this table.
*/
private int version;
/**
* The flags of this table.
*/
private int flags;
/**
* The entries in this table.
*/
private SortedMap<Integer, Entry> entries = new TreeMap<>();
/**
* The mapping for the named entries in the table.
*/
private Map<Integer, Integer> namedEntries = new HashMap<>();
/**
* Gets the format of this table.
* @return The format.
*/
public int getFormat() {
return format;
}
/**
* Sets the format of this table.
* @param format The format.
*/
public void setFormat(int format) {
this.format = format;
}
/**
* Gets the version of this table.
* @return The version of this table.
*/
public int getVersion() {
return version;
}
/**
* Sets the version of this table.
* @param version The version.
*/
public void setVersion(int version) {
this.version = version;
}
/**
* Gets the flags of this table.
* @return The flags.
*/
public int getFlags() {
return flags;
}
/**
* Sets the flags of this table.
* @param flags The flags.
*/
public void setFlags(int flags) {
this.flags = flags;
}
/**
* Gets the id for a named entry.
* @param name The name of the entry.
* @return The entry id or {@code -1} if the entry does not exist.
*/
public int getEntryId(String name) {
int hash = StringUtils.hash(name);
if(!namedEntries.containsKey(hash)) {
return -1;
}
return namedEntries.get(hash);
}
/**
* Gets the entry with the specified id, or {@code null} if it does not
* exist.
* @param id The id.
* @return The entry.
*/
public Entry getEntry(int id) {
return entries.get(id);
}
/**
* Gets the child entry with the specified id, or {@code null} if it does
* not exist.
* @param id The parent id.
* @param child The child id.
* @return The entry.
*/
public ChildEntry getEntry(int id, int child) {
Entry entry = entries.get(id);
if (entry == null)
return null;
return entry.getEntry(child);
}
/**
* Replaces or inserts the entry with the specified id.
* @param id The id.
* @param entry The entry.
*/
public void putEntry(int id, Entry entry) {
entries.put(id, entry);
}
/**
* Removes the entry with the specified id.
* @param id The id.
*/
public void removeEntry(int id) {
entries.remove(id);
}
/**
* Gets the number of actual entries.
* @return The number of actual entries.
*/
public int size() {
return entries.size();
}
/**
* Gets the maximum number of entries in this table.
* @return The maximum number of entries.
*/
public int capacity() {
if (entries.isEmpty())
return 0;
return entries.lastKey() + 1;
}
/**
* Encodes this {@link ReferenceTable} into a {@link ByteBuffer}.
* @return The {@link ByteBuffer}.
* @throws IOException if an I/O error occurs.
*/
public ByteBuffer encode() throws IOException {
/*
* we can't (easily) predict the size ahead of time, so we write to a
* stream and then to the buffer
*/
ByteArrayOutputStream bout = new ByteArrayOutputStream();
try (DataOutputStream os = new DataOutputStream(bout)) {
/* write the header */
os.write(format);
if (format >= 6) {
os.writeInt(version);
}
os.write(flags);
/* calculate and write the number of non-null entries */
os.writeShort(entries.size());
/* write the ids */
int last = 0;
for (int id = 0; id < capacity(); id++) {
if (entries.containsKey(id)) {
int delta = id - last;
os.writeShort(delta);
last = id;
}
}
/* write the identifiers if required */
if ((flags & FLAG_IDENTIFIERS) != 0) {
for (Entry entry : entries.values()) {
os.writeInt(entry.identifier);
}
}
/* write the CRC checksums */
for (Entry entry : entries.values()) {
os.writeInt(entry.crc);
}
/* write the whirlpool digests if required */
if ((flags & FLAG_WHIRLPOOL) != 0) {
for (Entry entry : entries.values()) {
os.write(entry.whirlpool);
}
}
/* write the versions */
for (Entry entry : entries.values()) {
os.writeInt(entry.version);
}
/* calculate and write the number of non-null child entries */
for (Entry entry : entries.values()) {
os.writeShort(entry.entries.size());
}
/* write the child ids */
for (Entry entry : entries.values()) {
last = 0;
for (int id = 0; id < entry.capacity(); id++) {
if (entry.entries.containsKey(id)) {
int delta = id - last;
os.writeShort(delta);
last = id;
}
}
}
/* write the child identifiers if required */
if ((flags & FLAG_IDENTIFIERS) != 0) {
for (Entry entry : entries.values()) {
for (ChildEntry child : entry.entries.values()) {
os.writeInt(child.identifier);
}
}
}
/* convert the stream to a byte array and then wrap a buffer */
byte[] bytes = bout.toByteArray();
return ByteBuffer.wrap(bytes);
}
}
}