forked from openrs2/openrs2
parent
8bed0fc875
commit
f984f357d8
@ -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") |
||||
} |
||||
} |
||||
} |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Reference in new issue