Add Js5Index implementation

Signed-off-by: Graham <gpe@openrs2.dev>
Graham 4 years ago
parent 8bed0fc875
commit f984f357d8
  1. 1
      cache/build.gradle.kts
  2. 369
      cache/src/main/java/dev/openrs2/cache/Js5Index.kt
  3. 24
      cache/src/main/java/dev/openrs2/cache/Js5Protocol.kt
  4. 18
      cache/src/main/java/dev/openrs2/cache/NamedEntry.kt
  5. 362
      cache/src/main/java/dev/openrs2/cache/NamedEntryCollection.kt
  6. 415
      cache/src/test/java/dev/openrs2/cache/Js5IndexTest.kt
  7. 746
      cache/src/test/java/dev/openrs2/cache/NamedEntryCollectionTest.kt
  8. BIN
      cache/src/test/resources/dev/openrs2/cache/index/all-flags.dat
  9. BIN
      cache/src/test/resources/dev/openrs2/cache/index/digest.dat
  10. BIN
      cache/src/test/resources/dev/openrs2/cache/index/empty.dat
  11. BIN
      cache/src/test/resources/dev/openrs2/cache/index/lengths.dat
  12. BIN
      cache/src/test/resources/dev/openrs2/cache/index/named.dat
  13. BIN
      cache/src/test/resources/dev/openrs2/cache/index/no-flags.dat
  14. BIN
      cache/src/test/resources/dev/openrs2/cache/index/null-digest.dat
  15. BIN
      cache/src/test/resources/dev/openrs2/cache/index/smart.dat
  16. BIN
      cache/src/test/resources/dev/openrs2/cache/index/uncompressed-checksum.dat
  17. BIN
      cache/src/test/resources/dev/openrs2/cache/index/versioned.dat

@ -10,6 +10,7 @@ dependencies {
implementation(project(":compress")) implementation(project(":compress"))
implementation(project(":crypto")) implementation(project(":crypto"))
implementation(project(":util")) implementation(project(":util"))
implementation("it.unimi.dsi:fastutil:${Versions.fastutil}")
testImplementation("com.google.jimfs:jimfs:${Versions.jimfs}") testImplementation("com.google.jimfs:jimfs:${Versions.jimfs}")
} }

@ -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<Js5Index.Group>(::Group) {
public class Group internal constructor(
parent: NamedEntryCollection<Group>,
override val id: Int
) : NamedEntryCollection<File>(::File), NamedEntry {
private var parent: NamedEntryCollection<Group>? = 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<File>,
override val id: Int,
) : NamedEntry {
private var parent: NamedEntryCollection<File>? = 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
}
}
}

@ -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
}
}
}
}

@ -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()
}

@ -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<T : NamedEntry>(
private val entryConstructor: (NamedEntryCollection<T>, Int) -> T
) : MutableIterable<T> {
private var singleEntry: T? = null
private var entries: Int2ObjectAVLTreeMap<T>? = 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<T>()
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<T> {
val singleEntry = singleEntry
if (singleEntry != null) {
return object : MutableIterator<T> {
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<T> {
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<T> {
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
}
}

@ -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<IllegalArgumentException> {
Js5Index.read(buf)
}
}
Unpooled.wrappedBuffer(byteArrayOf(8)).use { buf ->
assertThrows<IllegalArgumentException> {
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<IllegalStateException> {
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())
}
}
}

@ -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<TestEntry>,
override val id: Int
) : NamedEntry {
private var parent: NamedEntryCollection<TestEntry>? = 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>(::TestEntry)
@Test
fun testBounds() {
val collection = TestCollection()
assertThrows<IllegalArgumentException> {
collection.contains(-1)
}
assertThrows<IllegalArgumentException> {
collection.containsNamed(-1)
}
assertThrows<IllegalArgumentException> {
collection[-1]
}
assertThrows<IllegalArgumentException> {
collection.getNamed(-1)
}
assertThrows<IllegalArgumentException> {
collection.createOrGet(-1)
}
assertThrows<IllegalArgumentException> {
collection.createOrGetNamed(-1)
}
assertThrows<IllegalArgumentException> {
collection.remove(-1)
}
assertThrows<IllegalArgumentException> {
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<IllegalStateException> {
it.remove()
}
assertFalse(it.hasNext())
assertThrows<NoSuchElementException> {
it.next()
}
assertThrows<IllegalStateException> {
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<IllegalStateException> {
it.remove()
}
assertTrue(it.hasNext())
assertEquals(entry, it.next())
assertFalse(it.hasNext())
assertThrows<NoSuchElementException> {
it.next()
}
assertThrows<IllegalStateException> {
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<IllegalStateException> {
it.remove()
}
assertTrue(it.hasNext())
assertEquals(entry0, it.next())
assertTrue(it.hasNext())
assertEquals(entry1, it.next())
assertFalse(it.hasNext())
assertThrows<NoSuchElementException> {
it.next()
}
assertThrows<IllegalStateException> {
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<IllegalStateException> {
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<IllegalStateException> {
it.remove()
}
assertTrue(it.hasNext())
assertEquals(entry1, it.next())
it.remove()
assertThrows<IllegalStateException> {
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<IllegalStateException> {
entry.setName("hello")
}
}
}
Loading…
Cancel
Save