From f984f357d8dcaa4cd984892aeddac3fafde0ae6e Mon Sep 17 00:00:00 2001 From: Graham Date: Tue, 1 Sep 2020 17:38:07 +0100 Subject: [PATCH] Add Js5Index implementation Signed-off-by: Graham --- cache/build.gradle.kts | 1 + .../main/java/dev/openrs2/cache/Js5Index.kt | 369 +++++++++ .../java/dev/openrs2/cache/Js5Protocol.kt | 24 + .../main/java/dev/openrs2/cache/NamedEntry.kt | 18 + .../dev/openrs2/cache/NamedEntryCollection.kt | 362 +++++++++ .../java/dev/openrs2/cache/Js5IndexTest.kt | 415 ++++++++++ .../openrs2/cache/NamedEntryCollectionTest.kt | 746 ++++++++++++++++++ .../dev/openrs2/cache/index/all-flags.dat | Bin 0 -> 102 bytes .../dev/openrs2/cache/index/digest.dat | Bin 0 -> 80 bytes .../dev/openrs2/cache/index/empty.dat | Bin 0 -> 4 bytes .../dev/openrs2/cache/index/lengths.dat | Bin 0 -> 24 bytes .../dev/openrs2/cache/index/named.dat | Bin 0 -> 26 bytes .../dev/openrs2/cache/index/no-flags.dat | Bin 0 -> 46 bytes .../dev/openrs2/cache/index/null-digest.dat | Bin 0 -> 80 bytes .../dev/openrs2/cache/index/smart.dat | Bin 0 -> 40 bytes .../cache/index/uncompressed-checksum.dat | Bin 0 -> 20 bytes .../dev/openrs2/cache/index/versioned.dat | Bin 0 -> 8 bytes 17 files changed, 1935 insertions(+) create mode 100644 cache/src/main/java/dev/openrs2/cache/Js5Index.kt create mode 100644 cache/src/main/java/dev/openrs2/cache/Js5Protocol.kt create mode 100644 cache/src/main/java/dev/openrs2/cache/NamedEntry.kt create mode 100644 cache/src/main/java/dev/openrs2/cache/NamedEntryCollection.kt create mode 100644 cache/src/test/java/dev/openrs2/cache/Js5IndexTest.kt create mode 100644 cache/src/test/java/dev/openrs2/cache/NamedEntryCollectionTest.kt create mode 100644 cache/src/test/resources/dev/openrs2/cache/index/all-flags.dat create mode 100644 cache/src/test/resources/dev/openrs2/cache/index/digest.dat create mode 100644 cache/src/test/resources/dev/openrs2/cache/index/empty.dat create mode 100644 cache/src/test/resources/dev/openrs2/cache/index/lengths.dat create mode 100644 cache/src/test/resources/dev/openrs2/cache/index/named.dat create mode 100644 cache/src/test/resources/dev/openrs2/cache/index/no-flags.dat create mode 100644 cache/src/test/resources/dev/openrs2/cache/index/null-digest.dat create mode 100644 cache/src/test/resources/dev/openrs2/cache/index/smart.dat create mode 100644 cache/src/test/resources/dev/openrs2/cache/index/uncompressed-checksum.dat create mode 100644 cache/src/test/resources/dev/openrs2/cache/index/versioned.dat diff --git a/cache/build.gradle.kts b/cache/build.gradle.kts index 6f561823..64eef48c 100644 --- a/cache/build.gradle.kts +++ b/cache/build.gradle.kts @@ -10,6 +10,7 @@ dependencies { implementation(project(":compress")) implementation(project(":crypto")) implementation(project(":util")) + implementation("it.unimi.dsi:fastutil:${Versions.fastutil}") testImplementation("com.google.jimfs:jimfs:${Versions.jimfs}") } diff --git a/cache/src/main/java/dev/openrs2/cache/Js5Index.kt b/cache/src/main/java/dev/openrs2/cache/Js5Index.kt new file mode 100644 index 00000000..6a803fff --- /dev/null +++ b/cache/src/main/java/dev/openrs2/cache/Js5Index.kt @@ -0,0 +1,369 @@ +package dev.openrs2.cache + +import dev.openrs2.buffer.readUnsignedIntSmart +import dev.openrs2.buffer.writeUnsignedIntSmart +import dev.openrs2.crypto.Whirlpool +import io.netty.buffer.ByteBuf +import io.netty.buffer.ByteBufUtil + +public class Js5Index( + public var protocol: Js5Protocol, + public var version: Int = 0, + public var hasNames: Boolean = false, + public var hasDigests: Boolean = false, + public var hasLengths: Boolean = false, + public var hasUncompressedChecksums: Boolean = false +) : NamedEntryCollection(::Group) { + public class Group internal constructor( + parent: NamedEntryCollection, + override val id: Int + ) : NamedEntryCollection(::File), NamedEntry { + private var parent: NamedEntryCollection? = parent + public var version: Int = 0 + public var checksum: Int = 0 + public var uncompressedChecksum: Int = 0 + public var length: Int = 0 + public var uncompressedLength: Int = 0 + + public override var nameHash: Int = -1 + set(value) { + parent?.rename(id, field, value) + field = value + } + + public var digest: ByteArray? = null + set(value) { + require(value == null || value.size == Whirlpool.DIGESTBYTES) + field = value + } + + override fun remove() { + parent?.remove(this) + parent = null + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + if (!super.equals(other)) return false + + other as Group + + if (id != other.id) return false + if (version != other.version) return false + if (checksum != other.checksum) return false + if (uncompressedChecksum != other.uncompressedChecksum) return false + if (length != other.length) return false + if (uncompressedLength != other.uncompressedLength) return false + if (nameHash != other.nameHash) return false + if (digest != null) { + if (other.digest == null) return false + if (!digest.contentEquals(other.digest)) return false + } else if (other.digest != null) return false + + return true + } + + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + id + result = 31 * result + version + result = 31 * result + checksum + result = 31 * result + uncompressedChecksum + result = 31 * result + length + result = 31 * result + uncompressedLength + result = 31 * result + nameHash + result = 31 * result + (digest?.contentHashCode() ?: 0) + return result + } + + override fun toString(): String { + val digest = digest + val hex = if (digest != null) { + ByteBufUtil.hexDump(digest) + } else { + "null" + } + return "Group{id=$id, nameHash=$nameHash, version=$version, checksum=$checksum, " + + "uncompressedChecksum=$uncompressedChecksum, length=$length, uncompressedLength=$uncompressedLength, " + + "digest=$hex, size=$size, capacity=$capacity}" + } + } + + public class File internal constructor( + parent: NamedEntryCollection, + override val id: Int, + ) : NamedEntry { + private var parent: NamedEntryCollection? = parent + + public override var nameHash: Int = -1 + set(value) { + parent?.rename(id, field, value) + field = value + } + + override fun remove() { + parent?.remove(this) + parent = null + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as File + + if (id != other.id) return false + if (nameHash != other.nameHash) return false + + return true + } + + override fun hashCode(): Int { + var result = id + result = 31 * result + nameHash + return result + } + + override fun toString(): String { + return "File{id=$id, nameHash=$nameHash}" + } + } + + public fun write(buf: ByteBuf) { + val writeFunc = if (protocol >= Js5Protocol.SMART) { + buf::writeUnsignedIntSmart + } else { + { value -> + check(value in 0..65535) { + "$value outside of valid non-SMART range" + } + buf.writeShort(value) + } + } + + buf.writeByte(protocol.id) + + if (protocol >= Js5Protocol.VERSIONED) { + buf.writeInt(version) + } + + var flags = 0 + if (hasNames) { + flags = flags or FLAG_NAMES + } + if (hasDigests) { + flags = flags or FLAG_DIGESTS + } + if (hasLengths) { + flags = flags or FLAG_LENGTHS + } + if (hasUncompressedChecksums) { + flags = flags or FLAG_UNCOMPRESSED_CHECKSUMS + } + buf.writeByte(flags) + + writeFunc(size) + + var prevGroupId = 0 + for (group in this) { + writeFunc(group.id - prevGroupId) + prevGroupId = group.id + } + + if (hasNames) { + for (group in this) { + buf.writeInt(group.nameHash) + } + } + + for (group in this) { + buf.writeInt(group.checksum) + } + + if (hasUncompressedChecksums) { + for (group in this) { + buf.writeInt(group.uncompressedChecksum) + } + } + + if (hasDigests) { + for (group in this) { + val digest = group.digest + if (digest != null) { + buf.writeBytes(digest) + } else { + buf.writeZero(Whirlpool.DIGESTBYTES) + } + } + } + + if (hasLengths) { + for (group in this) { + buf.writeInt(group.length) + buf.writeInt(group.uncompressedLength) + } + } + + for (group in this) { + buf.writeInt(group.version) + } + + for (group in this) { + writeFunc(group.size) + } + + for (group in this) { + var prevFileId = 0 + for (file in group) { + writeFunc(file.id - prevFileId) + prevFileId = file.id + } + } + + if (hasNames) { + for (group in this) { + for (file in group) { + buf.writeInt(file.nameHash) + } + } + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + if (!super.equals(other)) return false + + other as Js5Index + + if (protocol != other.protocol) return false + if (version != other.version) return false + if (hasNames != other.hasNames) return false + if (hasDigests != other.hasDigests) return false + if (hasLengths != other.hasLengths) return false + if (hasUncompressedChecksums != other.hasUncompressedChecksums) return false + + return true + } + + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + protocol.hashCode() + result = 31 * result + version + result = 31 * result + hasNames.hashCode() + result = 31 * result + hasDigests.hashCode() + result = 31 * result + hasLengths.hashCode() + result = 31 * result + hasUncompressedChecksums.hashCode() + return result + } + + override fun toString(): String { + return "Js5Index{protocol=$protocol, version=$version, hasNames=$hasNames, hasDigests=$hasDigests, " + + "hasLengths=$hasLengths, hasUncompressedChecksums=$hasUncompressedChecksums, size=$size, " + + "capacity=$capacity}" + } + + public companion object { + private const val FLAG_NAMES = 0x01 + private const val FLAG_DIGESTS = 0x02 + private const val FLAG_LENGTHS = 0x04 + private const val FLAG_UNCOMPRESSED_CHECKSUMS = 0x08 + + public fun read(buf: ByteBuf): Js5Index { + val number = buf.readUnsignedByte().toInt() + val protocol = Js5Protocol.fromId(number) + require(protocol != null) { + "Unsupported JS5 protocol number: $number" + } + + val readFunc = if (protocol >= Js5Protocol.SMART) { + buf::readUnsignedIntSmart + } else { + buf::readUnsignedShort + } + + val version = if (protocol >= Js5Protocol.VERSIONED) { + buf.readInt() + } else { + 0 + } + val flags = buf.readUnsignedByte().toInt() + val size = readFunc() + + val index = Js5Index( + protocol, + version, + hasNames = flags and FLAG_NAMES != 0, + hasDigests = flags and FLAG_DIGESTS != 0, + hasLengths = flags and FLAG_LENGTHS != 0, + hasUncompressedChecksums = flags and FLAG_UNCOMPRESSED_CHECKSUMS != 0 + ) + + var prevGroupId = 0 + for (i in 0 until size) { + prevGroupId += readFunc() + index.createOrGet(prevGroupId) + } + + if (index.hasNames) { + for (group in index) { + group.nameHash = buf.readInt() + } + } + + for (group in index) { + group.checksum = buf.readInt() + } + + if (index.hasUncompressedChecksums) { + for (group in index) { + group.uncompressedChecksum = buf.readInt() + } + } + + if (index.hasDigests) { + for (group in index) { + val digest = ByteArray(Whirlpool.DIGESTBYTES) + buf.readBytes(digest) + group.digest = digest + } + } + + if (index.hasLengths) { + for (group in index) { + group.length = buf.readInt() + group.uncompressedLength = buf.readInt() + } + } + + for (group in index) { + group.version = buf.readInt() + } + + val groupSizes = IntArray(size) { + readFunc() + } + + for ((i, group) in index.withIndex()) { + val groupSize = groupSizes[i] + + var prevFileId = 0 + for (j in 0 until groupSize) { + prevFileId += readFunc() + group.createOrGet(prevFileId) + } + } + + if (index.hasNames) { + for (group in index) { + for (file in group) { + file.nameHash = buf.readInt() + } + } + } + + return index + } + } +} diff --git a/cache/src/main/java/dev/openrs2/cache/Js5Protocol.kt b/cache/src/main/java/dev/openrs2/cache/Js5Protocol.kt new file mode 100644 index 00000000..ae608973 --- /dev/null +++ b/cache/src/main/java/dev/openrs2/cache/Js5Protocol.kt @@ -0,0 +1,24 @@ +package dev.openrs2.cache + +public enum class Js5Protocol { + ORIGINAL, + VERSIONED, + SMART; + + public val id: Int + get() = ordinal + OFFSET + + public companion object { + private const val OFFSET = 5 + + public fun fromId(id: Int): Js5Protocol? { + val ordinal = id - OFFSET + val values = values() + return if (ordinal >= 0 && ordinal < values.size) { + values[ordinal] + } else { + null + } + } + } +} diff --git a/cache/src/main/java/dev/openrs2/cache/NamedEntry.kt b/cache/src/main/java/dev/openrs2/cache/NamedEntry.kt new file mode 100644 index 00000000..4b42aea8 --- /dev/null +++ b/cache/src/main/java/dev/openrs2/cache/NamedEntry.kt @@ -0,0 +1,18 @@ +package dev.openrs2.cache + +import dev.openrs2.util.krHashCode + +public interface NamedEntry { + public val id: Int + public var nameHash: Int + + public fun setName(name: String) { + nameHash = name.krHashCode() + } + + public fun clearName() { + nameHash = -1 + } + + public fun remove() +} diff --git a/cache/src/main/java/dev/openrs2/cache/NamedEntryCollection.kt b/cache/src/main/java/dev/openrs2/cache/NamedEntryCollection.kt new file mode 100644 index 00000000..15b29f72 --- /dev/null +++ b/cache/src/main/java/dev/openrs2/cache/NamedEntryCollection.kt @@ -0,0 +1,362 @@ +package dev.openrs2.cache + +import dev.openrs2.util.krHashCode +import it.unimi.dsi.fastutil.ints.Int2IntMap +import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap +import it.unimi.dsi.fastutil.ints.Int2ObjectAVLTreeMap + +/** + * A specialist collection type entirely designed for use by the [Js5Index] + * class. + * + * Entries may be accessed by their ID number or by the hash of their name, if + * they are named. + * + * Entries are sorted by their ID. The IDs should be fairly dense starting + * around 0, as the client stores entries in an array. Nevertheless, this + * implementation currently supports sparse IDs efficiently as the entries are + * actually stored in a tree map, primarily to make manipulation of the + * collection simpler (the client's implementation has the luxury of being + * read-only!) + * + * As the vast majority of [Js5Index.Group]s only have a single [Js5Index.File] + * entry, there is a special case for single entry collections. This avoids the + * memory overhead of the underlying collections in this special case, at the + * expense of making the logic slightly more complicated. + * + * Entries without a name have a name hash of -1. This is consistent with the + * client, which sets the name hash array to -1 before populating it. If the + * index or group is sparse, any non-existent IDs still have a hash of -1, which + * is passed into the client's IntHashTable class. This makes -1 unusable, so I + * feel this choice is justified. + * + * In practice, I think indexes generally either have names for every file or + * none at all, but allowing a mixture in this implementation makes mutating + * the index simpler. + * + * This implementation does not allow multiple entries with the same name hash, + * and it throws an exception if it detects a collision. This differs from the + * client's implementation, which does not throw an exception and allows the + * colliding entry with the lowest ID to be referred to by the name. + */ +public abstract class NamedEntryCollection( + private val entryConstructor: (NamedEntryCollection, Int) -> T +) : MutableIterable { + private var singleEntry: T? = null + private var entries: Int2ObjectAVLTreeMap? = null + private var nameHashTable: Int2IntMap? = null + + public val size: Int + get() { + if (singleEntry != null) { + return 1 + } + + val entries = entries ?: return 0 + return entries.size + } + + public val capacity: Int + get() { + val entry = singleEntry + if (entry != null) { + return entry.id + 1 + } + + val entries = entries ?: return 0 + check(entries.isNotEmpty()) + return entries.lastIntKey() + 1 + } + + public fun contains(id: Int): Boolean { + require(id >= 0) + + val entry = singleEntry + if (entry?.id == id) { + return true + } + + val entries = entries ?: return false + return entries.containsKey(id) + } + + public fun containsNamed(nameHash: Int): Boolean { + require(nameHash != -1) + + val entry = singleEntry + if (entry?.nameHash == nameHash) { + return true + } + + return nameHashTable?.containsKey(nameHash) ?: false + } + + public fun contains(name: String): Boolean { + return containsNamed(name.krHashCode()) + } + + public operator fun get(id: Int): T? { + require(id >= 0) + + val entry = singleEntry + if (entry?.id == id) { + return entry + } + + val entries = entries ?: return null + return entries[id] + } + + public fun getNamed(nameHash: Int): T? { + require(nameHash != -1) + + val entry = singleEntry + if (entry?.nameHash == nameHash) { + return entry + } + + val nameHashTable = nameHashTable ?: return null + val id = nameHashTable[nameHash] + return if (id != -1) { + get(id) ?: throw IllegalStateException() + } else { + null + } + } + + public operator fun get(name: String): T? { + return getNamed(name.krHashCode()) + } + + public fun createOrGet(id: Int): T { + var entry = get(id) + if (entry != null) { + return entry + } + + entry = entryConstructor(this, id) + + val singleEntry = singleEntry + var entries = entries + if (singleEntry == null && entries == null) { + this.singleEntry = entry + return entry + } + + if (entries == null) { + entries = Int2ObjectAVLTreeMap() + + if (singleEntry != null) { + entries[singleEntry.id] = singleEntry + + if (singleEntry.nameHash != -1) { + val nameHashTable = Int2IntOpenHashMap() + nameHashTable.defaultReturnValue(-1) + nameHashTable[singleEntry.nameHash] = singleEntry.id + this.nameHashTable = nameHashTable + } + } + + this.singleEntry = null + this.entries = entries + } + + entries[id] = entry + return entry + } + + public fun createOrGetNamed(nameHash: Int): T { + var entry = getNamed(nameHash) + if (entry != null) { + return entry + } + + entry = createOrGet(allocateId()) + entry.nameHash = nameHash + return entry + } + + public fun createOrGet(name: String): T { + return createOrGetNamed(name.krHashCode()) + } + + public fun remove(id: Int) { + get(id)?.remove() + } + + public fun removeNamed(nameHash: Int) { + getNamed(nameHash)?.remove() + } + + public fun remove(name: String) { + removeNamed(name.krHashCode()) + } + + private fun allocateId(): Int { + val size = size + val capacity = capacity + if (size == capacity) { + return capacity + } + + val singleEntry = singleEntry + if (singleEntry != null) { + check(singleEntry.id != 0) + return 0 + } + + val entries = entries + check(entries != null) + + for (id in 0 until capacity) { + if (!entries.containsKey(id)) { + return id + } + } + + throw IllegalStateException() + } + + internal fun rename(id: Int, prevNameHash: Int, newNameHash: Int) { + if (prevNameHash == newNameHash || singleEntry != null) { + return + } + + var nameHashTable = nameHashTable + if (nameHashTable != null && prevNameHash != -1) { + nameHashTable.remove(prevNameHash) + } + + if (newNameHash != -1) { + if (nameHashTable == null) { + nameHashTable = Int2IntOpenHashMap() + nameHashTable.defaultReturnValue(-1) + } + + val prevId = nameHashTable.put(newNameHash, id) + check(prevId == -1) { + "Name hash collision: $newNameHash is already used by entry $prevId" + } + + this.nameHashTable = nameHashTable + } + } + + internal fun remove(entry: T) { + if (singleEntry?.id == entry.id) { + singleEntry = null + return + } + + val entries = entries + check(entries != null) + + val removedEntry = entries.remove(entry.id) + check(removedEntry != null) + + if (entries.size == 1) { + val firstId = entries.firstIntKey() + singleEntry = entries[firstId] + this.entries = null + nameHashTable = null + return + } + + val nameHashTable = nameHashTable ?: return + if (entry.nameHash != -1) { + nameHashTable.remove(entry.nameHash) + + if (nameHashTable.isEmpty()) { + this.nameHashTable = null + } + } + } + + override fun iterator(): MutableIterator { + val singleEntry = singleEntry + if (singleEntry != null) { + return object : MutableIterator { + private var pos = 0 + private var removed = false + + override fun hasNext(): Boolean { + return pos == 0 + } + + override fun next(): T { + if (pos++ != 0) { + throw NoSuchElementException() + } + + return singleEntry + } + + override fun remove() { + check(pos == 1 && !removed) + removed = true + + singleEntry.remove() + } + } + } + + val entries = entries + if (entries != null) { + return object : MutableIterator { + private val it = entries.values.iterator() + private var last: T? = null + + override fun hasNext(): Boolean { + return it.hasNext() + } + + override fun next(): T { + last = null + + val entry = it.next() + last = entry + return entry + } + + override fun remove() { + val last = last + check(last != null) + last.remove() + this.last = null + } + } + } + + return object : MutableIterator { + override fun hasNext(): Boolean { + return false + } + + override fun next(): T { + throw NoSuchElementException() + } + + override fun remove() { + throw IllegalStateException() + } + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as NamedEntryCollection<*> + + if (singleEntry != other.singleEntry) return false + if (entries != other.entries) return false + + return true + } + + override fun hashCode(): Int { + var result = singleEntry?.hashCode() ?: 0 + result = 31 * result + (entries?.hashCode() ?: 0) + return result + } +} diff --git a/cache/src/test/java/dev/openrs2/cache/Js5IndexTest.kt b/cache/src/test/java/dev/openrs2/cache/Js5IndexTest.kt new file mode 100644 index 00000000..38ee0d12 --- /dev/null +++ b/cache/src/test/java/dev/openrs2/cache/Js5IndexTest.kt @@ -0,0 +1,415 @@ +package dev.openrs2.cache + +import dev.openrs2.buffer.use +import dev.openrs2.util.krHashCode +import io.netty.buffer.ByteBuf +import io.netty.buffer.ByteBufUtil +import io.netty.buffer.Unpooled +import org.junit.jupiter.api.assertThrows +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNull +import kotlin.test.assertTrue + +object Js5IndexTest { + private val emptyIndex = Js5Index(Js5Protocol.ORIGINAL) + + private val versionedIndex = Js5Index(Js5Protocol.VERSIONED, version = 0x12345678) + + private val noFlagsIndex = Js5Index(Js5Protocol.ORIGINAL).apply { + val group0 = createOrGet(0) + group0.checksum = 0x01234567 + group0.version = 0 + group0.createOrGet(0) + + val group1 = createOrGet(1) + group1.checksum = 0x89ABCDEF.toInt() + group1.version = 10 + + val group2 = createOrGet(3) + group2.checksum = 0xAAAA5555.toInt() + group2.version = 20 + group2.createOrGet(1) + group2.createOrGet(3) + } + + private val namedIndex = Js5Index(Js5Protocol.ORIGINAL, hasNames = true).apply { + val group0 = createOrGet("hello") + group0.checksum = 0x01234567 + group0.version = 0x89ABCDEF.toInt() + group0.createOrGet("world") + } + + private val smartIndex = Js5Index(Js5Protocol.SMART).apply { + val group0 = createOrGet(0) + group0.checksum = 0x01234567 + group0.version = 0x89ABCDEF.toInt() + group0.createOrGet(0) + group0.createOrGet(100000) + + val group1 = createOrGet(100000) + group1.checksum = 0xAAAA5555.toInt() + group1.version = 0x5555AAAA + } + + private val digestIndex = Js5Index(Js5Protocol.ORIGINAL, hasDigests = true).apply { + val group = createOrGet(0) + group.checksum = 0x01234567 + group.version = 0x89ABCDEF.toInt() + group.digest = ByteBufUtil.decodeHexDump( + "19FA61D75522A4669B44E39C1D2E1726C530232130D407F89AFEE0964997F7A7" + + "3E83BE698B288FEBCF88E3E03C4F0757EA8964E59B63D93708B138CC42A66EB3" + ) + } + + private val nullDigestIndex = Js5Index(Js5Protocol.ORIGINAL, hasDigests = true).apply { + val group = createOrGet(0) + group.checksum = 0x01234567 + group.version = 0x89ABCDEF.toInt() + group.digest = null + } + + private val lengthsIndex = Js5Index(Js5Protocol.ORIGINAL, hasLengths = true).apply { + val group = createOrGet(0) + group.checksum = 0x01234567 + group.version = 0x89ABCDEF.toInt() + group.length = 1000 + group.uncompressedLength = 2000 + } + + private val uncompressedChecksumIndex = Js5Index(Js5Protocol.ORIGINAL, hasUncompressedChecksums = true).apply { + val group = createOrGet(0) + group.checksum = 0x01234567 + group.version = 0x89ABCDEF.toInt() + group.uncompressedChecksum = 0xAAAA5555.toInt() + } + + private val allFlagsIndex = Js5Index( + Js5Protocol.ORIGINAL, + hasNames = true, + hasDigests = true, + hasLengths = true, + hasUncompressedChecksums = true + ).apply { + val group = createOrGet("hello") + group.checksum = 0x01234567 + group.version = 0x89ABCDEF.toInt() + group.digest = ByteBufUtil.decodeHexDump( + "19FA61D75522A4669B44E39C1D2E1726C530232130D407F89AFEE0964997F7A7" + + "3E83BE698B288FEBCF88E3E03C4F0757EA8964E59B63D93708B138CC42A66EB3" + ) + group.length = 1000 + group.uncompressedLength = 2000 + group.uncompressedChecksum = 0xAAAA5555.toInt() + group.createOrGet("world") + } + + @Test + fun testReadEmpty() { + read("empty.dat").use { buf -> + val index = Js5Index.read(buf) + assertFalse(buf.isReadable) + assertEquals(emptyIndex, index) + } + } + + @Test + fun testWriteEmpty() { + Unpooled.buffer().use { actual -> + emptyIndex.write(actual) + + read("empty.dat").use { expected -> + assertEquals(expected, actual) + } + } + } + + @Test + fun testReadUnsupportedProtocol() { + Unpooled.wrappedBuffer(byteArrayOf(4)).use { buf -> + assertThrows { + Js5Index.read(buf) + } + } + + Unpooled.wrappedBuffer(byteArrayOf(8)).use { buf -> + assertThrows { + Js5Index.read(buf) + } + } + } + + @Test + fun testReadVersioned() { + read("versioned.dat").use { buf -> + val index = Js5Index.read(buf) + assertFalse(buf.isReadable) + assertEquals(versionedIndex, index) + } + } + + @Test + fun testWriteVersioned() { + Unpooled.buffer().use { actual -> + versionedIndex.write(actual) + + read("versioned.dat").use { expected -> + assertEquals(expected, actual) + } + } + } + + @Test + fun testReadNoFlags() { + read("no-flags.dat").use { buf -> + val index = Js5Index.read(buf) + assertFalse(buf.isReadable) + assertEquals(noFlagsIndex, index) + } + } + + @Test + fun testWriteNoFlags() { + Unpooled.buffer().use { actual -> + noFlagsIndex.write(actual) + + read("no-flags.dat").use { expected -> + assertEquals(expected, actual) + } + } + } + + @Test + fun testReadNamed() { + read("named.dat").use { buf -> + val index = Js5Index.read(buf) + assertFalse(buf.isReadable) + assertEquals(namedIndex, index) + } + } + + @Test + fun testWriteNamed() { + Unpooled.buffer().use { actual -> + namedIndex.write(actual) + + read("named.dat").use { expected -> + assertEquals(expected, actual) + } + } + } + + @Test + fun testReadSmart() { + read("smart.dat").use { buf -> + val index = Js5Index.read(buf) + assertFalse(buf.isReadable) + assertEquals(smartIndex, index) + } + } + + @Test + fun testWriteSmart() { + Unpooled.buffer().use { actual -> + smartIndex.write(actual) + + read("smart.dat").use { expected -> + assertEquals(expected, actual) + } + } + } + + @Test + fun testWriteNonSmartOutOfRange() { + val index = Js5Index(Js5Protocol.ORIGINAL) + index.createOrGet(65536) + + Unpooled.buffer().use { buf -> + assertThrows { + index.write(buf) + } + } + } + + @Test + fun testReadDigest() { + read("digest.dat").use { buf -> + val index = Js5Index.read(buf) + assertFalse(buf.isReadable) + assertEquals(digestIndex, index) + } + } + + @Test + fun testWriteDigest() { + Unpooled.buffer().use { actual -> + digestIndex.write(actual) + + read("digest.dat").use { expected -> + assertEquals(expected, actual) + } + } + } + + @Test + fun testWriteNullDigest() { + Unpooled.buffer().use { actual -> + nullDigestIndex.write(actual) + + read("null-digest.dat").use { expected -> + assertEquals(expected, actual) + } + } + } + + @Test + fun testReadLengths() { + read("lengths.dat").use { buf -> + val index = Js5Index.read(buf) + assertFalse(buf.isReadable) + assertEquals(lengthsIndex, index) + } + } + + @Test + fun testWriteLengths() { + Unpooled.buffer().use { actual -> + lengthsIndex.write(actual) + + read("lengths.dat").use { expected -> + assertEquals(expected, actual) + } + } + } + + @Test + fun testReadUncompressedChecksum() { + read("uncompressed-checksum.dat").use { buf -> + val index = Js5Index.read(buf) + assertFalse(buf.isReadable) + assertEquals(uncompressedChecksumIndex, index) + } + } + + @Test + fun testWriteUncompressedChecksum() { + Unpooled.buffer().use { actual -> + uncompressedChecksumIndex.write(actual) + + read("uncompressed-checksum.dat").use { expected -> + assertEquals(expected, actual) + } + } + } + + @Test + fun testReadAllFlags() { + read("all-flags.dat").use { buf -> + val index = Js5Index.read(buf) + assertFalse(buf.isReadable) + assertEquals(allFlagsIndex, index) + } + } + + @Test + fun testWriteAllFlags() { + Unpooled.buffer().use { actual -> + allFlagsIndex.write(actual) + + read("all-flags.dat").use { expected -> + println(ByteBufUtil.prettyHexDump(actual)) + + assertEquals(expected, actual) + } + } + } + + @Test + fun testRenameGroup() { + val index = Js5Index(Js5Protocol.ORIGINAL) + + val group = index.createOrGet(0) + assertEquals(-1, group.nameHash) + assertNull(index["hello"]) + + group.setName("hello") + assertEquals("hello".krHashCode(), group.nameHash) + assertEquals(group, index["hello"]) + + group.clearName() + assertEquals(-1, group.nameHash) + assertNull(index["hello"]) + } + + @Test + fun testRenameFile() { + val index = Js5Index(Js5Protocol.ORIGINAL) + val group = index.createOrGet(0) + + val file = group.createOrGet(0) + assertEquals(-1, file.nameHash) + assertNull(group["hello"]) + + file.setName("hello") + assertEquals("hello".krHashCode(), file.nameHash) + assertEquals(file, group["hello"]) + + file.clearName() + assertEquals(-1, file.nameHash) + assertNull(group["hello"]) + } + + @Test + fun testRemoveGroup() { + val index = Js5Index(Js5Protocol.ORIGINAL) + + val group = index.createOrGet(0) + assertEquals(1, index.size) + assertEquals(1, index.capacity) + assertTrue(index.contains(0)) + assertEquals(group, index[0]) + + group.remove() + assertEquals(0, index.size) + assertEquals(0, index.capacity) + assertFalse(index.contains(0)) + assertNull(index[0]) + + group.remove() + assertEquals(0, index.size) + assertEquals(0, index.capacity) + assertFalse(index.contains(0)) + assertNull(index[0]) + } + + @Test + fun testRemoveFile() { + val index = Js5Index(Js5Protocol.ORIGINAL) + val group = index.createOrGet(0) + + val file = group.createOrGet(0) + assertEquals(1, group.size) + assertEquals(1, group.capacity) + assertTrue(group.contains(0)) + assertEquals(file, group[0]) + + file.remove() + assertEquals(0, group.size) + assertEquals(0, group.capacity) + assertFalse(group.contains(0)) + assertNull(group[0]) + + file.remove() + assertEquals(0, group.size) + assertEquals(0, group.capacity) + assertFalse(group.contains(0)) + assertNull(group[0]) + } + + private fun read(name: String): ByteBuf { + Js5IndexTest::class.java.getResourceAsStream("index/$name").use { input -> + return Unpooled.wrappedBuffer(input.readAllBytes()) + } + } +} diff --git a/cache/src/test/java/dev/openrs2/cache/NamedEntryCollectionTest.kt b/cache/src/test/java/dev/openrs2/cache/NamedEntryCollectionTest.kt new file mode 100644 index 00000000..f4d2fb26 --- /dev/null +++ b/cache/src/test/java/dev/openrs2/cache/NamedEntryCollectionTest.kt @@ -0,0 +1,746 @@ +package dev.openrs2.cache + +import dev.openrs2.util.krHashCode +import org.junit.jupiter.api.assertThrows +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNull +import kotlin.test.assertTrue + +object NamedEntryCollectionTest { + private class TestEntry( + parent: NamedEntryCollection, + override val id: Int + ) : NamedEntry { + private var parent: NamedEntryCollection? = parent + + override var nameHash: Int = -1 + set(value) { + parent?.rename(id, field, value) + field = value + } + + override fun remove() { + parent?.remove(this) + parent = null + } + } + + private class TestCollection : NamedEntryCollection(::TestEntry) + + @Test + fun testBounds() { + val collection = TestCollection() + + assertThrows { + collection.contains(-1) + } + + assertThrows { + collection.containsNamed(-1) + } + + assertThrows { + collection[-1] + } + + assertThrows { + collection.getNamed(-1) + } + + assertThrows { + collection.createOrGet(-1) + } + + assertThrows { + collection.createOrGetNamed(-1) + } + + assertThrows { + collection.remove(-1) + } + + assertThrows { + collection.removeNamed(-1) + } + } + + @Test + fun testEmpty() { + val collection = TestCollection() + assertEquals(0, collection.size) + assertEquals(0, collection.capacity) + assertEquals(emptyList(), collection.toList()) + } + + @Test + fun testSingleEntry() { + val collection = TestCollection() + + val entry = collection.createOrGet(0) + assertEquals(0, entry.id) + assertEquals(-1, entry.nameHash) + + assertEquals(1, collection.size) + assertEquals(1, collection.capacity) + + assertEquals(entry, collection.createOrGet(0)) + + assertEquals(1, collection.size) + assertEquals(1, collection.capacity) + + assertTrue(collection.contains(0)) + assertFalse(collection.contains(1)) + + assertEquals(entry, collection[0]) + assertNull(collection[1]) + + assertEquals(listOf(entry), collection.toList()) + } + + @Test + fun testMultipleEntries() { + val collection = TestCollection() + + val entry0 = collection.createOrGet(0) + assertEquals(0, entry0.id) + assertEquals(-1, entry0.nameHash) + + val entry1 = collection.createOrGet(1) + assertEquals(1, entry1.id) + assertEquals(-1, entry1.nameHash) + + assertEquals(2, collection.size) + assertEquals(2, collection.capacity) + + assertEquals(entry0, collection.createOrGet(0)) + assertEquals(entry1, collection.createOrGet(1)) + + assertEquals(2, collection.size) + assertEquals(2, collection.capacity) + + assertTrue(collection.contains(0)) + assertTrue(collection.contains(1)) + assertFalse(collection.contains(2)) + + assertEquals(entry0, collection[0]) + assertEquals(entry1, collection[1]) + assertNull(collection[2]) + + assertEquals(listOf(entry0, entry1), collection.toList()) + } + + @Test + fun testSingleNamedEntry() { + val collection = TestCollection() + + val entry = collection.createOrGet("hello") + assertEquals(0, entry.id) + assertEquals("hello".krHashCode(), entry.nameHash) + + assertEquals(1, collection.size) + assertEquals(1, collection.capacity) + + assertEquals(entry, collection.createOrGet("hello")) + + assertEquals(1, collection.size) + assertEquals(1, collection.capacity) + + assertTrue(collection.contains("hello")) + assertFalse(collection.contains("world")) + + assertEquals(entry, collection["hello"]) + assertNull(collection["world"]) + + assertEquals(listOf(entry), collection.toList()) + } + + @Test + fun testMultipleNamedEntries() { + val collection = TestCollection() + + val entry0 = collection.createOrGet("hello") + assertEquals(0, entry0.id) + assertEquals("hello".krHashCode(), entry0.nameHash) + + val entry1 = collection.createOrGet("world") + assertEquals(1, entry1.id) + assertEquals("world".krHashCode(), entry1.nameHash) + + assertEquals(2, collection.size) + assertEquals(2, collection.capacity) + + assertEquals(entry0, collection.createOrGet("hello")) + assertEquals(entry1, collection.createOrGet("world")) + + assertEquals(2, collection.size) + assertEquals(2, collection.capacity) + + assertTrue(collection.contains("hello")) + assertTrue(collection.contains("world")) + assertFalse(collection.contains("!")) + + assertEquals(entry0, collection["hello"]) + assertEquals(entry1, collection["world"]) + assertNull(collection["!"]) + + assertEquals(listOf(entry0, entry1), collection.toList()) + } + + @Test + fun testRename() { + val collection = TestCollection() + + val entry = collection.createOrGet("hello") + assertEquals(0, entry.id) + assertEquals("hello".krHashCode(), entry.nameHash) + + assertTrue(collection.contains("hello")) + assertFalse(collection.contains("world")) + + assertEquals(entry, collection["hello"]) + assertNull(collection["world"]) + + entry.setName("world") + + assertFalse(collection.contains("hello")) + assertTrue(collection.contains("world")) + + assertNull(collection["hello"]) + assertEquals(entry, collection["world"]) + } + + @Test + fun testSingleEntrySetName() { + val collection = TestCollection() + + val entry = collection.createOrGet(0) + assertEquals(0, entry.id) + assertEquals(-1, entry.nameHash) + + assertFalse(collection.contains("hello")) + assertNull(collection["hello"]) + + entry.setName("hello") + assertEquals("hello".krHashCode(), entry.nameHash) + + assertTrue(collection.contains("hello")) + assertEquals(entry, collection["hello"]) + } + + @Test + fun testSingleEntryResetName() { + val collection = TestCollection() + + val entry = collection.createOrGet("hello") + assertEquals(0, entry.id) + assertEquals("hello".krHashCode(), entry.nameHash) + + assertTrue(collection.contains("hello")) + assertEquals(entry, collection["hello"]) + + entry.nameHash = -1 + assertEquals(-1, entry.nameHash) + + assertFalse(collection.contains("hello")) + assertNull(collection["hello"]) + } + + @Test + fun testMultipleEntriesSetName() { + val collection = TestCollection() + + val entry0 = collection.createOrGet(0) + assertEquals(0, entry0.id) + assertEquals(-1, entry0.nameHash) + + val entry1 = collection.createOrGet(1) + assertEquals(1, entry1.id) + assertEquals(-1, entry1.nameHash) + + assertFalse(collection.contains("hello")) + assertNull(collection["hello"]) + + entry0.setName("hello") + assertEquals("hello".krHashCode(), entry0.nameHash) + + assertTrue(collection.contains("hello")) + assertEquals(entry0, collection["hello"]) + } + + @Test + fun testMultipleEntriesResetName() { + val collection = TestCollection() + + val entry0 = collection.createOrGet("hello") + assertEquals(0, entry0.id) + assertEquals("hello".krHashCode(), entry0.nameHash) + + val entry1 = collection.createOrGet(1) + assertEquals(1, entry1.id) + assertEquals(-1, entry1.nameHash) + + assertTrue(collection.contains("hello")) + assertEquals(entry0, collection["hello"]) + + entry0.nameHash = -1 + assertEquals(-1, entry0.nameHash) + + assertFalse(collection.contains("hello")) + assertNull(collection["hello"]) + } + + @Test + fun testSingleEntryRemove() { + val collection = TestCollection() + + val entry = collection.createOrGet(0) + assertEquals(0, entry.id) + assertEquals(-1, entry.nameHash) + + assertEquals(1, collection.size) + assertEquals(1, collection.capacity) + assertEquals(entry, collection[0]) + assertEquals(listOf(entry), collection.toList()) + + entry.remove() + + assertEquals(0, collection.size) + assertEquals(0, collection.capacity) + assertNull(collection[0]) + assertEquals(emptyList(), collection.toList()) + + entry.setName("hello") + assertEquals("hello".krHashCode(), entry.nameHash) + + assertFalse(collection.contains("hello")) + assertNull(collection["hello"]) + + entry.remove() + assertEquals(emptyList(), collection.toList()) + } + + @Test + fun testSingleEntryRemoveById() { + val collection = TestCollection() + + val entry = collection.createOrGet(0) + assertEquals(0, entry.id) + assertEquals(-1, entry.nameHash) + + assertEquals(1, collection.size) + assertEquals(1, collection.capacity) + assertEquals(entry, collection[0]) + assertEquals(listOf(entry), collection.toList()) + + collection.remove(0) + + assertEquals(0, collection.size) + assertEquals(0, collection.capacity) + assertNull(collection[0]) + assertEquals(emptyList(), collection.toList()) + + entry.setName("hello") + assertEquals("hello".krHashCode(), entry.nameHash) + + assertFalse(collection.contains("hello")) + assertNull(collection["hello"]) + + collection.remove(0) + assertEquals(emptyList(), collection.toList()) + } + + @Test + fun testSingleEntryRemoveByName() { + val collection = TestCollection() + + val entry = collection.createOrGet("hello") + assertEquals(0, entry.id) + assertEquals("hello".krHashCode(), entry.nameHash) + + assertEquals(1, collection.size) + assertEquals(1, collection.capacity) + assertEquals(entry, collection[0]) + assertEquals(listOf(entry), collection.toList()) + + collection.remove("hello") + + assertEquals(0, collection.size) + assertEquals(0, collection.capacity) + assertNull(collection[0]) + assertEquals(emptyList(), collection.toList()) + + entry.setName("world") + assertEquals("world".krHashCode(), entry.nameHash) + + assertFalse(collection.contains("world")) + assertNull(collection["world"]) + + collection.remove("hello") + assertEquals(emptyList(), collection.toList()) + } + + @Test + fun testMultipleEntriesRemove() { + val collection = TestCollection() + + val entry0 = collection.createOrGet(0) + assertEquals(0, entry0.id) + assertEquals(-1, entry0.nameHash) + + val entry1 = collection.createOrGet(1) + assertEquals(1, entry1.id) + assertEquals(-1, entry1.nameHash) + + assertEquals(2, collection.size) + assertEquals(2, collection.capacity) + assertEquals(entry0, collection[0]) + assertEquals(entry1, collection[1]) + assertEquals(listOf(entry0, entry1), collection.toList()) + + entry0.remove() + + assertEquals(1, collection.size) + assertEquals(2, collection.capacity) + assertNull(collection[0]) + assertEquals(entry1, collection[1]) + assertEquals(listOf(entry1), collection.toList()) + + entry0.setName("world") + assertEquals("world".krHashCode(), entry0.nameHash) + + assertFalse(collection.contains("world")) + assertNull(collection["world"]) + + entry0.remove() + assertEquals(listOf(entry1), collection.toList()) + } + + @Test + fun testMultipleEntriesRemoveById() { + val collection = TestCollection() + + val entry0 = collection.createOrGet(0) + assertEquals(0, entry0.id) + assertEquals(-1, entry0.nameHash) + + val entry1 = collection.createOrGet(1) + assertEquals(1, entry1.id) + assertEquals(-1, entry1.nameHash) + + assertEquals(2, collection.size) + assertEquals(2, collection.capacity) + assertEquals(entry0, collection[0]) + assertEquals(entry1, collection[1]) + assertEquals(listOf(entry0, entry1), collection.toList()) + + collection.remove(0) + + assertEquals(1, collection.size) + assertEquals(2, collection.capacity) + assertNull(collection[0]) + assertEquals(entry1, collection[1]) + assertEquals(listOf(entry1), collection.toList()) + + entry0.setName("world") + assertEquals("world".krHashCode(), entry0.nameHash) + + assertFalse(collection.contains("world")) + assertNull(collection["world"]) + + collection.remove(0) + assertEquals(listOf(entry1), collection.toList()) + } + + @Test + fun testMultipleEntriesRemoveByName() { + val collection = TestCollection() + + val entry0 = collection.createOrGet("hello") + assertEquals(0, entry0.id) + assertEquals("hello".krHashCode(), entry0.nameHash) + + val entry1 = collection.createOrGet("world") + assertEquals(1, entry1.id) + assertEquals("world".krHashCode(), entry1.nameHash) + + val entry2 = collection.createOrGet("abc") + assertEquals(2, entry2.id) + assertEquals("abc".krHashCode(), entry2.nameHash) + + assertEquals(3, collection.size) + assertEquals(3, collection.capacity) + assertEquals(entry0, collection["hello"]) + assertEquals(entry1, collection["world"]) + assertEquals(entry2, collection["abc"]) + assertEquals(listOf(entry0, entry1, entry2), collection.toList()) + + collection.remove("hello") + + assertEquals(2, collection.size) + assertEquals(3, collection.capacity) + assertNull(collection["hello"]) + assertEquals(entry1, collection["world"]) + assertEquals(entry2, collection["abc"]) + assertEquals(listOf(entry1, entry2), collection.toList()) + + entry0.setName("!") + assertEquals("!".krHashCode(), entry0.nameHash) + + assertFalse(collection.contains("!")) + assertNull(collection["!"]) + + collection.remove("hello") + assertEquals(listOf(entry1, entry2), collection.toList()) + } + + @Test + fun testRemoveLastNamedEntry() { + val collection = TestCollection() + + val entry0 = collection.createOrGet("hello") + assertEquals(0, entry0.id) + assertEquals("hello".krHashCode(), entry0.nameHash) + + val entry1 = collection.createOrGet(1) + assertEquals(1, entry1.id) + assertEquals(-1, entry1.nameHash) + + val entry2 = collection.createOrGet(2) + assertEquals(2, entry2.id) + assertEquals(-1, entry2.nameHash) + + assertEquals(3, collection.size) + assertEquals(3, collection.capacity) + assertEquals(entry0, collection["hello"]) + assertEquals(entry1, collection[1]) + assertEquals(entry2, collection[2]) + assertEquals(listOf(entry0, entry1, entry2), collection.toList()) + + entry0.remove() + + assertNull(collection["hello"]) + assertEquals(entry1, collection[1]) + assertEquals(entry2, collection[2]) + assertEquals(listOf(entry1, entry2), collection.toList()) + } + + @Test + fun testNonContiguousIds() { + val collection = TestCollection() + + val entry1 = collection.createOrGet(1) + assertEquals(1, entry1.id) + assertEquals(-1, entry1.nameHash) + + val entry4 = collection.createOrGet(4) + assertEquals(4, entry4.id) + assertEquals(-1, entry4.nameHash) + + val entry3 = collection.createOrGet(3) + assertEquals(3, entry3.id) + assertEquals(-1, entry3.nameHash) + + assertEquals(3, collection.size) + assertEquals(5, collection.capacity) + assertEquals(listOf(entry1, entry3, entry4), collection.toList()) + + val entry0 = collection.createOrGet("hello") + assertEquals(0, entry0.id) + assertEquals("hello".krHashCode(), entry0.nameHash) + + val entry2 = collection.createOrGet("world") + assertEquals(2, entry2.id) + assertEquals("world".krHashCode(), entry2.nameHash) + + val entry5 = collection.createOrGet("!") + assertEquals(5, entry5.id) + assertEquals("!".krHashCode(), entry5.nameHash) + + assertEquals(6, collection.size) + assertEquals(6, collection.capacity) + assertEquals(listOf(entry0, entry1, entry2, entry3, entry4, entry5), collection.toList()) + } + + @Test + fun testNonContiguousIdsSingleEntry() { + val collection = TestCollection() + + val entry1 = collection.createOrGet(1) + assertEquals(1, entry1.id) + assertEquals(-1, entry1.nameHash) + + assertEquals(1, collection.size) + assertEquals(2, collection.capacity) + assertEquals(listOf(entry1), collection.toList()) + + val entry0 = collection.createOrGet("hello") + assertEquals(0, entry0.id) + assertEquals("hello".krHashCode(), entry0.nameHash) + + assertEquals(2, collection.size) + assertEquals(2, collection.capacity) + assertEquals(listOf(entry0, entry1), collection.toList()) + } + + @Test + fun testEmptyIterator() { + val collection = TestCollection() + + val it = collection.iterator() + + assertThrows { + it.remove() + } + + assertFalse(it.hasNext()) + + assertThrows { + it.next() + } + + assertThrows { + it.remove() + } + } + + @Test + fun testSingleEntryIterator() { + val collection = TestCollection() + + val entry = collection.createOrGet(0) + assertEquals(0, entry.id) + assertEquals(-1, entry.nameHash) + + val it = collection.iterator() + + assertThrows { + it.remove() + } + + assertTrue(it.hasNext()) + assertEquals(entry, it.next()) + + assertFalse(it.hasNext()) + + assertThrows { + it.next() + } + + assertThrows { + it.remove() + } + } + + @Test + fun testMultipleEntriesIterator() { + val collection = TestCollection() + + val entry0 = collection.createOrGet(0) + assertEquals(0, entry0.id) + assertEquals(-1, entry0.nameHash) + + val entry1 = collection.createOrGet(1) + assertEquals(1, entry1.id) + assertEquals(-1, entry1.nameHash) + + val it = collection.iterator() + + assertThrows { + it.remove() + } + + assertTrue(it.hasNext()) + assertEquals(entry0, it.next()) + + assertTrue(it.hasNext()) + assertEquals(entry1, it.next()) + + assertFalse(it.hasNext()) + + assertThrows { + it.next() + } + + assertThrows { + it.remove() + } + } + + @Test + fun testSingleEntryRemoveIterator() { + val collection = TestCollection() + + val entry = collection.createOrGet(0) + assertEquals(0, entry.id) + assertEquals(-1, entry.nameHash) + + val it = collection.iterator() + + assertTrue(it.hasNext()) + assertEquals(entry, it.next()) + + it.remove() + + assertThrows { + it.remove() + } + + assertFalse(it.hasNext()) + + assertEquals(0, collection.size) + assertEquals(0, collection.capacity) + assertEquals(emptyList(), collection.toList()) + } + + @Test + fun testMultipleEntriesRemoveIterator() { + val collection = TestCollection() + + val entry0 = collection.createOrGet(0) + assertEquals(0, entry0.id) + assertEquals(-1, entry0.nameHash) + + val entry1 = collection.createOrGet(1) + assertEquals(1, entry1.id) + assertEquals(-1, entry1.nameHash) + + val it = collection.iterator() + + assertTrue(it.hasNext()) + assertEquals(entry0, it.next()) + + it.remove() + + assertThrows { + it.remove() + } + + assertTrue(it.hasNext()) + assertEquals(entry1, it.next()) + + it.remove() + + assertThrows { + it.remove() + } + + assertFalse(it.hasNext()) + + assertEquals(0, collection.size) + assertEquals(0, collection.capacity) + assertEquals(emptyList(), collection.toList()) + } + + @Test + fun testNameHashCollision() { + val collection = TestCollection() + collection.createOrGet("hello") + + val entry = collection.createOrGet(1) + assertThrows { + entry.setName("hello") + } + } +} diff --git a/cache/src/test/resources/dev/openrs2/cache/index/all-flags.dat b/cache/src/test/resources/dev/openrs2/cache/index/all-flags.dat new file mode 100644 index 0000000000000000000000000000000000000000..d45cb17a08ef42e3f9f76c971676e97af808cfba GIT binary patch literal 102 zcmV-s0Ga;<4*&rG00rq7(g7nyXR4}IRT=tW*Ht2(7Ye;5<(USL%sm<(p&KHwdvf%tEGavj6}C=l}o*(21+f?*IV+ I00zMul3T$m1poj5 literal 0 HcmV?d00001 diff --git a/cache/src/test/resources/dev/openrs2/cache/index/digest.dat b/cache/src/test/resources/dev/openrs2/cache/index/digest.dat new file mode 100644 index 0000000000000000000000000000000000000000..145affe4c3bed3f15110ece541fa9817d45dfcff GIT binary patch literal 80 zcmV-W0I&ZA0ssL300ARKXBqlo*Ht2(7Ye;5<(USL%sm<(p&KHwdvf%tEGavx%$C?*IV%S|jTK literal 0 HcmV?d00001 diff --git a/cache/src/test/resources/dev/openrs2/cache/index/empty.dat b/cache/src/test/resources/dev/openrs2/cache/index/empty.dat new file mode 100644 index 0000000000000000000000000000000000000000..a786e127004dd9e94e88fda7742d248237ad8885 GIT binary patch literal 4 LcmZQ&U|;|M02lxU literal 0 HcmV?d00001 diff --git a/cache/src/test/resources/dev/openrs2/cache/index/lengths.dat b/cache/src/test/resources/dev/openrs2/cache/index/lengths.dat new file mode 100644 index 0000000000000000000000000000000000000000..0c2b464da4deebba6339a59cbe9e48cc4bbc79f5 GIT binary patch literal 24 fcmZQ&VPIrnU{rQZXJBA{!N9y@w_Y4dGDp3WE literal 0 HcmV?d00001 diff --git a/cache/src/test/resources/dev/openrs2/cache/index/named.dat b/cache/src/test/resources/dev/openrs2/cache/index/named.dat new file mode 100644 index 0000000000000000000000000000000000000000..122fcc079636cef5a983d9e5d8ba0e601adfccf9 GIT binary patch literal 26 fcmZQ&WME`qV0|fZiBZ`#y>s>1_aHvoLFq{VPLl@} literal 0 HcmV?d00001 diff --git a/cache/src/test/resources/dev/openrs2/cache/index/no-flags.dat b/cache/src/test/resources/dev/openrs2/cache/index/no-flags.dat new file mode 100644 index 0000000000000000000000000000000000000000..aa1d64f37679e83692d1a8b673c507a667531ef9 GIT binary patch literal 46 tcmZQ&U|?ooU|?imVpMib?_7QM{i;==p+FH3-~wV1pd14O6A%N{006C%1?~U< literal 0 HcmV?d00001 diff --git a/cache/src/test/resources/dev/openrs2/cache/index/null-digest.dat b/cache/src/test/resources/dev/openrs2/cache/index/null-digest.dat new file mode 100644 index 0000000000000000000000000000000000000000..e490dc6dcb77f4c66679bfd0f8b5f0115e32f3a9 GIT binary patch literal 80 acmZQ&Vqj!oU{rQZXP^*tu0H#ofdK$XmjlTF literal 0 HcmV?d00001 diff --git a/cache/src/test/resources/dev/openrs2/cache/index/smart.dat b/cache/src/test/resources/dev/openrs2/cache/index/smart.dat new file mode 100644 index 0000000000000000000000000000000000000000..562e7179e9f1c7e70bba47bc008a757fe95d4b8c GIT binary patch literal 40 ncmZQ)00Sllh6cvA1&qqB>8ncN literal 0 HcmV?d00001 diff --git a/cache/src/test/resources/dev/openrs2/cache/index/versioned.dat b/cache/src/test/resources/dev/openrs2/cache/index/versioned.dat new file mode 100644 index 0000000000000000000000000000000000000000..a8c24c5d92e730bbd43118188954e843531d41b4 GIT binary patch literal 8 PcmZP*G6}0-U|;|M1$+S; literal 0 HcmV?d00001