Compare commits

..

No commits in common. 'master' and 'master' have entirely different histories.

  1. 30
      .drone.yml
  2. 13
      .editorconfig
  3. 34
      .github/workflows/build.yaml
  4. 1
      .gitignore
  5. 6
      README.md
  6. 8
      all/build.gradle.kts
  7. 4
      archive/build.gradle.kts
  8. 2
      archive/src/main/kotlin/org/openrs2/archive/ArchiveCommand.kt
  9. 4
      archive/src/main/kotlin/org/openrs2/archive/ArchiveConfigProvider.kt
  10. 2
      archive/src/main/kotlin/org/openrs2/archive/ArchiveModule.kt
  11. 4
      archive/src/main/kotlin/org/openrs2/archive/DataSourceProvider.kt
  12. 4
      archive/src/main/kotlin/org/openrs2/archive/DatabaseProvider.kt
  13. 73
      archive/src/main/kotlin/org/openrs2/archive/cache/CacheDownloader.kt
  14. 10
      archive/src/main/kotlin/org/openrs2/archive/cache/CacheExporter.kt
  15. 27
      archive/src/main/kotlin/org/openrs2/archive/cache/CacheImporter.kt
  16. 6
      archive/src/main/kotlin/org/openrs2/archive/cache/CrossPollinator.kt
  17. 17
      archive/src/main/kotlin/org/openrs2/archive/cache/Js5ChannelHandler.kt
  18. 11
      archive/src/main/kotlin/org/openrs2/archive/client/Architecture.kt
  19. 35
      archive/src/main/kotlin/org/openrs2/archive/client/Artifact.kt
  20. 46
      archive/src/main/kotlin/org/openrs2/archive/client/ArtifactFormat.kt
  21. 16
      archive/src/main/kotlin/org/openrs2/archive/client/ArtifactType.kt
  22. 14
      archive/src/main/kotlin/org/openrs2/archive/client/ClientCommand.kt
  23. 455
      archive/src/main/kotlin/org/openrs2/archive/client/ClientExporter.kt
  24. 997
      archive/src/main/kotlin/org/openrs2/archive/client/ClientImporter.kt
  25. 30
      archive/src/main/kotlin/org/openrs2/archive/client/ExportCommand.kt
  26. 32
      archive/src/main/kotlin/org/openrs2/archive/client/ImportCommand.kt
  27. 7
      archive/src/main/kotlin/org/openrs2/archive/client/Jvm.kt
  28. 116
      archive/src/main/kotlin/org/openrs2/archive/client/MachO.kt
  29. 43
      archive/src/main/kotlin/org/openrs2/archive/client/OperatingSystem.kt
  30. 16
      archive/src/main/kotlin/org/openrs2/archive/client/RefreshCommand.kt
  31. 4
      archive/src/main/kotlin/org/openrs2/archive/game/GameDatabase.kt
  32. 20
      archive/src/main/kotlin/org/openrs2/archive/key/BinaryKeyReader.kt
  33. 12
      archive/src/main/kotlin/org/openrs2/archive/key/HdosKeyDownloader.kt
  34. 6
      archive/src/main/kotlin/org/openrs2/archive/key/HexKeyReader.kt
  35. 4
      archive/src/main/kotlin/org/openrs2/archive/key/JsonKeyDownloader.kt
  36. 14
      archive/src/main/kotlin/org/openrs2/archive/key/JsonKeyReader.kt
  37. 14
      archive/src/main/kotlin/org/openrs2/archive/key/KeyBruteForcer.kt
  38. 4
      archive/src/main/kotlin/org/openrs2/archive/key/KeyDownloader.kt
  39. 16
      archive/src/main/kotlin/org/openrs2/archive/key/KeyExporter.kt
  40. 14
      archive/src/main/kotlin/org/openrs2/archive/key/KeyImporter.kt
  41. 4
      archive/src/main/kotlin/org/openrs2/archive/key/KeyReader.kt
  42. 4
      archive/src/main/kotlin/org/openrs2/archive/key/RuneLiteKeyDownloader.kt
  43. 6
      archive/src/main/kotlin/org/openrs2/archive/key/TextKeyReader.kt
  44. 2
      archive/src/main/kotlin/org/openrs2/archive/map/MapRenderer.kt
  45. 4
      archive/src/main/kotlin/org/openrs2/archive/name/NameImporter.kt
  46. 4
      archive/src/main/kotlin/org/openrs2/archive/name/RuneStarNameDownloader.kt
  47. 4
      archive/src/main/kotlin/org/openrs2/archive/web/CachesController.kt
  48. 82
      archive/src/main/kotlin/org/openrs2/archive/web/ClientsController.kt
  49. 8
      archive/src/main/kotlin/org/openrs2/archive/web/KeysController.kt
  50. 13
      archive/src/main/kotlin/org/openrs2/archive/web/WebServer.kt
  51. 31
      archive/src/main/kotlin/org/openrs2/archive/world/World.kt
  52. 22
      archive/src/main/kotlin/org/openrs2/archive/world/WorldList.kt
  53. 2
      archive/src/main/resources/org/openrs2/archive/migrations/V10__variants.sql
  54. 3
      archive/src/main/resources/org/openrs2/archive/migrations/V21__manual_source.sql
  55. 95
      archive/src/main/resources/org/openrs2/archive/migrations/V22__clients.sql
  56. 11
      archive/src/main/resources/org/openrs2/archive/migrations/V23__client_sources.sql
  57. 7
      archive/src/main/resources/org/openrs2/archive/migrations/V24__loginapplet_passapplet.sql
  58. 5
      archive/src/main/resources/org/openrs2/archive/migrations/V25__client_source_file_metadata.sql
  59. 63
      archive/src/main/resources/org/openrs2/archive/templates/clients/index.html
  60. 152
      archive/src/main/resources/org/openrs2/archive/templates/clients/show.html
  61. 8
      archive/src/main/resources/org/openrs2/archive/templates/index.html
  62. 3
      archive/src/main/resources/org/openrs2/archive/templates/layout.html
  63. 1
      asm/build.gradle.kts
  64. 2
      asm/src/main/kotlin/org/openrs2/asm/AsmJacksonModule.kt
  65. 32
      asm/src/main/kotlin/org/openrs2/asm/InsnListUtils.kt
  66. 9
      asm/src/main/kotlin/org/openrs2/asm/classpath/Library.kt
  67. 45
      asm/src/main/kotlin/org/openrs2/asm/io/CabLibraryReader.kt
  68. 4
      asm/src/main/kotlin/org/openrs2/asm/io/PackClassLibraryReader.kt
  69. 4
      asm/src/main/kotlin/org/openrs2/asm/io/PackClassLibraryWriter.kt
  70. 2
      asm/src/main/kotlin/org/openrs2/asm/transform/Transformer.kt
  71. 4
      buffer/src/main/kotlin/org/openrs2/buffer/ByteBufBodyHandler.kt
  72. 5
      build.gradle.kts
  73. 4
      cache-550/src/main/kotlin/org/openrs2/cache/config/enum/EnumTypeList.kt
  74. 4
      cache-550/src/main/kotlin/org/openrs2/cache/config/inv/InvTypeList.kt
  75. 4
      cache-550/src/main/kotlin/org/openrs2/cache/config/param/ParamTypeList.kt
  76. 4
      cache-550/src/main/kotlin/org/openrs2/cache/config/struct/StructTypeList.kt
  77. 4
      cache-550/src/main/kotlin/org/openrs2/cache/config/varbit/VarbitTypeList.kt
  78. 4
      cache-550/src/main/kotlin/org/openrs2/cache/config/varp/VarpTypeList.kt
  79. 38
      cache/src/main/kotlin/org/openrs2/cache/Archive.kt
  80. 48
      cache/src/main/kotlin/org/openrs2/cache/Cache.kt
  81. 24
      cache/src/main/kotlin/org/openrs2/cache/Js5Compression.kt
  82. 2
      cache/src/main/kotlin/org/openrs2/cache/Js5MasterIndex.kt
  83. 4
      cache/src/main/kotlin/org/openrs2/cache/RuneLiteStore.kt
  84. 62
      cache/src/test/kotlin/org/openrs2/cache/Js5CompressionTest.kt
  85. 6
      cache/src/test/kotlin/org/openrs2/cache/Js5MasterIndexTest.kt
  86. BIN
      cache/src/test/resources/org/openrs2/cache/master-index/lengths/255/0.dat
  87. 4
      compress-cli/src/main/kotlin/org/openrs2/compress/cli/pack200/Unpack200Command.kt
  88. 3
      conf/src/main/kotlin/org/openrs2/conf/Config.kt
  89. 4
      conf/src/main/kotlin/org/openrs2/conf/ConfigProvider.kt
  90. 2
      conf/src/main/kotlin/org/openrs2/conf/RsaKeyProvider.kt
  91. 6
      crypto/src/main/kotlin/org/openrs2/crypto/CryptoJacksonModule.kt
  92. 19
      crypto/src/main/kotlin/org/openrs2/crypto/Sha1.kt
  93. 69
      crypto/src/main/kotlin/org/openrs2/crypto/SymmetricKey.kt
  94. 2
      crypto/src/main/kotlin/org/openrs2/crypto/Whirlpool.kt
  95. 71
      crypto/src/main/kotlin/org/openrs2/crypto/Xtea.kt
  96. 6
      crypto/src/main/kotlin/org/openrs2/crypto/XteaKeyDeserializer.kt
  97. 4
      crypto/src/main/kotlin/org/openrs2/crypto/XteaKeySerializer.kt
  98. 2
      crypto/src/test/kotlin/org/openrs2/crypto/XteaTest.kt
  99. 2
      decompiler/src/main/kotlin/org/openrs2/decompiler/Decompiler.kt
  100. 4
      deob-ast/src/main/kotlin/org/openrs2/deob/ast/AstDeobfuscator.kt
  101. Some files were not shown because too many files have changed in this diff Show More

@ -0,0 +1,30 @@
kind: pipeline
type: docker
name: default
steps:
- name: build
image: registry.openrs2.org/openrs2-dev
commands:
- ./gradlew --no-daemon clean build
- name: deploy
image: registry.openrs2.org/openrs2-dev
commands:
- install -dm0700 $${HOME}/.ssh
- echo -n "$${SSH_KEY}" > $${HOME}/.ssh/id_ed25519
- chmod 0600 $${HOME}/.ssh/id_ed25519
- ./gradlew --no-daemon publish
environment:
ORG_GRADLE_PROJECT_openrs2Username:
from_secret: repo_username
ORG_GRADLE_PROJECT_openrs2Password:
from_secret: repo_password
SSH_KEY:
from_secret: ssh_key
when:
branch:
- master
event:
exclude:
- pull_request

@ -13,18 +13,9 @@ indent_style = tab
# @formatter:on
indent_style = space
indent_size = 4
# see https://github.com/pinterest/ktlint/issues/764
# noinspection EditorConfigKeyCorrectness
ktlint_standard_argument-list-wrapping = disabled
# noinspection EditorConfigKeyCorrectness
ktlint_standard_indent = disabled
# noinspection EditorConfigKeyCorrectness
ktlint_standard_parameter-list-wrapping = disabled
# noinspection EditorConfigKeyCorrectness
ktlint_standard_trailing-comma-on-call-site = disabled
# noinspection EditorConfigKeyCorrectness
ktlint_standard_trailing-comma-on-declaration-site = disabled
# noinspection EditorConfigKeyCorrectness
ktlint_standard_wrapping = disabled
disabled_rules = argument-list-wrapping, parameter-list-wrapping, wrapping
[*.md]
max_line_length = 80

@ -1,34 +0,0 @@
---
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/cache@v3
with:
path: ~/.ssh/known_hosts
key: ssh-known-hosts
- uses: actions/checkout@v3
- uses: actions/setup-java@v3
with:
distribution: temurin
java-version: 11
- uses: gradle/wrapper-validation-action@v1
- uses: gradle/gradle-build-action@v2
with:
arguments: build
- if: github.ref == 'refs/heads/master'
run: |
install -dm0700 ~/.ssh
touch ~/.ssh/id_ed25519
chmod 0600 ~/.ssh/id_ed25519
echo "${SSH_KEY}" > ~/.ssh/id_ed25519
env:
SSH_KEY: ${{ secrets.SSH_KEY }}
- if: github.ref == 'refs/heads/master'
uses: gradle/gradle-build-action@v2
with:
arguments: publish
env:
ORG_GRADLE_PROJECT_openrs2Username: ${{ secrets.REPO_USERNAME }}
ORG_GRADLE_PROJECT_openrs2Password: ${{ secrets.REPO_PASSWORD }}

1
.gitignore vendored

@ -1,4 +1,5 @@
.*
!.drone.yml
!.editorconfig
!.git*
!.idea

@ -1,6 +1,6 @@
# OpenRS2
[![GitHub Actions][actions-badge]][actions] [![Discord][discord-badge]][discord] [![ISC license][isc-badge]][isc]
[![Drone][drone-badge]][drone] [![Discord][discord-badge]][discord] [![ISC license][isc-badge]][isc]
## Introduction
@ -76,10 +76,10 @@ OpenRS2 is available under the terms of the [ISC license][isc], which is similar
to the 2-clause BSD license. The full copyright notice and terms are available
in the `LICENSE` file.
[actions-badge]: https://github.com/openrs2/openrs2/actions/workflows/build.yaml/badge.svg?branch=master
[actions]: https://github.com/openrs2/openrs2/actions
[discord-badge]: https://img.shields.io/discord/684495254145335298
[discord]: https://chat.openrs2.org/
[drone-badge]: https://build.openrs2.org/api/badges/openrs2/openrs2/status.svg
[drone]: https://build.openrs2.org/openrs2/openrs2/
[isc-badge]: https://img.shields.io/badge/license-ISC-informational
[isc]: https://opensource.org/licenses/ISC
[issue-tracker]: https://git.openrs2.org/openrs2/openrs2/issues

@ -39,10 +39,10 @@ tasks.shadowJar {
tasks.register("generateAuthors") {
inputs.dir("$rootDir/.git")
outputs.file(layout.buildDirectory.file("AUTHORS"))
outputs.file("$buildDir/AUTHORS")
doLast {
Files.newOutputStream(layout.buildDirectory.file("AUTHORS").get().asFile.toPath()).use { out ->
Files.newOutputStream(buildDir.toPath().resolve("AUTHORS")).use { out ->
exec {
commandLine("git", "shortlog", "-esn", "HEAD")
standardOutput = out
@ -80,7 +80,7 @@ distributions {
distributionBaseName.set("openrs2")
contents {
from(layout.buildDirectory.file("AUTHORS"))
from("$buildDir/AUTHORS")
from("$rootDir/CONTRIBUTING.md")
from("$rootDir/DCO")
from("$rootDir/LICENSE")
@ -97,7 +97,7 @@ distributions {
exclude(".*", "*~")
into("share")
}
from(layout.buildDirectory.file("reports/dependency-license/THIRD-PARTY-NOTICES.txt")) {
from("$buildDir/reports/dependency-license/THIRD-PARTY-NOTICES.txt") {
rename { "third-party-licenses.txt" }
into("share/doc")
}

@ -12,7 +12,6 @@ dependencies {
api(libs.bundles.guice)
api(libs.clikt)
implementation(projects.asm)
implementation(projects.buffer)
implementation(projects.cache550)
implementation(projects.cli)
@ -31,18 +30,15 @@ dependencies {
implementation(libs.bundles.ktor)
implementation(libs.bundles.thymeleaf)
implementation(libs.byteUnits)
implementation(libs.cabParser)
implementation(libs.flyway)
implementation(libs.guava)
implementation(libs.hikaricp)
implementation(libs.jackson.jsr310)
implementation(libs.jdom)
implementation(libs.jelf)
implementation(libs.jquery)
implementation(libs.jsoup)
implementation(libs.kotlin.coroutines.core)
implementation(libs.netty.handler)
implementation(libs.pecoff4j)
implementation(libs.postgres)
}

@ -3,7 +3,6 @@ package org.openrs2.archive
import com.github.ajalt.clikt.core.NoOpCliktCommand
import com.github.ajalt.clikt.core.subcommands
import org.openrs2.archive.cache.CacheCommand
import org.openrs2.archive.client.ClientCommand
import org.openrs2.archive.key.KeyCommand
import org.openrs2.archive.name.NameCommand
import org.openrs2.archive.web.WebCommand
@ -14,7 +13,6 @@ public class ArchiveCommand : NoOpCliktCommand(name = "archive") {
init {
subcommands(
CacheCommand(),
ClientCommand(),
KeyCommand(),
NameCommand(),
WebCommand()

@ -1,11 +1,11 @@
package org.openrs2.archive
import com.fasterxml.jackson.databind.ObjectMapper
import jakarta.inject.Inject
import jakarta.inject.Provider
import org.openrs2.yaml.Yaml
import java.nio.file.Files
import java.nio.file.Path
import javax.inject.Inject
import javax.inject.Provider
public class ArchiveConfigProvider @Inject constructor(
@Yaml private val mapper: ObjectMapper

@ -10,7 +10,6 @@ import org.openrs2.archive.key.KeyDownloader
import org.openrs2.archive.key.RuneLiteKeyDownloader
import org.openrs2.archive.name.NameDownloader
import org.openrs2.archive.name.RuneStarNameDownloader
import org.openrs2.asm.AsmModule
import org.openrs2.buffer.BufferModule
import org.openrs2.cache.CacheModule
import org.openrs2.db.Database
@ -22,7 +21,6 @@ import javax.sql.DataSource
public object ArchiveModule : AbstractModule() {
override fun configure() {
install(AsmModule)
install(BufferModule)
install(CacheModule)
install(HttpModule)

@ -2,10 +2,10 @@ package org.openrs2.archive
import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource
import jakarta.inject.Inject
import jakarta.inject.Provider
import org.flywaydb.core.Flyway
import org.postgresql.ds.PGSimpleDataSource
import javax.inject.Inject
import javax.inject.Provider
import javax.sql.DataSource
public class DataSourceProvider @Inject constructor(

@ -1,9 +1,9 @@
package org.openrs2.archive
import jakarta.inject.Inject
import jakarta.inject.Provider
import org.openrs2.db.Database
import org.openrs2.db.PostgresDeadlockDetector
import javax.inject.Inject
import javax.inject.Provider
import javax.sql.DataSource
public class DatabaseProvider @Inject constructor(

@ -1,20 +1,15 @@
package org.openrs2.archive.cache
import jakarta.inject.Inject
import jakarta.inject.Singleton
import org.openrs2.archive.cache.nxt.MusicStreamClient
import org.openrs2.archive.game.GameDatabase
import org.openrs2.archive.jav.JavConfig
import org.openrs2.archive.world.World
import org.openrs2.archive.world.WorldList
import org.openrs2.buffer.ByteBufBodyHandler
import org.openrs2.buffer.use
import org.openrs2.net.BootstrapFactory
import org.openrs2.net.awaitSuspend
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import kotlin.coroutines.resumeWithException
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.coroutines.suspendCoroutine
@Singleton
@ -27,30 +22,23 @@ public class CacheDownloader @Inject constructor(
) {
public suspend fun download(gameName: String, environment: String, language: String) {
val game = gameDatabase.getGame(gameName, environment, language) ?: throw Exception("Game not found")
val url = game.url ?: throw Exception("URL not set")
val buildMajor = game.buildMajor ?: throw Exception("Current major build not set")
val config = JavConfig.download(client, url)
val group = bootstrapFactory.createEventLoopGroup()
try {
suspendCoroutine { continuation ->
val bootstrap = bootstrapFactory.createBootstrap(group)
val hostname: String
val initializer = when (gameName) {
"oldschool" -> {
var buildMajor = game.buildMajor
hostname = if (environment == "beta") {
findOsrsWorld(config, World::isBeta) ?: throw Exception("Failed to find beta world")
} else {
val codebase = config.config[CODEBASE] ?: throw Exception("Codebase missing")
URI(codebase).host ?: throw Exception("Hostname missing")
}
val serverVersion = config.params[OSRS_SERVER_VERSION]
if (serverVersion != null) {
buildMajor = serverVersion.toInt()
}
val codebase = config.config[CODEBASE] ?: throw Exception("Codebase missing")
hostname = URI(codebase).host ?: throw Exception("Hostname missing")
OsrsJs5ChannelInitializer(
OsrsJs5ChannelHandler(
@ -59,7 +47,7 @@ public class CacheDownloader @Inject constructor(
game.id,
hostname,
PORT,
buildMajor ?: throw Exception("Current major build not set"),
buildMajor,
game.lastMasterIndexId,
continuation,
importer
@ -68,22 +56,7 @@ public class CacheDownloader @Inject constructor(
}
"runescape" -> {
var buildMajor = game.buildMajor
var buildMinor = game.buildMinor
val serverVersion = config.config[NXT_SERVER_VERSION]
if (serverVersion != null) {
val n = serverVersion.toInt()
/*
* Only reset buildMinor if buildMajor changes, so
* we don't have to keep retrying minor versions.
*/
if (buildMajor != n) {
buildMajor = n
buildMinor = 1
}
}
val buildMinor = game.buildMinor ?: throw Exception("Current minor build not set")
val tokens = config.params.values.filter { TOKEN_REGEX.matches(it) }
val token = tokens.singleOrNull() ?: throw Exception("Multiple candidate tokens: $tokens")
@ -103,8 +76,8 @@ public class CacheDownloader @Inject constructor(
game.id,
hostname,
PORT,
buildMajor ?: throw Exception("Current major build not set"),
buildMinor ?: throw Exception("Current minor build not set"),
buildMajor,
buildMinor,
game.lastMasterIndexId,
continuation,
importer,
@ -120,36 +93,14 @@ public class CacheDownloader @Inject constructor(
bootstrap.handler(initializer)
.connect(hostname, PORT)
.addListener { future ->
if (!future.isSuccess) {
continuation.resumeWithException(future.cause())
}
}
}
} finally {
group.shutdownGracefully().awaitSuspend()
}
}
private fun findOsrsWorld(config: JavConfig, predicate: (World) -> Boolean): String? {
val url = config.params[OSRS_WORLD_LIST_URL] ?: throw Exception("World list URL missing")
val list = client.send(HttpRequest.newBuilder(URI(url)).build(), byteBufBodyHandler).body().use { buf ->
WorldList.read(buf)
}
return list.worlds
.filter(predicate)
.map(World::hostname)
.shuffled()
.firstOrNull()
}
private companion object {
private const val CODEBASE = "codebase"
private const val OSRS_WORLD_LIST_URL = "17"
private const val OSRS_SERVER_VERSION = "25"
private const val NXT_SERVER_VERSION = "server_version"
private const val NXT_LIVE_HOSTNAME = "content.runescape.com"
private const val NXT_BETA_HOSTNAME = "content.beta.runescape.com"
private const val PORT = 443

@ -6,8 +6,6 @@ import com.fasterxml.jackson.annotation.JsonUnwrapped
import io.netty.buffer.ByteBuf
import io.netty.buffer.ByteBufAllocator
import io.netty.buffer.Unpooled
import jakarta.inject.Inject
import jakarta.inject.Singleton
import org.openrs2.buffer.use
import org.openrs2.cache.ChecksumTable
import org.openrs2.cache.DiskStore
@ -16,7 +14,7 @@ import org.openrs2.cache.Js5Compression
import org.openrs2.cache.Js5MasterIndex
import org.openrs2.cache.MasterIndexFormat
import org.openrs2.cache.Store
import org.openrs2.crypto.SymmetricKey
import org.openrs2.crypto.XteaKey
import org.openrs2.db.Database
import org.postgresql.util.PGobject
import java.sql.Connection
@ -24,6 +22,8 @@ import java.time.Instant
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.util.SortedSet
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
public class CacheExporter @Inject constructor(
@ -196,7 +196,7 @@ public class CacheExporter @Inject constructor(
val nameHash: Int?,
val name: String?,
@JsonProperty("mapsquare") val mapSquare: Int?,
val key: SymmetricKey
val key: XteaKey
)
public suspend fun totalSize(): Long {
@ -779,7 +779,7 @@ public class CacheExporter @Inject constructor(
val k3 = rows.getInt(8)
val mapSquare = getMapSquare(name)
keys += Key(archive, group, nameHash, name, mapSquare, SymmetricKey(k0, k1, k2, k3))
keys += Key(archive, group, nameHash, name, mapSquare, XteaKey(k0, k1, k2, k3))
}
keys

@ -6,8 +6,6 @@ import io.netty.buffer.ByteBufAllocator
import io.netty.buffer.ByteBufUtil
import io.netty.buffer.DefaultByteBufHolder
import io.netty.buffer.Unpooled
import jakarta.inject.Inject
import jakarta.inject.Singleton
import org.openrs2.buffer.crc32
import org.openrs2.buffer.use
import org.openrs2.cache.ChecksumTable
@ -24,8 +22,6 @@ import org.openrs2.cache.StoreCorruptException
import org.openrs2.cache.VersionList
import org.openrs2.cache.VersionTrailer
import org.openrs2.crypto.Whirlpool
import org.openrs2.crypto.sha1
import org.openrs2.crypto.whirlpool
import org.openrs2.db.Database
import org.postgresql.util.PSQLState
import java.io.IOException
@ -34,6 +30,8 @@ import java.sql.SQLException
import java.sql.Types
import java.time.Instant
import java.time.ZoneOffset
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
public class CacheImporter @Inject constructor(
@ -86,8 +84,7 @@ public class CacheImporter @Inject constructor(
) : DefaultByteBufHolder(buf) {
public val bytes: ByteArray = ByteBufUtil.getBytes(buf, buf.readerIndex(), buf.readableBytes(), false)
public val crc32: Int = buf.crc32()
public val sha1: ByteArray = buf.sha1()
public val whirlpool: ByteArray = buf.whirlpool()
public val whirlpool: ByteArray = Whirlpool.whirlpool(bytes)
}
public class ChecksumTableBlob(
@ -857,7 +854,6 @@ public class CacheImporter @Inject constructor(
CREATE TEMPORARY TABLE tmp_blobs (
index INTEGER NOT NULL,
crc32 INTEGER NOT NULL,
sha1 BYTEA NOT NULL,
whirlpool BYTEA NOT NULL,
data BYTEA NOT NULL
) ON COMMIT DROP
@ -996,11 +992,11 @@ public class CacheImporter @Inject constructor(
return ids as List<Long>
}
public fun addBlob(connection: Connection, blob: Blob): Long {
private fun addBlob(connection: Connection, blob: Blob): Long {
return addBlobs(connection, listOf(blob)).single()
}
public fun addBlobs(connection: Connection, blobs: List<Blob>): List<Long> {
private fun addBlobs(connection: Connection, blobs: List<Blob>): List<Long> {
connection.prepareStatement(
"""
TRUNCATE TABLE tmp_blobs
@ -1011,16 +1007,15 @@ public class CacheImporter @Inject constructor(
connection.prepareStatement(
"""
INSERT INTO tmp_blobs (index, crc32, sha1, whirlpool, data)
VALUES (?, ?, ?, ?, ?)
INSERT INTO tmp_blobs (index, crc32, whirlpool, data)
VALUES (?, ?, ?, ?)
""".trimIndent()
).use { stmt ->
for ((i, blob) in blobs.withIndex()) {
stmt.setInt(1, i)
stmt.setInt(2, blob.crc32)
stmt.setBytes(3, blob.sha1)
stmt.setBytes(4, blob.whirlpool)
stmt.setBytes(5, blob.bytes)
stmt.setBytes(3, blob.whirlpool)
stmt.setBytes(4, blob.bytes)
stmt.addBatch()
}
@ -1030,8 +1025,8 @@ public class CacheImporter @Inject constructor(
connection.prepareStatement(
"""
INSERT INTO blobs (crc32, sha1, whirlpool, data)
SELECT t.crc32, t.sha1, t.whirlpool, t.data
INSERT INTO blobs (crc32, whirlpool, data)
SELECT t.crc32, t.whirlpool, t.data
FROM tmp_blobs t
LEFT JOIN blobs b ON b.whirlpool = t.whirlpool
WHERE b.whirlpool IS NULL

@ -4,8 +4,6 @@ import io.netty.buffer.ByteBuf
import io.netty.buffer.ByteBufAllocator
import io.netty.buffer.ByteBufInputStream
import io.netty.buffer.Unpooled
import jakarta.inject.Inject
import jakarta.inject.Singleton
import org.openrs2.buffer.crc32
import org.openrs2.buffer.use
import org.openrs2.cache.Js5Compression
@ -13,6 +11,8 @@ import org.openrs2.cache.Js5CompressionType
import org.openrs2.db.Database
import java.sql.Connection
import java.util.zip.GZIPInputStream
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
public class CrossPollinator @Inject constructor(
@ -23,7 +23,7 @@ public class CrossPollinator @Inject constructor(
public suspend fun crossPollinate() {
database.execute { connection ->
for ((index, archive) in OLD_TO_NEW_ENGINE) {
crossPollinate(connection, index, archive)
crossPollinate(connection, index, archive);
}
}
}

@ -3,11 +3,11 @@ package org.openrs2.archive.cache
import com.github.michaelbull.logging.InlineLogger
import io.netty.bootstrap.Bootstrap
import io.netty.buffer.ByteBuf
import io.netty.channel.ChannelException
import io.netty.channel.ChannelHandler
import io.netty.channel.ChannelHandlerContext
import io.netty.channel.ChannelPipeline
import io.netty.channel.SimpleChannelInboundHandler
import io.netty.handler.timeout.ReadTimeoutException
import kotlinx.coroutines.runBlocking
import org.openrs2.buffer.crc32
import org.openrs2.buffer.use
@ -16,7 +16,6 @@ import org.openrs2.cache.Js5Compression
import org.openrs2.cache.Js5Index
import org.openrs2.cache.Js5MasterIndex
import org.openrs2.cache.MasterIndexFormat
import java.io.IOException
import java.nio.channels.ClosedChannelException
import java.time.Instant
import kotlin.coroutines.Continuation
@ -159,11 +158,11 @@ public abstract class Js5ChannelHandler(
if (state == State.RESUMING_CONTINUATION) {
logger.warn(cause) { "Swallowing exception as continuation has already resumed" }
} else if (cause !is ChannelException && cause !is IOException) {
} else if (cause != ReadTimeoutException.INSTANCE) {
/*
* We skip continuation resumption if there's an I/O error or
* timeout - this allows channelInactive() to attempt to reconnect
* if we haven't used too many reconnection attempts.
* We skip continuation resumption if there's a read timeout - this
* allows channelInactive() to attempt to reconnect if we haven't
* used too many reconnection attempts.
*/
state = State.RESUMING_CONTINUATION
continuation.resumeWithException(cause)
@ -254,12 +253,6 @@ public abstract class Js5ChannelHandler(
continuation.resume(Unit)
ctx.close()
} else {
/*
* Reset the number of reconnection attempts as we are making
* progress.
*/
reconnectionAttempts = 0
}
}

@ -1,11 +0,0 @@
package org.openrs2.archive.client
public enum class Architecture {
INDEPENDENT,
UNIVERSAL,
X86,
AMD64,
POWERPC,
SPARC,
SPARCV9
}

@ -1,35 +0,0 @@
package org.openrs2.archive.client
import io.netty.buffer.ByteBuf
import io.netty.buffer.ByteBufUtil
import org.openrs2.archive.cache.CacheExporter
import org.openrs2.archive.cache.CacheImporter
import java.time.Instant
public class Artifact(
data: ByteBuf,
public val game: String,
public val environment: String,
public val build: CacheExporter.Build?,
public val timestamp: Instant?,
public val type: ArtifactType,
public val format: ArtifactFormat,
public val os: OperatingSystem,
public val arch: Architecture,
public val jvm: Jvm,
public val links: List<ArtifactLink>
) : CacheImporter.Blob(data)
public data class ArtifactLink(
val type: ArtifactType,
val format: ArtifactFormat,
val os: OperatingSystem,
val arch: Architecture,
val jvm: Jvm,
val crc32: Int?,
val sha1: ByteArray,
val size: Int?
) {
public val sha1Hex: String
get() = ByteBufUtil.hexDump(sha1)
}

@ -1,46 +0,0 @@
package org.openrs2.archive.client
import io.ktor.http.ContentType
public enum class ArtifactFormat {
CAB,
JAR,
NATIVE,
PACK200,
PACKCLASS;
public fun getPrefix(os: OperatingSystem): String {
return when (this) {
NATIVE -> os.getPrefix()
else -> ""
}
}
public fun getExtension(os: OperatingSystem): String {
return when (this) {
CAB -> "cab"
JAR -> "jar"
NATIVE -> os.getExtension()
PACK200 -> "pack200"
PACKCLASS -> "js5"
}
}
public fun getContentType(os: OperatingSystem): ContentType {
return when (this) {
CAB -> CAB_MIME_TYPE
JAR -> JAR_MIME_TYPE
NATIVE -> os.getContentType()
PACK200, PACKCLASS -> ContentType.Application.OctetStream
}
}
public fun isJar(): Boolean {
return this != NATIVE
}
private companion object {
private val CAB_MIME_TYPE = ContentType("application", "vnd.ms-cab-compressed")
private val JAR_MIME_TYPE = ContentType("application", "java-archive")
}
}

@ -1,16 +0,0 @@
package org.openrs2.archive.client
public enum class ArtifactType {
BROWSERCONTROL,
CLIENT,
CLIENT_GL,
GLUEGEN_RT,
JAGGL,
JAGGL_DRI,
JAGMISC,
JOGL,
JOGL_AWT,
LOADER,
LOADER_GL,
UNPACKCLASS
}

@ -1,14 +0,0 @@
package org.openrs2.archive.client
import com.github.ajalt.clikt.core.NoOpCliktCommand
import com.github.ajalt.clikt.core.subcommands
public class ClientCommand : NoOpCliktCommand(name = "client") {
init {
subcommands(
ExportCommand(),
ImportCommand(),
RefreshCommand()
)
}
}

@ -1,455 +0,0 @@
package org.openrs2.archive.client
import io.netty.buffer.ByteBuf
import io.netty.buffer.ByteBufUtil
import io.netty.buffer.DefaultByteBufHolder
import io.netty.buffer.Unpooled
import jakarta.inject.Inject
import jakarta.inject.Singleton
import org.openrs2.archive.cache.CacheExporter
import org.openrs2.db.Database
import java.time.Instant
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
@Singleton
public class ClientExporter @Inject constructor(
private val database: Database
) {
public data class ArtifactSummary(
public val id: Long,
public val game: String,
public val environment: String,
public val build: CacheExporter.Build?,
public val timestamp: Instant?,
public val type: ArtifactType,
public val format: ArtifactFormat,
public val os: OperatingSystem,
public val arch: Architecture,
public val jvm: Jvm,
public val size: Int
) {
public val name: String
get() {
val builder = StringBuilder()
builder.append(format.getPrefix(os))
when (type) {
ArtifactType.CLIENT -> builder.append(game)
ArtifactType.CLIENT_GL -> builder.append("${game}_gl")
ArtifactType.GLUEGEN_RT -> builder.append("gluegen-rt")
else -> builder.append(type.name.lowercase())
}
if (jvm == Jvm.MICROSOFT) {
builder.append("ms")
}
if (os != OperatingSystem.INDEPENDENT) {
builder.append('-')
builder.append(os.name.lowercase())
}
if (arch != Architecture.INDEPENDENT) {
builder.append('-')
builder.append(arch.name.lowercase())
}
if (build != null) {
builder.append("-b")
builder.append(build)
}
if (timestamp != null) {
builder.append('-')
builder.append(
timestamp
.atOffset(ZoneOffset.UTC)
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm-ss"))
)
}
builder.append("-openrs2#")
builder.append(id)
builder.append('.')
builder.append(format.getExtension(os))
return builder.toString()
}
}
public data class ArtifactSource(
public val name: String?,
public val description: String?,
public val url: String?
)
public data class ArtifactLinkExport(
public val id: Long?,
public val build: CacheExporter.Build?,
public val timestamp: Instant?,
public val link: ArtifactLink
)
public class Artifact(
public val summary: ArtifactSummary,
public val crc32: Int,
public val sha1: ByteArray,
public val sources: List<ArtifactSource>,
public val links: List<ArtifactLinkExport>
) {
public val sha1Hex: String
get() = ByteBufUtil.hexDump(sha1)
}
public class ArtifactExport(
public val summary: ArtifactSummary,
buf: ByteBuf
) : DefaultByteBufHolder(buf)
public suspend fun list(): List<ArtifactSummary> {
return database.execute { connection ->
connection.prepareStatement(
"""
SELECT
a.blob_id,
g.name,
e.name,
a.build_major,
a.build_minor,
a.timestamp,
a.type,
a.format,
a.os,
a.arch,
a.jvm,
length(b.data) AS size
FROM artifacts a
JOIN blobs b ON b.id = a.blob_id
JOIN games g ON g.id = a.game_id
JOIN environments e ON e.id = a.environment_id
ORDER BY a.build_major ASC, a.timestamp ASC, a.type ASC, a.format ASC, a.os ASC, a.arch ASC, a.jvm ASC
""".trimIndent()
).use { stmt ->
stmt.executeQuery().use { rows ->
val artifacts = mutableListOf<ArtifactSummary>()
while (rows.next()) {
val id = rows.getLong(1)
val game = rows.getString(2)
val environment = rows.getString(3)
var buildMajor: Int? = rows.getInt(4)
if (rows.wasNull()) {
buildMajor = null
}
var buildMinor: Int? = rows.getInt(5)
if (rows.wasNull()) {
buildMinor = null
}
val build = if (buildMajor != null) {
CacheExporter.Build(buildMajor, buildMinor)
} else {
null
}
val timestamp = rows.getTimestamp(6)?.toInstant()
val type = ArtifactType.valueOf(rows.getString(7).uppercase())
val format = ArtifactFormat.valueOf(rows.getString(8).uppercase())
val os = OperatingSystem.valueOf(rows.getString(9).uppercase())
val arch = Architecture.valueOf(rows.getString(10).uppercase())
val jvm = Jvm.valueOf(rows.getString(11).uppercase())
val size = rows.getInt(12)
artifacts += ArtifactSummary(
id,
game,
environment,
build,
timestamp,
type,
format,
os,
arch,
jvm,
size
)
}
return@execute artifacts
}
}
}
}
public suspend fun get(id: Long): Artifact? {
return database.execute { connection ->
val sources = mutableListOf<ArtifactSource>()
val links = mutableListOf<ArtifactLinkExport>()
connection.prepareStatement(
"""
SELECT DISTINCT name, description, url
FROM artifact_sources
WHERE blob_id = ?
""".trimIndent()
).use { stmt ->
stmt.setLong(1, id)
stmt.executeQuery().use { rows ->
while (rows.next()) {
val name = rows.getString(1)
val description = rows.getString(2)
val url = rows.getString(3)
sources += ArtifactSource(name, description, url)
}
}
}
connection.prepareStatement(
"""
SELECT
a.blob_id,
a.build_major,
a.build_minor,
a.timestamp,
l.type,
l.format,
l.os,
l.arch,
l.jvm,
COALESCE(l.crc32, b.crc32),
l.sha1,
COALESCE(l.size, length(b.data))
FROM artifact_links l
LEFT JOIN blobs b ON b.sha1 = l.sha1
LEFT JOIN artifacts a ON a.blob_id = b.id
WHERE l.blob_id = ?
ORDER BY l.type, l.format, l.os, l.arch, l.jvm
""".trimIndent()
).use { stmt ->
stmt.setLong(1, id)
stmt.executeQuery().use { rows ->
while (rows.next()) {
var linkId: Long? = rows.getLong(1)
if (rows.wasNull()) {
linkId = null
}
var buildMajor: Int? = rows.getInt(2)
if (rows.wasNull()) {
buildMajor = null
}
var buildMinor: Int? = rows.getInt(3)
if (rows.wasNull()) {
buildMinor = null
}
val build = if (buildMajor != null) {
CacheExporter.Build(buildMajor, buildMinor)
} else {
null
}
val timestamp = rows.getTimestamp(4)?.toInstant()
val type = ArtifactType.valueOf(rows.getString(5).uppercase())
val format = ArtifactFormat.valueOf(rows.getString(6).uppercase())
val os = OperatingSystem.valueOf(rows.getString(7).uppercase())
val arch = Architecture.valueOf(rows.getString(8).uppercase())
val jvm = Jvm.valueOf(rows.getString(9).uppercase())
var crc32: Int? = rows.getInt(10)
if (rows.wasNull()) {
crc32 = null
}
val sha1 = rows.getBytes(11)
var size: Int? = rows.getInt(12)
if (rows.wasNull()) {
size = null
}
links += ArtifactLinkExport(
linkId,
build,
timestamp,
ArtifactLink(
type,
format,
os,
arch,
jvm,
crc32,
sha1,
size
)
)
}
}
}
connection.prepareStatement(
"""
SELECT
g.name,
e.name,
a.build_major,
a.build_minor,
a.timestamp,
a.type,
a.format,
a.os,
a.arch,
a.jvm,
length(b.data) AS size,
b.crc32,
b.sha1
FROM artifacts a
JOIN games g ON g.id = a.game_id
JOIN environments e ON e.id = a.environment_id
JOIN blobs b ON b.id = a.blob_id
WHERE a.blob_id = ?
""".trimIndent()
).use { stmt ->
stmt.setLong(1, id)
stmt.executeQuery().use { rows ->
if (!rows.next()) {
return@execute null
}
val game = rows.getString(1)
val environment = rows.getString(2)
var buildMajor: Int? = rows.getInt(3)
if (rows.wasNull()) {
buildMajor = null
}
var buildMinor: Int? = rows.getInt(4)
if (rows.wasNull()) {
buildMinor = null
}
val build = if (buildMajor != null) {
CacheExporter.Build(buildMajor!!, buildMinor)
} else {
null
}
val timestamp = rows.getTimestamp(5)?.toInstant()
val type = ArtifactType.valueOf(rows.getString(6).uppercase())
val format = ArtifactFormat.valueOf(rows.getString(7).uppercase())
val os = OperatingSystem.valueOf(rows.getString(8).uppercase())
val arch = Architecture.valueOf(rows.getString(9).uppercase())
val jvm = Jvm.valueOf(rows.getString(10).uppercase())
val size = rows.getInt(11)
val crc32 = rows.getInt(12)
val sha1 = rows.getBytes(13)
return@execute Artifact(
ArtifactSummary(
id,
game,
environment,
build,
timestamp,
type,
format,
os,
arch,
jvm,
size
), crc32, sha1, sources, links
)
}
}
}
}
public suspend fun export(id: Long): ArtifactExport? {
return database.execute { connection ->
connection.prepareStatement(
"""
SELECT
g.name,
e.name,
a.build_major,
a.build_minor,
a.timestamp,
a.type,
a.format,
a.os,
a.arch,
a.jvm,
b.data
FROM artifacts a
JOIN games g ON g.id = a.game_id
JOIN environments e ON e.id = a.environment_id
JOIN blobs b ON b.id = a.blob_id
WHERE a.blob_id = ?
""".trimIndent()
).use { stmt ->
stmt.setLong(1, id)
stmt.executeQuery().use { rows ->
if (!rows.next()) {
return@execute null
}
val game = rows.getString(1)
val environment = rows.getString(2)
var buildMajor: Int? = rows.getInt(3)
if (rows.wasNull()) {
buildMajor = null
}
var buildMinor: Int? = rows.getInt(4)
if (rows.wasNull()) {
buildMinor = null
}
val build = if (buildMajor != null) {
CacheExporter.Build(buildMajor, buildMinor)
} else {
null
}
val timestamp = rows.getTimestamp(5)?.toInstant()
val type = ArtifactType.valueOf(rows.getString(6).uppercase())
val format = ArtifactFormat.valueOf(rows.getString(7).uppercase())
val os = OperatingSystem.valueOf(rows.getString(8).uppercase())
val arch = Architecture.valueOf(rows.getString(9).uppercase())
val jvm = Jvm.valueOf(rows.getString(10).uppercase())
val buf = Unpooled.wrappedBuffer(rows.getBytes(11))
val size = buf.readableBytes()
return@execute ArtifactExport(
ArtifactSummary(
id,
game,
environment,
build,
timestamp,
type,
format,
os,
arch,
jvm,
size
), buf
)
}
}
}
}
}

@ -1,997 +0,0 @@
package org.openrs2.archive.client
import com.github.michaelbull.logging.InlineLogger
import com.kichik.pecoff4j.PE
import com.kichik.pecoff4j.constant.MachineType
import com.kichik.pecoff4j.io.PEParser
import dorkbox.cabParser.CabParser
import dorkbox.cabParser.CabStreamSaver
import dorkbox.cabParser.structure.CabFileEntry
import io.netty.buffer.ByteBuf
import io.netty.buffer.ByteBufAllocator
import io.netty.buffer.ByteBufInputStream
import io.netty.buffer.ByteBufOutputStream
import io.netty.buffer.Unpooled
import io.netty.util.ByteProcessor
import jakarta.inject.Inject
import jakarta.inject.Singleton
import net.fornwall.jelf.ElfFile
import net.fornwall.jelf.ElfSymbol
import org.objectweb.asm.Opcodes
import org.objectweb.asm.tree.AbstractInsnNode
import org.objectweb.asm.tree.ClassNode
import org.objectweb.asm.tree.JumpInsnNode
import org.objectweb.asm.tree.LdcInsnNode
import org.objectweb.asm.tree.MethodInsnNode
import org.objectweb.asm.tree.TypeInsnNode
import org.openrs2.archive.cache.CacheExporter
import org.openrs2.archive.cache.CacheImporter
import org.openrs2.asm.InsnMatcher
import org.openrs2.asm.classpath.Library
import org.openrs2.asm.getArgumentExpressions
import org.openrs2.asm.hasCode
import org.openrs2.asm.intConstant
import org.openrs2.asm.io.CabLibraryReader
import org.openrs2.asm.io.JarLibraryReader
import org.openrs2.asm.io.LibraryReader
import org.openrs2.asm.io.Pack200LibraryReader
import org.openrs2.asm.io.PackClassLibraryReader
import org.openrs2.asm.nextReal
import org.openrs2.buffer.use
import org.openrs2.compress.gzip.Gzip
import org.openrs2.db.Database
import org.openrs2.util.io.entries
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.InputStream
import java.io.OutputStream
import java.nio.file.Files
import java.nio.file.Path
import java.sql.Connection
import java.sql.Types
import java.time.Instant
import java.time.LocalDate
import java.time.Month
import java.time.ZoneOffset
import java.util.jar.JarInputStream
import java.util.jar.JarOutputStream
import java.util.jar.Pack200
import kotlin.io.path.getLastModifiedTime
@Singleton
public class ClientImporter @Inject constructor(
private val database: Database,
private val alloc: ByteBufAllocator,
private val packClassLibraryReader: PackClassLibraryReader,
private val importer: CacheImporter
) {
public suspend fun import(
paths: Iterable<Path>,
name: String?,
description: String?,
url: String?,
skipErrors: Boolean
) {
alloc.buffer().use { buf ->
for (path in paths) {
buf.clear()
Files.newInputStream(path).use { input ->
ByteBufOutputStream(buf).use { output ->
input.copyTo(output)
}
}
logger.info { "Importing $path" }
try {
import(
parse(buf),
name,
description,
url,
path.fileName.toString(),
path.getLastModifiedTime().toInstant()
)
} catch (t: Throwable) {
if (skipErrors) {
logger.warn(t) { "Failed to import $path" }
continue
}
throw t
}
}
}
}
public suspend fun import(
artifact: Artifact,
name: String?,
description: String?,
url: String?,
fileName: String,
timestamp: Instant
) {
database.execute { connection ->
importer.prepare(connection)
val id = import(connection, artifact)
connection.prepareStatement(
"""
INSERT INTO artifact_sources (blob_id, name, description, url, file_name, timestamp)
VALUES (?, ?, ?, ?, ?, ?)
""".trimIndent()
).use { stmt ->
stmt.setLong(1, id)
stmt.setString(2, name)
stmt.setString(3, description)
stmt.setString(4, url)
stmt.setString(5, fileName)
stmt.setObject(6, timestamp.atOffset(ZoneOffset.UTC), Types.TIMESTAMP_WITH_TIMEZONE)
stmt.execute()
}
}
}
private fun import(connection: Connection, artifact: Artifact): Long {
val id = importer.addBlob(connection, artifact)
val gameId = connection.prepareStatement(
"""
SELECT id
FROM games
WHERE name = ?
""".trimIndent()
).use { stmt ->
stmt.setString(1, artifact.game)
stmt.executeQuery().use { rows ->
if (!rows.next()) {
throw IllegalArgumentException()
}
rows.getInt(1)
}
}
val environmentId = connection.prepareStatement(
"""
SELECT id
FROM environments
WHERE name = ?
""".trimIndent()
).use { stmt ->
stmt.setString(1, artifact.environment)
stmt.executeQuery().use { rows ->
if (!rows.next()) {
throw IllegalArgumentException()
}
rows.getInt(1)
}
}
connection.prepareStatement(
"""
INSERT INTO artifacts (blob_id, game_id, environment_id, build_major, build_minor, timestamp, type, format, os, arch, jvm)
VALUES (?, ?, ?, ?, ?, ?, ?::artifact_type, ?::artifact_format, ?::os, ?::arch, ?::jvm)
ON CONFLICT (blob_id) DO UPDATE SET
game_id = EXCLUDED.game_id,
environment_id = EXCLUDED.environment_id,
build_major = EXCLUDED.build_major,
build_minor = EXCLUDED.build_minor,
timestamp = EXCLUDED.timestamp,
type = EXCLUDED.type,
format = EXCLUDED.format,
os = EXCLUDED.os,
arch = EXCLUDED.arch,
jvm = EXCLUDED.jvm
""".trimIndent()
).use { stmt ->
stmt.setLong(1, id)
stmt.setInt(2, gameId)
stmt.setInt(3, environmentId)
stmt.setObject(4, artifact.build?.major, Types.INTEGER)
stmt.setObject(5, artifact.build?.minor, Types.INTEGER)
stmt.setObject(6, artifact.timestamp?.atOffset(ZoneOffset.UTC), Types.TIMESTAMP_WITH_TIMEZONE)
stmt.setString(7, artifact.type.name.lowercase())
stmt.setString(8, artifact.format.name.lowercase())
stmt.setString(9, artifact.os.name.lowercase())
stmt.setString(10, artifact.arch.name.lowercase())
stmt.setString(11, artifact.jvm.name.lowercase())
stmt.execute()
}
connection.prepareStatement(
"""
DELETE FROM artifact_links
WHERE blob_id = ?
""".trimIndent()
).use { stmt ->
stmt.setLong(1, id)
stmt.execute()
}
connection.prepareStatement(
"""
INSERT INTO artifact_links (blob_id, type, format, os, arch, jvm, sha1, crc32, size)
VALUES (?, ?::artifact_type, ?::artifact_format, ?::os, ?::arch, ?::jvm, ?, ?, ?)
""".trimIndent()
).use { stmt ->
for (link in artifact.links) {
stmt.setLong(1, id)
stmt.setString(2, link.type.name.lowercase())
stmt.setString(3, link.format.name.lowercase())
stmt.setString(4, link.os.name.lowercase())
stmt.setString(5, link.arch.name.lowercase())
stmt.setString(6, link.jvm.name.lowercase())
stmt.setBytes(7, link.sha1)
stmt.setObject(8, link.crc32, Types.INTEGER)
stmt.setObject(9, link.size, Types.INTEGER)
stmt.addBatch()
}
stmt.executeBatch()
}
return id
}
public suspend fun refresh() {
data class Blob(val id: Long, val bytes: ByteArray)
database.execute { connection ->
importer.prepare(connection)
var lastId: Long? = null
val blobs = mutableListOf<Blob>()
while (true) {
blobs.clear()
connection.prepareStatement(
"""
SELECT a.blob_id, b.data
FROM artifacts a
JOIN blobs b ON b.id = a.blob_id
WHERE ? IS NULL OR a.blob_id > ?
ORDER BY a.blob_id ASC
LIMIT 1024
""".trimIndent()
).use { stmt ->
stmt.setObject(1, lastId, Types.BIGINT)
stmt.setObject(2, lastId, Types.BIGINT)
stmt.executeQuery().use { rows ->
while (rows.next()) {
val id = rows.getLong(1)
lastId = id
blobs += Blob(id, rows.getBytes(2))
}
}
}
if (blobs.isEmpty()) {
return@execute
}
for (blob in blobs) {
logger.info { "Refreshing artifact ${blob.id}" }
Unpooled.wrappedBuffer(blob.bytes).use { buf ->
import(connection, parse(buf))
}
}
}
}
}
private fun parse(buf: ByteBuf): Artifact {
return if (buf.hasPrefix(JAR)) {
parseJar(buf)
} else if (buf.hasPrefix(PACK200)) {
parsePack200(buf)
} else if (buf.hasPrefix(CAB)) {
parseCab(buf)
} else if (
buf.hasPrefix(PACKCLASS_UNCOMPRESSED) ||
buf.hasPrefix(PACKCLASS_BZIP2) ||
buf.hasPrefix(PACKCLASS_GZIP)
) {
parseLibrary(buf, packClassLibraryReader, ArtifactFormat.PACKCLASS)
} else if (buf.hasPrefix(ELF)) {
parseElf(buf)
} else if (buf.hasPrefix(PE)) {
parsePe(buf)
} else if (
buf.hasPrefix(MACHO32BE) ||
buf.hasPrefix(MACHO32LE) ||
buf.hasPrefix(MACHO64BE) ||
buf.hasPrefix(MACHO64LE) ||
buf.hasPrefix(MACHO_UNIVERSAL)
) {
parseMachO(buf)
} else {
throw IllegalArgumentException()
}
}
private fun parseElf(buf: ByteBuf): Artifact {
val elf = ElfFile.from(ByteBufInputStream(buf.slice()))
val arch = when (elf.e_machine.toInt()) {
ElfFile.ARCH_i386 -> Architecture.X86
ElfFile.ARCH_X86_64 -> Architecture.AMD64
ElfFile.ARCH_SPARC -> Architecture.SPARC
ARCH_SPARCV9 -> Architecture.SPARCV9
else -> throw IllegalArgumentException()
}
val comment = String(elf.firstSectionByName(".comment").data)
val os = if (comment.contains(SOLARIS_COMMENT)) {
OperatingSystem.SOLARIS
} else {
OperatingSystem.LINUX
}
val symbols = elf.dynamicSymbolTableSection ?: throw IllegalArgumentException()
val type = getArtifactType(symbols.symbols.asSequence().mapNotNull(ElfSymbol::getName))
return Artifact(
buf.retain(),
"shared",
"live",
null,
null,
type,
ArtifactFormat.NATIVE,
os,
arch,
Jvm.SUN,
emptyList()
)
}
private fun getArtifactType(symbols: Sequence<String>): ArtifactType {
for (symbol in symbols) {
var name = symbol
if (name.startsWith('_')) {
name = name.substring(1)
}
if (name.startsWith("Java_")) { // RNI methods don't have a Java_ prefix
name = name.substring("Java_".length)
}
if (name.startsWith("jaggl_X11_dri_")) {
return ArtifactType.JAGGL_DRI
} else if (name.startsWith("jaggl_opengl_")) {
return ArtifactType.JAGGL
} else if (name.startsWith("com_sun_opengl_impl_GLImpl_")) {
return ArtifactType.JOGL
} else if (name.startsWith("com_sun_opengl_impl_JAWT_")) {
return ArtifactType.JOGL_AWT
} else if (name.startsWith("com_sun_gluegen_runtime_")) {
return ArtifactType.GLUEGEN_RT
} else if (name.startsWith("jagex3_jagmisc_jagmisc_")) {
return ArtifactType.JAGMISC
} else if (name.startsWith("nativeadvert_browsercontrol_")) {
return ArtifactType.BROWSERCONTROL
}
}
throw IllegalArgumentException()
}
private fun parsePe(buf: ByteBuf): Artifact {
val pe = PEParser.parse(ByteBufInputStream(buf.slice()))
val arch = when (pe.coffHeader.machine) {
MachineType.IMAGE_FILE_MACHINE_I386 -> Architecture.X86
MachineType.IMAGE_FILE_MACHINE_AMD64 -> Architecture.AMD64
else -> throw IllegalArgumentException()
}
val symbols = parsePeExportNames(buf, pe).toSet()
val type = getArtifactType(symbols.asSequence())
val jvm = if (symbols.contains("RNIGetCompatibleVersion")) {
Jvm.MICROSOFT
} else {
Jvm.SUN
}
return Artifact(
buf.retain(),
"shared",
"live",
null,
Instant.ofEpochSecond(pe.coffHeader.timeDateStamp.toLong()),
type,
ArtifactFormat.NATIVE,
OperatingSystem.WINDOWS,
arch,
jvm,
emptyList()
)
}
private fun parsePeExportNames(buf: ByteBuf, pe: PE): Sequence<String> {
return sequence {
val exportTable = pe.imageData.exportTable
val namePointerTable =
pe.sectionTable.rvaConverter.convertVirtualAddressToRawDataPointer(exportTable.namePointerRVA.toInt())
for (i in 0 until exportTable.numberOfNamePointers.toInt()) {
val namePointerRva = buf.readerIndex() + buf.getIntLE(buf.readerIndex() + namePointerTable + 4 * i)
val namePointer = pe.sectionTable.rvaConverter.convertVirtualAddressToRawDataPointer(namePointerRva)
val end = buf.forEachByte(namePointer, buf.writerIndex() - namePointer, ByteProcessor.FIND_NUL)
require(end != -1) {
"Unterminated string"
}
yield(buf.toString(namePointer, end - namePointer, Charsets.US_ASCII))
}
}
}
private fun parseMachO(buf: ByteBuf): Artifact {
val (arch, symbols) = MachO.parse(buf.slice())
val type = getArtifactType(symbols.asSequence())
return Artifact(
buf.retain(),
"shared",
"live",
null,
null,
type,
ArtifactFormat.NATIVE,
OperatingSystem.MACOS,
arch,
Jvm.SUN,
emptyList()
)
}
private fun parseJar(buf: ByteBuf): Artifact {
val timestamp = getJarTimestamp(ByteBufInputStream(buf.slice()))
return parseLibrary(buf, JarLibraryReader, ArtifactFormat.JAR, timestamp)
}
private fun parsePack200(buf: ByteBuf): Artifact {
val timestamp = ByteArrayOutputStream().use { tempOutput ->
Gzip.createHeaderlessInputStream(ByteBufInputStream(buf.slice())).use { gzipInput ->
JarOutputStream(tempOutput).use { jarOutput ->
Pack200.newUnpacker().unpack(gzipInput, jarOutput)
}
}
getJarTimestamp(ByteArrayInputStream(tempOutput.toByteArray()))
}
return parseLibrary(buf, Pack200LibraryReader, ArtifactFormat.PACK200, timestamp)
}
private fun parseCab(buf: ByteBuf): Artifact {
val timestamp = getCabTimestamp(ByteBufInputStream(buf.slice()))
return parseLibrary(buf, CabLibraryReader, ArtifactFormat.CAB, timestamp)
}
private fun getJarTimestamp(input: InputStream): Instant? {
var timestamp: Instant? = null
JarInputStream(input).use { jar ->
for (entry in jar.entries) {
val t = entry.lastModifiedTime?.toInstant()
if (timestamp == null || (t != null && t < timestamp)) {
timestamp = t
}
}
}
return timestamp
}
private fun getCabTimestamp(input: InputStream): Instant? {
var timestamp: Instant? = null
CabParser(input, object : CabStreamSaver {
override fun closeOutputStream(outputStream: OutputStream, entry: CabFileEntry) {
// entry
}
override fun openOutputStream(entry: CabFileEntry): OutputStream {
val t = entry.date.toInstant()
if (timestamp == null || t < timestamp) {
timestamp = t
}
return OutputStream.nullOutputStream()
}
override fun saveReservedAreaData(data: ByteArray?, dataLength: Int): Boolean {
return false
}
}).extractStream()
return timestamp
}
private fun parseLibrary(
buf: ByteBuf,
reader: LibraryReader,
format: ArtifactFormat,
timestamp: Instant? = null
): Artifact {
val library = Library.read("client", ByteBufInputStream(buf.slice()), reader)
val game: String
val build: CacheExporter.Build?
val type: ArtifactType
val links: List<ArtifactLink>
val mudclient = library["mudclient"]
val client = library["client"]
val loader = library["loader"]
if (mudclient != null) {
game = "classic"
build = null // TODO(gpe): classic support
type = ArtifactType.CLIENT
links = emptyList()
} else if (client != null) {
game = "runescape"
build = parseClientBuild(library, client)
type = if (build != null && build.major < COMBINED_BUILD && isClientGl(library)) {
ArtifactType.CLIENT_GL
} else {
ArtifactType.CLIENT
}
links = emptyList()
} else if (loader != null) {
if (isLoaderClassic(loader)) {
game = "classic"
build = null // TODO(gpe): classic support
type = ArtifactType.LOADER
links = emptyList() // TODO(gpe): classic support
} else {
game = "runescape"
build = parseSignLinkBuild(library)
type = if (timestamp != null && timestamp < COMBINED_TIMESTAMP && isLoaderGl(library)) {
ArtifactType.LOADER_GL
} else {
ArtifactType.LOADER
}
links = parseLinks(library)
}
} else if (library.contains("mapview")) {
game = "mapview"
build = null
type = ArtifactType.CLIENT
links = emptyList()
} else if (library.contains("loginapplet")) {
game = "loginapplet"
build = null
type = ArtifactType.CLIENT
links = emptyList()
} else if (library.contains("passwordapp")) {
game = "passapplet"
build = null
type = ArtifactType.CLIENT
links = emptyList()
} else if (library.contains("jaggl/opengl")) {
game = "shared"
type = ArtifactType.JAGGL
build = null
links = emptyList()
} else if (library.contains("com/sun/opengl/impl/GLImpl")) {
game = "shared"
type = ArtifactType.JOGL
build = null
links = emptyList()
} else if (library.contains("unpackclass")) {
game = "shared"
type = ArtifactType.UNPACKCLASS
build = null
links = emptyList()
} else {
throw IllegalArgumentException()
}
return Artifact(
buf.retain(),
game,
"live",
build,
timestamp,
type,
format,
OperatingSystem.INDEPENDENT,
Architecture.INDEPENDENT,
Jvm.INDEPENDENT,
links
)
}
private fun isClientGl(library: Library): Boolean {
for (clazz in library) {
for (method in clazz.methods) {
if (!method.hasCode) {
continue
}
for (insn in method.instructions) {
if (insn is MethodInsnNode && insn.name == "glBegin") {
return true
}
}
}
}
return false
}
private fun isLoaderClassic(clazz: ClassNode): Boolean {
for (method in clazz.methods) {
if (!method.hasCode) {
continue
}
for (insn in method.instructions) {
if (insn is LdcInsnNode && insn.cst == "mudclient") {
return true
}
}
}
return false
}
private fun isLoaderGl(library: Library): Boolean {
for (clazz in library) {
for (method in clazz.methods) {
if (!method.hasCode || method.name != "<clinit>") {
continue
}
for (insn in method.instructions) {
if (insn !is LdcInsnNode) {
continue
}
if (insn.cst == "jaggl.dll" || insn.cst == "jogl.dll") {
return true
}
}
}
}
return false
}
private fun parseClientBuild(library: Library, clazz: ClassNode): CacheExporter.Build? {
for (method in clazz.methods) {
if (!method.hasCode || method.name != "main") {
continue
}
for (match in OLD_ENGINE_VERSION_MATCHER.match(method)) {
val ldc = match[0] as LdcInsnNode
if (ldc.cst != OLD_ENGINE_VERSION_STRING) {
continue
}
val version = match[2].intConstant
if (version != null) {
return CacheExporter.Build(version, null)
}
}
var betweenNewAndReturn = false
val candidates = mutableListOf<Int>()
for (insn in method.instructions) {
if (insn is TypeInsnNode && insn.desc == "client") {
betweenNewAndReturn = true
} else if (insn.opcode == Opcodes.RETURN) {
break
} else if (betweenNewAndReturn) {
val candidate = insn.intConstant
if (candidate != null && candidate in NEW_ENGINE_BUILDS) {
candidates += candidate
}
}
}
for (build in NEW_ENGINE_RESOLUTIONS) {
candidates -= build
}
val version = candidates.singleOrNull()
if (version != null) {
return CacheExporter.Build(version, null)
}
}
return parseSignLinkBuild(library)
}
private fun parseSignLinkBuild(library: Library): CacheExporter.Build? {
val clazz = library["sign/signlink"] ?: return null
for (field in clazz.fields) {
val value = field.value
if (field.name == "clientversion" && field.desc == "I" && value is Int) {
return CacheExporter.Build(value, null)
}
}
return null
}
private fun parseLinks(library: Library): List<ArtifactLink> {
val sig = library["sig"]
if (sig != null) {
var size: Int? = null
var sha1: ByteArray? = null
for (field in sig.fields) {
val value = field.value
if (field.name == "len" && field.desc == "I" && value is Int) {
size = value
}
}
for (method in sig.methods) {
if (!method.hasCode || method.name != "<clinit>") {
continue
}
for (match in SHA1_MATCHER.match(method)) {
val len = match[0].intConstant
if (len != SHA1_BYTES) {
continue
}
sha1 = ByteArray(SHA1_BYTES)
for (i in 2 until match.size step 4) {
val k = match[i + 1].intConstant!!
val v = match[i + 2].intConstant!!
sha1[k] = v.toByte()
}
}
}
require(size != null && sha1 != null)
return listOf(
ArtifactLink(
ArtifactType.CLIENT,
ArtifactFormat.JAR,
OperatingSystem.INDEPENDENT,
Architecture.INDEPENDENT,
Jvm.INDEPENDENT,
crc32 = null,
sha1,
size
)
)
}
val loader = library["loader"]
if (loader != null) {
val links = mutableListOf<ArtifactLink>()
val paths = mutableSetOf<String>()
for (method in loader.methods) {
if (method.name != "run" || method.desc != "()V") {
continue
}
for (insn in method.instructions) {
if (insn !is MethodInsnNode || insn.owner != loader.name || !insn.desc.endsWith(")[B")) {
continue
}
// TODO(gpe): extract file size too (tricky due to dummy arguments)
val exprs = getArgumentExpressions(insn) ?: continue
for (expr in exprs) {
val single = expr.singleOrNull() ?: continue
if (single !is LdcInsnNode) {
continue
}
val cst = single.cst
if (cst is String && FILE_NAME_REGEX.matches(cst)) {
paths += cst
}
}
}
}
val hashes = mutableMapOf<AbstractInsnNode, ByteArray>()
for (method in loader.methods) {
for (match in SHA1_CMP_MATCHER.match(method)) {
val sha1 = ByteArray(SHA1_BYTES)
var i = 0
while (i < match.size) {
var n = match[i++].intConstant
if (n != null) {
i++ // ALOAD
}
val index = match[i++].intConstant!!
i++ // BALOAD
var xor = false
if (i + 1 < match.size && match[i + 1].opcode == Opcodes.IXOR) {
i += 2 // ICONST_M1, IXOR
xor = true
}
if (match[i].opcode == Opcodes.IFNE) {
n = 0
i++
} else {
if (n == null) {
n = match[i++].intConstant!!
}
i++ // ICMP_IFNE
}
if (xor) {
n = n.inv()
}
sha1[index] = n.toByte()
}
hashes[match[0]] = sha1
}
}
for (method in loader.methods) {
for (match in PATH_CMP_MATCHER.match(method)) {
val first = match[0]
val ldc = if (first is LdcInsnNode) {
first
} else {
match[1] as LdcInsnNode
}
val path = ldc.cst
if (path !is String) {
continue
}
val acmp = match[2] as JumpInsnNode
val target = if (acmp.opcode == Opcodes.IF_ACMPNE) {
acmp.nextReal
} else {
acmp.label.nextReal
}
val hash = hashes.remove(target) ?: continue
if (!paths.remove(path)) {
logger.warn { "Adding link for unused file $path" }
}
links += parseLink(path, hash)
}
}
if (paths.size != hashes.size || paths.size > 1) {
throw IllegalArgumentException()
} else if (paths.size == 1) {
links += parseLink(paths.single(), hashes.values.single())
}
return links
}
// TODO(gpe)
return emptyList()
}
private fun parseLink(path: String, sha1: ByteArray): ArtifactLink {
val m = FILE_NAME_REGEX.matchEntire(path) ?: throw IllegalArgumentException()
val (name, crc1, ext, crc2) = m.destructured
val type = when (name) {
// TODO(gpe): funorb loaders
"runescape", "client" -> ArtifactType.CLIENT
"unpackclass" -> ArtifactType.UNPACKCLASS
"jogl", "jogltrimmed" -> ArtifactType.JOGL
"jogl_awt" -> ArtifactType.JOGL_AWT
else -> throw IllegalArgumentException()
}
val format = when (ext) {
"pack200" -> ArtifactFormat.PACK200
"js5" -> ArtifactFormat.PACKCLASS
"jar", "pack" -> ArtifactFormat.JAR
"dll" -> ArtifactFormat.NATIVE
else -> throw IllegalArgumentException()
}
val os = if (format == ArtifactFormat.NATIVE) OperatingSystem.WINDOWS else OperatingSystem.INDEPENDENT
val arch = if (format == ArtifactFormat.NATIVE) Architecture.X86 else Architecture.INDEPENDENT
val jvm = if (format == ArtifactFormat.NATIVE) Jvm.SUN else Jvm.INDEPENDENT
val crc = crc1.toIntOrNull() ?: crc2.toIntOrNull() ?: throw IllegalArgumentException()
return ArtifactLink(
type,
format,
os,
arch,
jvm,
crc,
sha1,
null
)
}
private fun ByteBuf.hasPrefix(bytes: ByteArray): Boolean {
Unpooled.wrappedBuffer(bytes).use { prefix ->
val len = prefix.readableBytes()
if (readableBytes() < len) {
return false
}
return slice(readerIndex(), len) == prefix
}
}
private companion object {
private val logger = InlineLogger()
private val CAB = byteArrayOf('M'.code.toByte(), 'S'.code.toByte(), 'C'.code.toByte(), 'F'.code.toByte())
private val ELF = byteArrayOf(0x7F, 'E'.code.toByte(), 'L'.code.toByte(), 'F'.code.toByte())
private val JAR = byteArrayOf('P'.code.toByte(), 'K'.code.toByte(), 0x03, 0x04)
private val MACHO32BE = byteArrayOf(0xFE.toByte(), 0xED.toByte(), 0xFA.toByte(), 0xCE.toByte())
private val MACHO32LE = byteArrayOf(0xCE.toByte(), 0xFA.toByte(), 0xED.toByte(), 0xFE.toByte())
private val MACHO64BE = byteArrayOf(0xFE.toByte(), 0xED.toByte(), 0xFA.toByte(), 0xCF.toByte())
private val MACHO64LE = byteArrayOf(0xCF.toByte(), 0xFA.toByte(), 0xED.toByte(), 0xFE.toByte())
private val MACHO_UNIVERSAL = byteArrayOf(0xCA.toByte(), 0xFE.toByte(), 0xBA.toByte(), 0xBE.toByte())
private val PACK200 = byteArrayOf(0x08)
private val PACKCLASS_UNCOMPRESSED = byteArrayOf(0x00)
private val PACKCLASS_BZIP2 = byteArrayOf(0x01)
private val PACKCLASS_GZIP = byteArrayOf(0x02)
private val PE = byteArrayOf('M'.code.toByte(), 'Z'.code.toByte())
private const val OLD_ENGINE_VERSION_STRING = "RS2 user client - release #"
private val OLD_ENGINE_VERSION_MATCHER =
InsnMatcher.compile("LDC INVOKESPECIAL (ICONST | BIPUSH | SIPUSH | LDC)")
private val NEW_ENGINE_RESOLUTIONS = listOf(765, 503, 1024, 768)
private val NEW_ENGINE_BUILDS = 402..916
private const val COMBINED_BUILD = 555
private val COMBINED_TIMESTAMP = LocalDate.of(2009, Month.SEPTEMBER, 2)
.atStartOfDay(ZoneOffset.UTC)
.toInstant()
private const val ARCH_SPARCV9 = 43
private const val SOLARIS_COMMENT = "Solaris Link Editors:"
private const val SHA1_BYTES = 20
private val SHA1_MATCHER =
InsnMatcher.compile("BIPUSH NEWARRAY (DUP (ICONST | BIPUSH) (ICONST | BIPUSH | SIPUSH) IASTORE)+")
private val FILE_NAME_REGEX = Regex("([a-z_]+)(?:_(-?[0-9]+))?[.]([a-z0-9]+)(?:\\?crc=(-?[0-9]+))?")
private val SHA1_CMP_MATCHER =
InsnMatcher.compile("((ICONST | BIPUSH)? ALOAD (ICONST | BIPUSH) BALOAD (ICONST IXOR)? (ICONST | BIPUSH)? (IF_ICMPEQ | IF_ICMPNE | IFEQ | IFNE))+")
private val PATH_CMP_MATCHER = InsnMatcher.compile("(LDC ALOAD | ALOAD LDC) (IF_ACMPEQ | IF_ACMPNE)")
}
}

@ -1,30 +0,0 @@
package org.openrs2.archive.client
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.parameters.arguments.argument
import com.github.ajalt.clikt.parameters.types.defaultStdout
import com.github.ajalt.clikt.parameters.types.long
import com.github.ajalt.clikt.parameters.types.outputStream
import com.google.inject.Guice
import kotlinx.coroutines.runBlocking
import org.openrs2.archive.ArchiveModule
import org.openrs2.inject.CloseableInjector
import java.io.FileNotFoundException
public class ExportCommand : CliktCommand(name = "export") {
private val id by argument().long()
private val output by argument().outputStream().defaultStdout()
override fun run(): Unit = runBlocking {
CloseableInjector(Guice.createInjector(ArchiveModule)).use { injector ->
val exporter = injector.getInstance(ClientExporter::class.java)
val artifact = exporter.export(id) ?: throw FileNotFoundException()
try {
val buf = artifact.content()
buf.readBytes(output, buf.readableBytes())
} finally {
artifact.release()
}
}
}
}

@ -1,32 +0,0 @@
package org.openrs2.archive.client
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.parameters.arguments.argument
import com.github.ajalt.clikt.parameters.arguments.multiple
import com.github.ajalt.clikt.parameters.options.flag
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.types.path
import com.google.inject.Guice
import kotlinx.coroutines.runBlocking
import org.openrs2.archive.ArchiveModule
import org.openrs2.inject.CloseableInjector
public class ImportCommand : CliktCommand(name = "import") {
private val name by option()
private val description by option()
private val url by option()
private val skipErrors by option().flag()
private val input by argument().path(
mustExist = true,
canBeDir = false,
mustBeReadable = true,
).multiple()
override fun run(): Unit = runBlocking {
CloseableInjector(Guice.createInjector(ArchiveModule)).use { injector ->
val importer = injector.getInstance(ClientImporter::class.java)
importer.import(input, name, description, url, skipErrors)
}
}
}

@ -1,7 +0,0 @@
package org.openrs2.archive.client
public enum class Jvm {
INDEPENDENT,
SUN,
MICROSOFT
}

@ -1,116 +0,0 @@
package org.openrs2.archive.client
import io.netty.buffer.ByteBuf
import org.openrs2.buffer.readString
public data class MachO(
public val architecture: Architecture,
public val symbols: Set<String>,
) {
public companion object {
private const val MACHO_UNIVERSAL = 0xCAFEBABE.toInt()
private const val MACHO32BE = 0xFEEDFACE.toInt()
private const val MACHO32LE = 0xCEFAEDFE.toInt()
private const val MACHO64BE = 0xFEEDFACF.toInt()
private const val MACHO64LE = 0xCFFAEDFE.toInt()
private const val CPU_TYPE_X86 = 0x7
private const val CPU_TYPE_AMD64 = 0x1000007
private const val CPU_TYPE_POWERPC = 0x12
private const val COMMAND_SYMTAB = 0x2
public fun parse(buf: ByteBuf): MachO {
val magic = buf.getInt(buf.readerIndex())
return if (magic == MACHO_UNIVERSAL) {
parseFat(buf)
} else {
parseMachO(buf)
}
}
private fun parseFat(buf: ByteBuf): MachO {
buf.skipBytes(4)
val symbols = mutableSetOf<String>()
val count = buf.readInt()
for (i in 0 until count) {
buf.skipBytes(8)
val offset = buf.readInt()
val size = buf.readInt()
buf.skipBytes(4)
symbols += parseMachO(buf.slice(offset, size)).symbols
}
return MachO(Architecture.UNIVERSAL, symbols)
}
private fun parseMachO(buf: ByteBuf): MachO {
val magic = buf.readInt()
require(magic == MACHO32BE || magic == MACHO32LE || magic == MACHO64BE || magic == MACHO64LE)
val big = magic == MACHO32BE || magic == MACHO64BE
val x64 = magic == MACHO64LE || magic == MACHO64BE
val arch = when (if (big) buf.readInt() else buf.readIntLE()) {
CPU_TYPE_X86 -> Architecture.X86
CPU_TYPE_AMD64 -> Architecture.AMD64
CPU_TYPE_POWERPC -> Architecture.POWERPC
else -> throw IllegalArgumentException()
}
buf.skipBytes(4) // cpuSubType
buf.skipBytes(4) // fileType
val nCmds = if (big) buf.readInt() else buf.readIntLE()
buf.skipBytes(4) // sizeOfCmds
buf.skipBytes(4) // flags
if (x64) {
buf.skipBytes(4) // reserved
}
val symbols = parseCommands(buf, big, nCmds)
return MachO(arch, symbols)
}
private fun parseCommands(buf: ByteBuf, big: Boolean, count: Int): Set<String> {
for (i in 0 until count) {
val base = buf.readerIndex()
val command = if (big) buf.readInt() else buf.readIntLE()
val size = if (big) buf.readInt() else buf.readIntLE()
if (command == COMMAND_SYMTAB) {
buf.skipBytes(8)
val strOff = if (big) buf.readInt() else buf.readIntLE()
val strSize = if (big) buf.readInt() else buf.readIntLE()
return parseStringTable(buf.slice(strOff, strSize))
}
buf.readerIndex(base + size)
}
return emptySet()
}
private fun parseStringTable(buf: ByteBuf): Set<String> {
return buildSet {
while (buf.isReadable) {
val str = buf.readString(Charsets.US_ASCII)
if (str.isNotEmpty()) {
add(str)
}
}
}
}
}
}

@ -1,43 +0,0 @@
package org.openrs2.archive.client
import io.ktor.http.ContentType
public enum class OperatingSystem {
INDEPENDENT,
WINDOWS,
MACOS,
LINUX,
SOLARIS;
public fun getPrefix(): String {
return when (this) {
INDEPENDENT -> throw IllegalArgumentException()
WINDOWS -> ""
else -> "lib"
}
}
public fun getExtension(): String {
return when (this) {
INDEPENDENT -> throw IllegalArgumentException()
WINDOWS -> "dll"
MACOS -> "dylib"
LINUX, SOLARIS -> "so"
}
}
public fun getContentType(): ContentType {
return when (this) {
INDEPENDENT -> throw IllegalArgumentException()
WINDOWS -> PE
MACOS -> MACHO
LINUX, SOLARIS -> ELF_SHARED
}
}
private companion object {
private val ELF_SHARED = ContentType("application", "x-sharedlib")
private val MACHO = ContentType("application", "x-mach-binary")
private val PE = ContentType("application", "vnd.microsoft.portable-executable")
}
}

@ -1,16 +0,0 @@
package org.openrs2.archive.client
import com.github.ajalt.clikt.core.CliktCommand
import com.google.inject.Guice
import kotlinx.coroutines.runBlocking
import org.openrs2.archive.ArchiveModule
import org.openrs2.inject.CloseableInjector
public class RefreshCommand : CliktCommand(name = "refresh") {
override fun run(): Unit = runBlocking {
CloseableInjector(Guice.createInjector(ArchiveModule)).use { injector ->
val importer = injector.getInstance(ClientImporter::class.java)
importer.refresh()
}
}
}

@ -1,8 +1,8 @@
package org.openrs2.archive.game
import jakarta.inject.Inject
import jakarta.inject.Singleton
import org.openrs2.db.Database
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
public class GameDatabase @Inject constructor(

@ -3,17 +3,17 @@ package org.openrs2.archive.key
import io.netty.buffer.ByteBuf
import io.netty.buffer.Unpooled
import org.openrs2.buffer.use
import org.openrs2.crypto.SymmetricKey
import org.openrs2.crypto.XteaKey
import java.io.InputStream
public object BinaryKeyReader : KeyReader {
override fun read(input: InputStream): Sequence<SymmetricKey> {
override fun read(input: InputStream): Sequence<XteaKey> {
Unpooled.wrappedBuffer(input.readBytes()).use { buf ->
val len = buf.readableBytes()
if (len == (128 * 128 * 16)) {
val keys = read(buf, 0)
require(SymmetricKey.ZERO in keys)
require(XteaKey.ZERO in keys)
return keys.asSequence()
}
@ -22,19 +22,19 @@ public object BinaryKeyReader : KeyReader {
if (maybeShort && !maybeInt) {
val keys = read(buf, 2)
require(SymmetricKey.ZERO in keys)
require(XteaKey.ZERO in keys)
return keys.asSequence()
} else if (!maybeShort && maybeInt) {
val keys = read(buf, 4).asSequence()
require(SymmetricKey.ZERO in keys)
require(XteaKey.ZERO in keys)
return keys.asSequence()
} else if (maybeShort && maybeInt) {
val shortKeys = read(buf, 2)
val intKeys = read(buf, 4)
return if (SymmetricKey.ZERO in shortKeys && SymmetricKey.ZERO !in intKeys) {
return if (XteaKey.ZERO in shortKeys && XteaKey.ZERO !in intKeys) {
shortKeys.asSequence()
} else if (SymmetricKey.ZERO !in shortKeys && SymmetricKey.ZERO in intKeys) {
} else if (XteaKey.ZERO !in shortKeys && XteaKey.ZERO in intKeys) {
intKeys.asSequence()
} else {
throw IllegalArgumentException("Failed to determine if map square IDs are 2 or 4 bytes")
@ -47,8 +47,8 @@ public object BinaryKeyReader : KeyReader {
}
}
private fun read(buf: ByteBuf, mapSquareLen: Int): Set<SymmetricKey> {
val keys = mutableSetOf<SymmetricKey>()
private fun read(buf: ByteBuf, mapSquareLen: Int): Set<XteaKey> {
val keys = mutableSetOf<XteaKey>()
while (buf.isReadable) {
buf.skipBytes(mapSquareLen)
@ -57,7 +57,7 @@ public object BinaryKeyReader : KeyReader {
val k1 = buf.readInt()
val k2 = buf.readInt()
val k3 = buf.readInt()
keys += SymmetricKey(k0, k1, k2, k3)
keys += XteaKey(k0, k1, k2, k3)
}
return keys

@ -1,17 +1,17 @@
package org.openrs2.archive.key
import jakarta.inject.Inject
import jakarta.inject.Singleton
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.future.await
import kotlinx.coroutines.withContext
import org.openrs2.crypto.SymmetricKey
import org.openrs2.crypto.XteaKey
import org.openrs2.http.checkStatusCode
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.time.Duration
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
public class HdosKeyDownloader @Inject constructor(
@ -21,7 +21,7 @@ public class HdosKeyDownloader @Inject constructor(
return setOf(ENDPOINT)
}
override suspend fun download(url: String): Sequence<SymmetricKey> {
override suspend fun download(url: String): Sequence<XteaKey> {
val request = HttpRequest.newBuilder(URI(url))
.GET()
.timeout(Duration.ofSeconds(30))
@ -33,7 +33,7 @@ public class HdosKeyDownloader @Inject constructor(
return withContext(Dispatchers.IO) {
response.body().use { input ->
input.bufferedReader().use { reader ->
val keys = mutableSetOf<SymmetricKey>()
val keys = mutableSetOf<XteaKey>()
for (line in reader.lineSequence()) {
val parts = line.split(',')
@ -41,7 +41,7 @@ public class HdosKeyDownloader @Inject constructor(
continue
}
val key = SymmetricKey.fromHexOrNull(parts[2]) ?: continue
val key = XteaKey.fromHexOrNull(parts[2]) ?: continue
keys += key
}

@ -1,13 +1,13 @@
package org.openrs2.archive.key
import org.openrs2.crypto.SymmetricKey
import org.openrs2.crypto.XteaKey
import java.io.InputStream
public object HexKeyReader : KeyReader {
override fun read(input: InputStream): Sequence<SymmetricKey> {
override fun read(input: InputStream): Sequence<XteaKey> {
return input.bufferedReader()
.lineSequence()
.map(SymmetricKey::fromHexOrNull)
.map(XteaKey::fromHexOrNull)
.filterNotNull()
}
}

@ -3,7 +3,7 @@ package org.openrs2.archive.key
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.future.await
import kotlinx.coroutines.withContext
import org.openrs2.crypto.SymmetricKey
import org.openrs2.crypto.XteaKey
import org.openrs2.http.checkStatusCode
import java.net.URI
import java.net.http.HttpClient
@ -16,7 +16,7 @@ public abstract class JsonKeyDownloader(
private val client: HttpClient,
private val jsonKeyReader: JsonKeyReader
) : KeyDownloader(source) {
override suspend fun download(url: String): Sequence<SymmetricKey> {
override suspend fun download(url: String): Sequence<XteaKey> {
val request = HttpRequest.newBuilder(URI(url))
.GET()
.timeout(Duration.ofSeconds(30))

@ -2,32 +2,32 @@ package org.openrs2.archive.key
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.treeToValue
import jakarta.inject.Inject
import jakarta.inject.Singleton
import org.openrs2.crypto.SymmetricKey
import org.openrs2.crypto.XteaKey
import org.openrs2.json.Json
import java.io.IOException
import java.io.InputStream
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
public class JsonKeyReader @Inject constructor(
@Json private val mapper: ObjectMapper
) : KeyReader {
override fun read(input: InputStream): Sequence<SymmetricKey> {
val keys = mutableSetOf<SymmetricKey>()
override fun read(input: InputStream): Sequence<XteaKey> {
val keys = mutableSetOf<XteaKey>()
val root = mapper.readTree(input)
when {
root.isArray -> {
for (entry in root) {
val key = entry["key"] ?: entry["keys"] ?: throw IOException("Missing 'key' or 'keys' field")
keys += mapper.treeToValue<SymmetricKey?>(key) ?: throw IOException("Key must be non-null")
keys += mapper.treeToValue<XteaKey?>(key) ?: throw IOException("Key must be non-null")
}
}
root.isObject -> {
for (entry in root.fields()) {
keys += mapper.treeToValue<SymmetricKey?>(entry.value) ?: throw IOException("Key must be non-null")
keys += mapper.treeToValue<XteaKey?>(entry.value) ?: throw IOException("Key must be non-null")
}
}

@ -1,15 +1,15 @@
package org.openrs2.archive.key
import io.netty.buffer.Unpooled
import jakarta.inject.Inject
import jakarta.inject.Singleton
import org.openrs2.buffer.crc32
import org.openrs2.buffer.use
import org.openrs2.cache.Js5Compression
import org.openrs2.crypto.SymmetricKey
import org.openrs2.crypto.XteaKey
import org.openrs2.db.Database
import java.sql.Connection
import java.sql.Types
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
public class KeyBruteForcer @Inject constructor(
@ -203,7 +203,7 @@ public class KeyBruteForcer @Inject constructor(
val k1 = rows.getInt(3)
val k2 = rows.getInt(4)
val k3 = rows.getInt(5)
val key = SymmetricKey(k0, k1, k2, k3)
val key = XteaKey(k0, k1, k2, k3)
validatedKey = validateKey(data, key, keyId, containerId)
if (validatedKey != null) {
@ -326,7 +326,7 @@ public class KeyBruteForcer @Inject constructor(
}
}
private fun nextKey(connection: Connection, lastKeyId: Long?): Pair<Long, SymmetricKey>? {
private fun nextKey(connection: Connection, lastKeyId: Long?): Pair<Long, XteaKey>? {
connection.prepareStatement(
"""
SELECT id, (key).k0, (key).k1, (key).k2, (key).k3
@ -350,7 +350,7 @@ public class KeyBruteForcer @Inject constructor(
val k1 = rows.getInt(3)
val k2 = rows.getInt(4)
val k3 = rows.getInt(5)
val key = SymmetricKey(k0, k1, k2, k3)
val key = XteaKey(k0, k1, k2, k3)
return Pair(keyId, key)
}
@ -359,7 +359,7 @@ public class KeyBruteForcer @Inject constructor(
private fun validateKey(
data: ByteArray,
key: SymmetricKey,
key: XteaKey,
keyId: Long,
containerId: Long
): ValidatedKey? {

@ -1,10 +1,10 @@
package org.openrs2.archive.key
import org.openrs2.crypto.SymmetricKey
import org.openrs2.crypto.XteaKey
public abstract class KeyDownloader(
public val source: KeySource
) {
public abstract suspend fun getMissingUrls(seenUrls: Set<String>): Set<String>
public abstract suspend fun download(url: String): Sequence<SymmetricKey>
public abstract suspend fun download(url: String): Sequence<XteaKey>
}

@ -1,11 +1,11 @@
package org.openrs2.archive.key
import jakarta.inject.Inject
import jakarta.inject.Singleton
import org.openrs2.crypto.SymmetricKey
import org.openrs2.crypto.XteaKey
import org.openrs2.db.Database
import java.io.BufferedOutputStream
import java.io.DataOutputStream
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
public class KeyExporter @Inject constructor(
@ -82,11 +82,11 @@ public class KeyExporter @Inject constructor(
}
}
public suspend fun exportAll(): List<SymmetricKey> {
public suspend fun exportAll(): List<XteaKey> {
return export(validOnly = false)
}
public suspend fun exportValid(): List<SymmetricKey> {
public suspend fun exportValid(): List<XteaKey> {
return export(validOnly = true)
}
@ -116,7 +116,7 @@ public class KeyExporter @Inject constructor(
return analysis
}
private suspend fun export(validOnly: Boolean): List<SymmetricKey> {
private suspend fun export(validOnly: Boolean): List<XteaKey> {
return database.execute { connection ->
val query = if (validOnly) {
EXPORT_VALID_QUERY
@ -126,14 +126,14 @@ public class KeyExporter @Inject constructor(
connection.prepareStatement(query).use { stmt ->
stmt.executeQuery().use { rows ->
val keys = mutableListOf<SymmetricKey>()
val keys = mutableListOf<XteaKey>()
while (rows.next()) {
val k0 = rows.getInt(1)
val k1 = rows.getInt(2)
val k2 = rows.getInt(3)
val k3 = rows.getInt(4)
keys += SymmetricKey(k0, k1, k2, k3)
keys += XteaKey(k0, k1, k2, k3)
}
keys

@ -1,9 +1,7 @@
package org.openrs2.archive.key
import com.github.michaelbull.logging.InlineLogger
import jakarta.inject.Inject
import jakarta.inject.Singleton
import org.openrs2.crypto.SymmetricKey
import org.openrs2.crypto.XteaKey
import org.openrs2.db.Database
import java.io.IOException
import java.nio.file.Files
@ -12,6 +10,8 @@ import java.sql.Connection
import java.sql.Types
import java.time.Instant
import java.time.ZoneOffset
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
public class KeyImporter @Inject constructor(
@ -19,10 +19,10 @@ public class KeyImporter @Inject constructor(
private val jsonKeyReader: JsonKeyReader,
private val downloaders: Set<KeyDownloader>
) {
private data class Key(val key: SymmetricKey, val source: KeySource)
private data class Key(val key: XteaKey, val source: KeySource)
public suspend fun import(path: Path) {
val keys = mutableSetOf<SymmetricKey>()
val keys = mutableSetOf<XteaKey>()
for (file in Files.walk(path)) {
if (!Files.isRegularFile(file)) {
@ -45,7 +45,7 @@ public class KeyImporter @Inject constructor(
}
}
keys -= SymmetricKey.ZERO
keys -= XteaKey.ZERO
logger.info { "Importing ${keys.size} keys" }
@ -108,7 +108,7 @@ public class KeyImporter @Inject constructor(
}
}
public suspend fun import(keys: Iterable<SymmetricKey>, source: KeySource) {
public suspend fun import(keys: Iterable<XteaKey>, source: KeySource) {
val now = Instant.now()
database.execute { connection ->

@ -1,8 +1,8 @@
package org.openrs2.archive.key
import org.openrs2.crypto.SymmetricKey
import org.openrs2.crypto.XteaKey
import java.io.InputStream
public interface KeyReader {
public fun read(input: InputStream): Sequence<SymmetricKey>
public fun read(input: InputStream): Sequence<XteaKey>
}

@ -1,7 +1,5 @@
package org.openrs2.archive.key
import jakarta.inject.Inject
import jakarta.inject.Singleton
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.future.await
import kotlinx.coroutines.withContext
@ -12,6 +10,8 @@ import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.time.Duration
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
public class RuneLiteKeyDownloader @Inject constructor(

@ -1,10 +1,10 @@
package org.openrs2.archive.key
import org.openrs2.crypto.SymmetricKey
import org.openrs2.crypto.XteaKey
import java.io.InputStream
public object TextKeyReader : KeyReader {
override fun read(input: InputStream): Sequence<SymmetricKey> {
override fun read(input: InputStream): Sequence<XteaKey> {
val reader = input.bufferedReader()
val k0 = reader.readLine()?.toIntOrNull() ?: return emptySequence()
@ -12,6 +12,6 @@ public object TextKeyReader : KeyReader {
val k2 = reader.readLine()?.toIntOrNull() ?: return emptySequence()
val k3 = reader.readLine()?.toIntOrNull() ?: return emptySequence()
return sequenceOf(SymmetricKey(k0, k1, k2, k3))
return sequenceOf(XteaKey(k0, k1, k2, k3))
}
}

@ -3,7 +3,6 @@ package org.openrs2.archive.map
import io.netty.buffer.ByteBuf
import io.netty.buffer.Unpooled
import it.unimi.dsi.fastutil.ints.Int2ObjectSortedMap
import jakarta.inject.Inject
import org.openrs2.buffer.use
import org.openrs2.cache.Group
import org.openrs2.cache.Js5Archive
@ -15,6 +14,7 @@ import java.awt.Color
import java.awt.Graphics2D
import java.awt.image.BufferedImage
import java.sql.Connection
import javax.inject.Inject
import kotlin.math.max
import kotlin.math.min

@ -1,9 +1,9 @@
package org.openrs2.archive.name
import jakarta.inject.Inject
import jakarta.inject.Singleton
import org.openrs2.db.Database
import org.openrs2.util.krHashCode
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
public class NameImporter @Inject constructor(

@ -1,7 +1,5 @@
package org.openrs2.archive.name
import jakarta.inject.Inject
import jakarta.inject.Singleton
import kotlinx.coroutines.future.await
import org.openrs2.http.checkStatusCode
import java.io.IOException
@ -10,6 +8,8 @@ import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.time.Duration
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.streams.asSequence
@Singleton

@ -18,8 +18,6 @@ import io.ktor.server.response.respondOutputStream
import io.ktor.server.thymeleaf.ThymeleafContent
import io.netty.buffer.ByteBufAllocator
import io.netty.buffer.ByteBufUtil
import jakarta.inject.Inject
import jakarta.inject.Singleton
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream
@ -39,6 +37,8 @@ import java.util.zip.Deflater
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
import javax.imageio.ImageIO
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
public class CachesController @Inject constructor(

@ -1,82 +0,0 @@
package org.openrs2.archive.web
import io.ktor.http.ContentDisposition
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.ApplicationCall
import io.ktor.server.response.header
import io.ktor.server.response.respond
import io.ktor.server.response.respondOutputStream
import io.ktor.server.thymeleaf.ThymeleafContent
import jakarta.inject.Inject
import jakarta.inject.Singleton
import org.openrs2.archive.client.ClientExporter
@Singleton
public class ClientsController @Inject constructor(
private val exporter: ClientExporter
) {
public suspend fun index(call: ApplicationCall) {
val artifacts = exporter.list()
call.respond(
ThymeleafContent(
"clients/index.html", mapOf(
"artifacts" to artifacts
)
)
)
}
public suspend fun show(call: ApplicationCall) {
val id = call.parameters["id"]?.toLongOrNull()
if (id == null) {
call.respond(HttpStatusCode.NotFound)
return
}
val artifact = exporter.get(id)
if (artifact == null) {
call.respond(HttpStatusCode.NotFound)
return
}
call.respond(
ThymeleafContent(
"clients/show.html", mapOf(
"artifact" to artifact
)
)
)
}
public suspend fun export(call: ApplicationCall) {
val id = call.parameters["id"]?.toLongOrNull()
if (id == null) {
call.respond(HttpStatusCode.NotFound)
return
}
val artifact = exporter.export(id)
if (artifact == null) {
call.respond(HttpStatusCode.NotFound)
return
}
call.response.header(
HttpHeaders.ContentLength,
artifact.summary.size.toString()
)
call.response.header(
HttpHeaders.ContentDisposition,
ContentDisposition.Attachment
.withParameter(ContentDisposition.Parameters.FileName, artifact.summary.name)
.toString()
)
call.respondOutputStream(artifact.summary.format.getContentType(artifact.summary.os)) {
artifact.content().readBytes(this, artifact.summary.size)
}
}
}

@ -5,12 +5,12 @@ import io.ktor.server.application.ApplicationCall
import io.ktor.server.request.receive
import io.ktor.server.response.respond
import io.ktor.server.thymeleaf.ThymeleafContent
import jakarta.inject.Inject
import jakarta.inject.Singleton
import org.openrs2.archive.key.KeyExporter
import org.openrs2.archive.key.KeyImporter
import org.openrs2.archive.key.KeySource
import org.openrs2.crypto.SymmetricKey
import org.openrs2.crypto.XteaKey
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
public class KeysController @Inject constructor(
@ -24,7 +24,7 @@ public class KeysController @Inject constructor(
}
public suspend fun import(call: ApplicationCall) {
val keys = call.receive<Array<IntArray>>().mapTo(mutableSetOf(), SymmetricKey::fromIntArray)
val keys = call.receive<Array<IntArray>>().mapTo(mutableSetOf(), XteaKey::fromIntArray)
if (keys.isNotEmpty()) {
importer.import(keys, KeySource.API)

@ -9,7 +9,8 @@ import io.ktor.server.application.call
import io.ktor.server.application.createApplicationPlugin
import io.ktor.server.application.install
import io.ktor.server.engine.embeddedServer
import io.ktor.server.http.content.staticResources
import io.ktor.server.http.content.resources
import io.ktor.server.http.content.static
import io.ktor.server.jetty.Jetty
import io.ktor.server.plugins.autohead.AutoHeadResponse
import io.ktor.server.plugins.cachingheaders.CachingHeaders
@ -26,17 +27,16 @@ import io.ktor.server.routing.routing
import io.ktor.server.thymeleaf.Thymeleaf
import io.ktor.server.thymeleaf.ThymeleafContent
import io.ktor.server.webjars.Webjars
import jakarta.inject.Inject
import jakarta.inject.Singleton
import org.openrs2.json.Json
import org.thymeleaf.extras.java8time.dialect.Java8TimeDialect
import org.thymeleaf.templatemode.TemplateMode
import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
public class WebServer @Inject constructor(
private val cachesController: CachesController,
private val clientsController: ClientsController,
private val keysController: KeysController,
@Json private val mapper: ObjectMapper
) {
@ -85,14 +85,11 @@ public class WebServer @Inject constructor(
get("/caches/{scope}/{id}/keys.json") { cachesController.exportKeysJson(call) }
get("/caches/{scope}/{id}/keys.zip") { cachesController.exportKeysZip(call) }
get("/caches/{scope}/{id}/map.png") { cachesController.renderMap(call) }
get("/clients") { clientsController.index(call) }
get("/clients/{id}.dat") { clientsController.export(call) }
get("/clients/{id}") { clientsController.show(call) }
get("/keys") { keysController.index(call) }
post("/keys") { keysController.import(call) }
get("/keys/all.json") { keysController.exportAll(call) }
get("/keys/valid.json") { keysController.exportValid(call) }
staticResources("/static", "/org/openrs2/archive/static")
static("/static") { resources("/org/openrs2/archive/static") }
// compatibility redirects
get("/caches/{id}") { redirect(call, permanent = true, "/caches/runescape/{id}") }

@ -1,31 +0,0 @@
package org.openrs2.archive.world
import io.netty.buffer.ByteBuf
import org.openrs2.buffer.readString
public data class World(
public val id: Int,
public val flags: Int,
public val hostname: String,
public val activity: String,
public val country: Int,
public val players: Int
) {
public val isBeta: Boolean
get() = (flags and FLAG_BETA) != 0
public companion object {
private const val FLAG_BETA = 0x10000
public fun read(buf: ByteBuf): World {
val id = buf.readUnsignedShort()
val flags = buf.readInt()
val hostname = buf.readString()
val activity = buf.readString()
val country = buf.readUnsignedByte().toInt()
val players = buf.readShort().toInt()
return World(id, flags, hostname, activity, country, players)
}
}
}

@ -1,22 +0,0 @@
package org.openrs2.archive.world
import io.netty.buffer.ByteBuf
public data class WorldList(
public val worlds: List<World>
) {
public companion object {
public fun read(buf: ByteBuf): WorldList {
buf.skipBytes(4)
val count = buf.readUnsignedShort()
val worlds = buildList(count) {
for (i in 0 until count) {
add(World.read(buf))
}
}
return WorldList(worlds)
}
}
}

@ -38,7 +38,7 @@ INSERT INTO games (id, name)
SELECT id, name
FROM game_variants;
SELECT setval('games_id_seq', MAX(id)) FROM game_variants;
SELECT setval('game_variants_id_seq', MAX(id)) FROM game_variants;
ALTER TABLE game_variants
ADD COLUMN game_id INT NULL REFERENCES games (id),

@ -1,3 +0,0 @@
-- @formatter:off
ALTER TYPE source_type ADD VALUE 'manual';

@ -1,95 +0,0 @@
-- @formatter:off
CREATE EXTENSION IF NOT EXISTS pgcrypto;
ALTER TABLE blobs ADD COLUMN sha1 BYTEA NULL;
UPDATE blobs SET sha1 = digest(data, 'sha1');
ALTER TABLE blobs ALTER COLUMN sha1 SET NOT NULL;
-- not UNIQUE as SHA-1 collisions are possible
CREATE INDEX ON blobs USING HASH (sha1);
INSERT INTO scopes (name) VALUES ('shared');
INSERT INTO games (name, scope_id) VALUES ('shared', (SELECT id FROM scopes WHERE name = 'shared'));
INSERT INTO scopes (name) VALUES ('classic');
INSERT INTO games (name, scope_id) VALUES ('classic', (SELECT id FROM scopes WHERE name = 'classic'));
INSERT INTO scopes (name) VALUES ('mapview');
INSERT INTO games (name, scope_id) VALUES ('mapview', (SELECT id FROM scopes WHERE name = 'mapview'));
CREATE TYPE artifact_type AS ENUM (
'browsercontrol',
'client',
'client_gl',
'gluegen_rt',
'jaggl',
'jaggl_dri',
'jagmisc',
'jogl',
'jogl_awt',
'loader',
'loader_gl',
'unpackclass'
);
CREATE TYPE artifact_format AS ENUM (
'cab',
'jar',
'native',
'pack200',
'packclass'
);
CREATE TYPE os AS ENUM (
'independent',
'windows',
'macos',
'linux',
'solaris'
);
CREATE TYPE arch AS ENUM (
'independent',
'universal',
'x86',
'amd64',
'powerpc',
'sparc',
'sparcv9'
);
CREATE TYPE jvm AS ENUM (
'independent',
'sun',
'microsoft'
);
CREATE TABLE artifacts (
blob_id BIGINT PRIMARY KEY NOT NULL REFERENCES blobs (id),
game_id INTEGER NOT NULL REFERENCES games (id),
environment_id INTEGER NOT NULL REFERENCES environments (id),
build_major INTEGER NULL,
build_minor INTEGER NULL,
timestamp TIMESTAMPTZ NULL,
type artifact_type NOT NULL,
format artifact_format NOT NULL,
os os NOT NULL,
arch arch NOT NULL,
jvm jvm NOT NULL
);
CREATE TABLE artifact_links (
blob_id BIGINT NOT NULL REFERENCES artifacts (blob_id),
type artifact_type NOT NULL,
format artifact_format NOT NULL,
os os NOT NULL,
arch arch NOT NULL,
jvm jvm NOT NULL,
sha1 BYTEA NOT NULL,
crc32 INTEGER NULL,
size INTEGER NULL,
PRIMARY KEY (blob_id, type, format, os, arch, jvm)
);

@ -1,11 +0,0 @@
-- @formatter:off
CREATE TABLE artifact_sources (
id SERIAL PRIMARY KEY NOT NULL,
blob_id BIGINT NOT NULL REFERENCES artifacts (blob_id),
name TEXT NULL,
description TEXT NULL,
url TEXT NULL
);
CREATE INDEX ON artifact_sources (blob_id);

@ -1,7 +0,0 @@
-- @formatter:off
INSERT INTO scopes (name) VALUES ('loginapplet');
INSERT INTO games (name, scope_id) VALUES ('loginapplet', (SELECT id FROM scopes WHERE name = 'loginapplet'));
INSERT INTO scopes (name) VALUES ('passapplet');
INSERT INTO games (name, scope_id) VALUES ('passapplet', (SELECT id FROM scopes WHERE name = 'passapplet'));

@ -1,5 +0,0 @@
-- @formatter:off
ALTER TABLE artifact_sources
ADD COLUMN file_name TEXT NULL,
ADD COLUMN timestamp TIMESTAMPTZ NULL;

@ -1,63 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head th:replace="layout.html :: head(title='Clients')">
<title>Clients - OpenRS2 Archive</title>
<link rel="stylesheet" href="/webjars/bootstrap/css/bootstrap.min.css" />
<link rel="stylesheet" href="/static/css/openrs2.css" />
<script src="/webjars/jquery/jquery.min.js" defer></script>
<script src="/webjars/bootstrap/js/bootstrap.bundle.min.js" defer></script>
</head>
<body>
<nav th:replace="layout.html :: nav(active='clients')"></nav>
<main class="container">
<h1>Clients</h1>
<div class="table-responsive">
<table class="table table-striped table-bordered table-hover" data-toggle="table" data-filter-control="true" data-sticky-header="true" data-custom-sort="customSort">
<thead class="table-dark">
<tr>
<th data-field="game" data-filter-control="select">Game</th>
<th data-field="environment" data-filter-control="select">Env</th>
<th data-field="build" data-filter-control="input" data-sortable="true">Build</th>
<th data-field="timestamp" data-sortable="true">Timestamp</th>
<th data-field="type" data-filter-control="select">Type</th>
<th data-field="format" data-filter-control="select">Format</th>
<th data-field="os" data-filter-control="select">OS</th>
<th data-field="arch" data-filter-control="select">Arch</th>
<th data-field="jvm" data-filter-control="select">JVM</th>
<th>Size</th>
<th>Links</th>
</tr>
</thead>
<tbody>
<!--/*@thymesVar id="artifacts" type="java.util.List<org.openrs2.archive.client.ClientExporter.ArtifactSummary>"*/-->
<tr th:each="artifact : ${artifacts}">
<td th:text="${artifact.game}">runescape</td>
<td th:text="${artifact.environment}">live</td>
<td th:text="${artifact.build}" class="text-end">550</td>
<td>
<span th:if="${artifact.timestamp}" th:remove="tag">
<span th:text="${#temporals.format(artifact.timestamp, 'yyyy-MM-dd')}"></span>
<br />
<span th:text="${#temporals.format(artifact.timestamp, 'HH:mm:ss')}"></span>
</span>
</td>
<td th:text="${artifact.type.toString().toLowerCase()}">client_gl</td>
<td th:text="${artifact.format.toString().toLowerCase()}">pack200</td>
<td th:text="${artifact.os.toString().toLowerCase()}">independent</td>
<td th:text="${artifact.arch.toString().toLowerCase()}">independent</td>
<td th:text="${artifact.jvm.toString().toLowerCase()}">independent</td>
<!--/*@thymesVar id="#byteunits" type="org.openrs2.archive.web.ByteUnits"*/-->
<td th:text="${#byteunits.format(artifact.size)}" class="text-end">494 KiB</td>
<td>
<div class="btn-group">
<a th:href="${'/clients/' + artifact.id + '.dat'}" class="btn btn-primary btn-sm">Download</a>
<a th:href="${'/clients/' + artifact.id}" class="btn btn-secondary btn-sm">More</a>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</main>
</body>
</html>

@ -1,152 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head th:replace="layout.html :: head(title='Cache')">
<title>Client - OpenRS2 Archive</title>
<link rel="stylesheet" href="/webjars/bootstrap/css/bootstrap.min.css" />
<link rel="stylesheet" href="/static/css/openrs2.css" />
<script src="/webjars/jquery/jquery.min.js" defer></script>
<script src="/webjars/bootstrap/js/bootstrap.bundle.min.js" defer></script>
</head>
<body>
<nav th:replace="layout.html :: nav"></nav>
<main class="container">
<h1>Client</h1>
<!--/*@thymesVar id="artifact" type="org.openrs2.archive.client.ClientExporter.Artifact"*/-->
<table class="table table-striped table-bordered table-hover">
<tr>
<th class="table-dark">Game</th>
<td th:text="${artifact.summary.game}">runescape</td>
</tr>
<tr>
<th class="table-dark">Environment</th>
<td th:text="${artifact.summary.environment}">live</td>
</tr>
<tr>
<th class="table-dark">Build</th>
<td th:text="${artifact.summary.build}">550</td>
</tr>
<tr>
<th class="table-dark">Timestamp</th>
<td th:text="${#temporals.format(artifact.summary.timestamp, 'yyyy-MM-dd HH:mm:ss')}"></td>
</tr>
<tr>
<th class="table-dark">Type</th>
<td th:text="${artifact.summary.type.toString().toLowerCase()}">client_gl</td>
</tr>
<tr>
<th class="table-dark">Format</th>
<td th:text="${artifact.summary.format.toString().toLowerCase()}">pack200</td>
</tr>
<tr>
<th class="table-dark">OS</th>
<td th:text="${artifact.summary.os.toString().toLowerCase()}">independent</td>
</tr>
<tr>
<th class="table-dark">Architecture</th>
<td th:text="${artifact.summary.arch.toString().toLowerCase()}">independent</td>
</tr>
<tr>
<th class="table-dark">JVM</th>
<td th:text="${artifact.summary.jvm.toString().toLowerCase()}">independent</td>
</tr>
<tr>
<th class="table-dark">Size</th>
<!--/*@thymesVar id="#byteunits" type="org.openrs2.archive.web.ByteUnits"*/-->
<td th:text="${#byteunits.format(artifact.summary.size)}">494 KiB</td>
</tr>
<tr>
<th class="table-dark">Checksum</th>
<td th:text="${artifact.crc32}"></td>
</tr>
<tr>
<th class="table-dark">SHA-1</th>
<td>
<code th:text="${artifact.sha1Hex}"></code>
</td>
</tr>
<tr>
<th class="table-dark">Download</th>
<td>
<a th:href="${'/clients/' + artifact.summary.id + '.dat'}" class="btn btn-primary btn-sm">Download</a>
</td>
</tr>
</table>
<h2>Sources</h2>
<div class="table-responsive">
<table class="table table-striped table-bordered table-hover">
<thead class="table-dark">
<tr>
<th>Name</th>
<th>Description</th>
<th>URL</th>
</tr>
</thead>
<tbody>
<tr th:each="source : ${artifact.sources}">
<td th:text="${source.name}">Moparisthebest</td>
<td th:text="${source.description}"></td>
<td>
<a th:href="${source.url}" th:text="${source.url}" th:if="${source.url}">https://www.example.com/</a>
</td>
</tr>
</tbody>
</table>
</div>
<div th:if="${artifact.links}" th:tag="remove">
<h2>Links</h2>
<div class="table-responsive">
<table class="table table-striped table-bordered table-hover">
<thead class="table-dark">
<tr>
<th>Build</th>
<th>Timestamp</th>
<th>Type</th>
<th>Format</th>
<th>OS</th>
<th>Arch</th>
<th>JVM</th>
<th>Checksum</th>
<th>SHA-1</th>
<th>Size</th>
<th>Links</th>
</tr>
</thead>
<tbody>
<tr th:each="entry : ${artifact.links}" th:classappend="${entry.id}? 'table-success' : 'table-danger'">
<td th:text="${entry.build}" class="text-end">550</td>
<td>
<span th:if="${entry.timestamp}" th:remove="tag">
<span th:text="${#temporals.format(entry.timestamp, 'yyyy-MM-dd')}"></span>
<br />
<span th:text="${#temporals.format(entry.timestamp, 'HH:mm:ss')}"></span>
</span>
</td>
<td th:text="${entry.link.type.toString().toLowerCase()}">client_gl</td>
<td th:text="${entry.link.format.toString().toLowerCase()}">pack200</td>
<td th:text="${entry.link.os.toString().toLowerCase()}">independent</td>
<td th:text="${entry.link.arch.toString().toLowerCase()}">independent</td>
<td th:text="${entry.link.jvm.toString().toLowerCase()}">independent</td>
<td th:text="${entry.link.crc32}"></td>
<td><code th:text="${entry.link.sha1Hex}"></code></td>
<!--/*@thymesVar id="#byteunits" type="org.openrs2.archive.web.ByteUnits"*/-->
<td th:text="${#byteunits.format(entry.link.size)}">494 KiB</td>
<td th:if="${entry.id}">
<div class="btn-group">
<a th:href="${'/clients/' + entry.id + '.dat'}" class="btn btn-primary btn-sm">Download</a>
<a th:href="${'/clients/' + entry.id}" class="btn btn-secondary btn-sm">More</a>
</div>
</td>
<td th:unless="${entry.id}"></td>
</tr>
</tbody>
</table>
</div>
</div>
</main>
</body>
</html>

@ -54,8 +54,10 @@
<li><a href="https://gregs.world/archive/">Greg's archive</a></li>
<li><a href="https://www.hdos.dev/">HDOS</a></li>
<li><a href="https://archive.runestats.com/">Polar's archive</a></li>
<li><a href="https://www.moparisthebest.com/rs/">Moparisthebest's archive</a></li>
<li><a href="https://rs-hacking.com/">RS-Hacking</a></li>
<!-- We don't use Moparisthebest's or RS-Hacking's -->
<!-- data yet, but we will once we start archiving clients. -->
<!-- <li><a href="https://www.moparisthebest.com/rs/">Moparisthebest's archive</a></li> -->
<!-- <li><a href="https://rs-hacking.com/">RS-Hacking</a></li> -->
<li><a href="https://runearchive.org/">RuneArchive</a></li>
<li><a href="https://runelite.net/">RuneLite</a></li>
<li><a href="https://rs-archive.github.io/">The RuneScape Archive</a></li>
@ -67,7 +69,7 @@
<div class="col-md-4">
<h2>Contributing</h2>
<p>
Please contact Graham in
Please contact Graham#5361 in
<a href="https://chat.openrs2.org/">OpenRS2's Discord server</a>
if you have data (old clients/gamepacks, loaders, native
libraries, caches and XTEA keys - ideally original or

@ -24,9 +24,6 @@
<li class="nav-item">
<a class="nav-link" th:classappend="${active == 'caches'}? 'active'" href="/caches">Caches</a>
</li>
<li class="nav-item">
<a class="nav-link" th:classappend="${active == 'clients'}? 'active'" href="/clients">Clients</a>
</li>
<li class="nav-item">
<a class="nav-link" th:classappend="${active == 'keys'}? 'active'" href="/keys">Keys</a>
</li>

@ -14,7 +14,6 @@ dependencies {
implementation(projects.cache)
implementation(projects.compress)
implementation(projects.crypto)
implementation(libs.cabParser)
}
publishing {

@ -1,7 +1,7 @@
package org.openrs2.asm
import com.fasterxml.jackson.databind.module.SimpleModule
import jakarta.inject.Singleton
import javax.inject.Singleton
@Singleton
public class AsmJacksonModule : SimpleModule() {

@ -1,26 +1,23 @@
package org.openrs2.asm
import org.objectweb.asm.Type
import org.objectweb.asm.tree.AbstractInsnNode
import org.objectweb.asm.tree.InsnList
import org.objectweb.asm.tree.LabelNode
import org.objectweb.asm.tree.MethodInsnNode
private val ANY_INSN = { _: AbstractInsnNode -> true }
public fun getExpression(
last: AbstractInsnNode,
filter: (AbstractInsnNode) -> Boolean = ANY_INSN,
initialHeight: Int = 0
filter: (AbstractInsnNode) -> Boolean = ANY_INSN
): List<AbstractInsnNode>? {
val expr = mutableListOf<AbstractInsnNode>()
var height = initialHeight
var height = 0
var insn: AbstractInsnNode? = last
do {
val (pops, pushes) = insn!!.stackMetadata
expr.add(insn)
if (insn !== last || initialHeight != 0) {
if (insn !== last) {
expr.add(insn)
height -= pushes
}
height += pops
@ -35,31 +32,13 @@ public fun getExpression(
return null
}
public fun getArgumentExpressions(
invoke: MethodInsnNode,
filter: (AbstractInsnNode) -> Boolean = ANY_INSN
): List<List<AbstractInsnNode>>? {
val exprs = mutableListOf<List<AbstractInsnNode>>()
var insn: AbstractInsnNode = invoke.previous ?: return null
for (type in Type.getArgumentTypes(invoke.desc)) {
val expr = getExpression(insn, filter, type.size) ?: return null
exprs += expr
insn = expr.first().previous ?: return null
}
return exprs.asReversed()
}
public fun InsnList.replaceExpression(
last: AbstractInsnNode,
replacement: AbstractInsnNode,
filter: (AbstractInsnNode) -> Boolean = ANY_INSN
): Boolean {
val expr = getExpression(last, filter) ?: return false
expr.filter { it !== last }.forEach(this::remove)
expr.forEach(this::remove)
this[last] = replacement
return true
}
@ -70,6 +49,7 @@ public fun InsnList.deleteExpression(
): Boolean {
val expr = getExpression(last, filter) ?: return false
expr.forEach(this::remove)
remove(last)
return true
}

@ -5,7 +5,6 @@ import org.objectweb.asm.tree.ClassNode
import org.openrs2.asm.io.LibraryReader
import org.openrs2.asm.io.LibraryWriter
import org.openrs2.util.io.useAtomicOutputStream
import java.io.InputStream
import java.nio.file.Files
import java.nio.file.Path
import java.util.SortedMap
@ -60,13 +59,9 @@ public class Library(public val name: String) : Iterable<ClassNode> {
public fun read(name: String, path: Path, reader: LibraryReader): Library {
logger.info { "Reading library $path" }
Files.newInputStream(path).use { input ->
return read(name, input, reader)
val classes = Files.newInputStream(path).use { input ->
reader.read(input)
}
}
public fun read(name: String, input: InputStream, reader: LibraryReader): Library {
val classes = reader.read(input)
val library = Library(name)
for (clazz in classes) {

@ -1,45 +0,0 @@
package org.openrs2.asm.io
import dorkbox.cabParser.CabParser
import dorkbox.cabParser.CabStreamSaver
import dorkbox.cabParser.structure.CabFileEntry
import org.objectweb.asm.ClassReader
import org.objectweb.asm.tree.ClassNode
import org.openrs2.asm.classpath.JsrInliner
import java.io.ByteArrayOutputStream
import java.io.InputStream
import java.io.OutputStream
public object CabLibraryReader : LibraryReader {
private const val CLASS_SUFFIX = ".class"
override fun read(input: InputStream): Iterable<ClassNode> {
val classes = mutableListOf<ClassNode>()
ByteArrayOutputStream().use { tempOutput ->
CabParser(input, object : CabStreamSaver {
override fun closeOutputStream(outputStream: OutputStream, entry: CabFileEntry) {
if (entry.name.endsWith(CLASS_SUFFIX)) {
val clazz = ClassNode()
val reader = ClassReader(tempOutput.toByteArray())
reader.accept(JsrInliner(clazz), ClassReader.SKIP_DEBUG or ClassReader.SKIP_FRAMES)
classes += clazz
}
tempOutput.reset()
}
override fun openOutputStream(entry: CabFileEntry): OutputStream {
return tempOutput
}
override fun saveReservedAreaData(data: ByteArray?, dataLength: Int): Boolean {
return false
}
}).extractStream()
}
return classes
}
}

@ -1,14 +1,14 @@
package org.openrs2.asm.io
import io.netty.buffer.ByteBufAllocator
import jakarta.inject.Inject
import jakarta.inject.Singleton
import org.objectweb.asm.tree.ClassNode
import org.openrs2.asm.packclass.ConstantPool
import org.openrs2.asm.packclass.PackClass
import org.openrs2.buffer.use
import org.openrs2.cache.Js5Pack
import java.io.InputStream
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
public class PackClassLibraryReader @Inject constructor(

@ -1,8 +1,6 @@
package org.openrs2.asm.io
import io.netty.buffer.ByteBufAllocator
import jakarta.inject.Inject
import jakarta.inject.Singleton
import org.objectweb.asm.tree.ClassNode
import org.openrs2.asm.classpath.ClassPath
import org.openrs2.asm.packclass.ConstantPool
@ -10,6 +8,8 @@ import org.openrs2.asm.packclass.PackClass
import org.openrs2.buffer.use
import org.openrs2.cache.Js5Pack
import java.io.OutputStream
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
public class PackClassLibraryWriter @Inject constructor(

@ -8,8 +8,6 @@ import org.openrs2.asm.classpath.Library
import org.openrs2.asm.hasCode
public abstract class Transformer {
public val name: String = javaClass.simpleName.removeSuffix("Transformer")
public open fun transform(classPath: ClassPath) {
preTransform(classPath)

@ -3,13 +3,13 @@ package org.openrs2.buffer
import io.netty.buffer.ByteBuf
import io.netty.buffer.ByteBufAllocator
import io.netty.buffer.Unpooled
import jakarta.inject.Inject
import jakarta.inject.Singleton
import java.net.http.HttpResponse
import java.nio.ByteBuffer
import java.util.concurrent.CompletableFuture
import java.util.concurrent.CompletionStage
import java.util.concurrent.Flow
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
public class ByteBufBodyHandler @Inject constructor(

@ -51,7 +51,8 @@ allprojects {
plugins.withType<KotlinterPlugin> {
configure<KotlinterExtension> {
ignoreFailures = true
// see https://github.com/pinterest/ktlint/issues/764
disabledRules = arrayOf("argument-list-wrapping", "parameter-list-wrapping", "wrapping")
}
}
@ -318,7 +319,7 @@ tasks.register<Exec>("publishDokka") {
"ssh -oStrictHostKeyChecking=accept-new",
"--delete",
"-rtz",
"${layout.buildDirectory.get()}/dokka/htmlCollector/",
"$buildDir/dokka/htmlCollector/",
"build@docs.openrs2.org:/srv/www/docs"
)
}

@ -1,10 +1,10 @@
package org.openrs2.cache.config.enum
import jakarta.inject.Inject
import jakarta.inject.Singleton
import org.openrs2.cache.Cache
import org.openrs2.cache.Js5Archive
import org.openrs2.cache.config.ArchiveConfigTypeList
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
public class EnumTypeList @Inject constructor(cache: Cache) : ArchiveConfigTypeList<EnumType>(

@ -1,11 +1,11 @@
package org.openrs2.cache.config.inv
import jakarta.inject.Inject
import jakarta.inject.Singleton
import org.openrs2.cache.Cache
import org.openrs2.cache.Js5Archive
import org.openrs2.cache.Js5ConfigGroup
import org.openrs2.cache.config.GroupConfigTypeList
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
public class InvTypeList @Inject constructor(cache: Cache) : GroupConfigTypeList<InvType>(

@ -1,11 +1,11 @@
package org.openrs2.cache.config.param
import jakarta.inject.Inject
import jakarta.inject.Singleton
import org.openrs2.cache.Cache
import org.openrs2.cache.Js5Archive
import org.openrs2.cache.Js5ConfigGroup
import org.openrs2.cache.config.GroupConfigTypeList
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
public class ParamTypeList @Inject constructor(cache: Cache) : GroupConfigTypeList<ParamType>(

@ -1,11 +1,11 @@
package org.openrs2.cache.config.struct
import jakarta.inject.Inject
import jakarta.inject.Singleton
import org.openrs2.cache.Cache
import org.openrs2.cache.Js5Archive
import org.openrs2.cache.Js5ConfigGroup
import org.openrs2.cache.config.GroupConfigTypeList
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
public class StructTypeList @Inject constructor(cache: Cache) : GroupConfigTypeList<StructType>(

@ -1,10 +1,10 @@
package org.openrs2.cache.config.varbit
import jakarta.inject.Inject
import jakarta.inject.Singleton
import org.openrs2.cache.Cache
import org.openrs2.cache.Js5Archive
import org.openrs2.cache.config.ArchiveConfigTypeList
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
public class VarbitTypeList @Inject constructor(cache: Cache) : ArchiveConfigTypeList<VarbitType>(

@ -1,11 +1,11 @@
package org.openrs2.cache.config.varp
import jakarta.inject.Inject
import jakarta.inject.Singleton
import org.openrs2.cache.Cache
import org.openrs2.cache.Js5Archive
import org.openrs2.cache.Js5ConfigGroup
import org.openrs2.cache.config.GroupConfigTypeList
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
public class VarpTypeList @Inject constructor(cache: Cache) : GroupConfigTypeList<VarpType>(

@ -7,7 +7,7 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectSortedMap
import it.unimi.dsi.fastutil.ints.Int2ObjectSortedMaps
import org.openrs2.buffer.crc32
import org.openrs2.buffer.use
import org.openrs2.crypto.SymmetricKey
import org.openrs2.crypto.XteaKey
import org.openrs2.crypto.whirlpool
import org.openrs2.util.krHashCode
import java.io.FileNotFoundException
@ -23,7 +23,7 @@ public abstract class Archive internal constructor(
internal inner class Unpacked(
private val entry: Js5Index.MutableGroup,
val key: SymmetricKey,
val key: XteaKey,
private var files: Int2ObjectSortedMap<ByteBuf>
) {
private var dirty = false
@ -193,7 +193,7 @@ public abstract class Archive internal constructor(
}
@JvmOverloads
public fun read(group: Int, file: Int, key: SymmetricKey = SymmetricKey.ZERO): ByteBuf {
public fun read(group: Int, file: Int, key: XteaKey = XteaKey.ZERO): ByteBuf {
require(group >= 0 && file >= 0)
val entry = index[group] ?: throw FileNotFoundException()
@ -202,19 +202,19 @@ public abstract class Archive internal constructor(
}
@JvmOverloads
public fun readNamed(groupNameHash: Int, fileNameHash: Int, key: SymmetricKey = SymmetricKey.ZERO): ByteBuf {
public fun readNamed(groupNameHash: Int, fileNameHash: Int, key: XteaKey = XteaKey.ZERO): ByteBuf {
val entry = index.getNamed(groupNameHash) ?: throw FileNotFoundException()
val unpacked = getUnpacked(entry, key)
return unpacked.readNamed(fileNameHash)
}
@JvmOverloads
public fun read(group: String, file: String, key: SymmetricKey = SymmetricKey.ZERO): ByteBuf {
public fun read(group: String, file: String, key: XteaKey = XteaKey.ZERO): ByteBuf {
return readNamed(group.krHashCode(), file.krHashCode(), key)
}
@JvmOverloads
public fun readNamedGroup(groupNameHash: Int, file: Int, key: SymmetricKey = SymmetricKey.ZERO): ByteBuf {
public fun readNamedGroup(groupNameHash: Int, file: Int, key: XteaKey = XteaKey.ZERO): ByteBuf {
require(file >= 0)
val entry = index.getNamed(groupNameHash) ?: throw FileNotFoundException()
@ -223,12 +223,12 @@ public abstract class Archive internal constructor(
}
@JvmOverloads
public fun read(group: String, file: Int, key: SymmetricKey = SymmetricKey.ZERO): ByteBuf {
public fun read(group: String, file: Int, key: XteaKey = XteaKey.ZERO): ByteBuf {
return readNamedGroup(group.krHashCode(), file, key)
}
@JvmOverloads
public fun write(group: Int, file: Int, buf: ByteBuf, key: SymmetricKey = SymmetricKey.ZERO) {
public fun write(group: Int, file: Int, buf: ByteBuf, key: XteaKey = XteaKey.ZERO) {
require(group >= 0 && file >= 0)
val entry = index.createOrGet(group)
@ -239,7 +239,7 @@ public abstract class Archive internal constructor(
}
@JvmOverloads
public fun writeNamed(groupNameHash: Int, fileNameHash: Int, buf: ByteBuf, key: SymmetricKey = SymmetricKey.ZERO) {
public fun writeNamed(groupNameHash: Int, fileNameHash: Int, buf: ByteBuf, key: XteaKey = XteaKey.ZERO) {
val entry = index.createOrGetNamed(groupNameHash)
val unpacked = createOrGetUnpacked(entry, key, isOverwritingNamed(entry, fileNameHash))
unpacked.writeNamed(fileNameHash, buf)
@ -249,12 +249,12 @@ public abstract class Archive internal constructor(
}
@JvmOverloads
public fun write(group: String, file: String, buf: ByteBuf, key: SymmetricKey = SymmetricKey.ZERO) {
public fun write(group: String, file: String, buf: ByteBuf, key: XteaKey = XteaKey.ZERO) {
return writeNamed(group.krHashCode(), file.krHashCode(), buf, key)
}
@JvmOverloads
public fun writeNamedGroup(groupNameHash: Int, file: Int, buf: ByteBuf, key: SymmetricKey = SymmetricKey.ZERO) {
public fun writeNamedGroup(groupNameHash: Int, file: Int, buf: ByteBuf, key: XteaKey = XteaKey.ZERO) {
require(file >= 0)
val entry = index.createOrGetNamed(groupNameHash)
@ -266,7 +266,7 @@ public abstract class Archive internal constructor(
}
@JvmOverloads
public fun write(group: String, file: Int, buf: ByteBuf, key: SymmetricKey = SymmetricKey.ZERO) {
public fun write(group: String, file: Int, buf: ByteBuf, key: XteaKey = XteaKey.ZERO) {
return writeNamedGroup(group.krHashCode(), file, buf, key)
}
@ -293,7 +293,7 @@ public abstract class Archive internal constructor(
}
@JvmOverloads
public fun remove(group: Int, file: Int, key: SymmetricKey = SymmetricKey.ZERO) {
public fun remove(group: Int, file: Int, key: XteaKey = XteaKey.ZERO) {
require(group >= 0 && file >= 0)
val entry = index[group] ?: return
@ -310,7 +310,7 @@ public abstract class Archive internal constructor(
}
@JvmOverloads
public fun removeNamed(groupNameHash: Int, fileNameHash: Int, key: SymmetricKey = SymmetricKey.ZERO) {
public fun removeNamed(groupNameHash: Int, fileNameHash: Int, key: XteaKey = XteaKey.ZERO) {
val entry = index.getNamed(groupNameHash) ?: return
if (isOverwritingNamed(entry, fileNameHash)) {
@ -325,12 +325,12 @@ public abstract class Archive internal constructor(
}
@JvmOverloads
public fun remove(group: String, file: String, key: SymmetricKey = SymmetricKey.ZERO) {
public fun remove(group: String, file: String, key: XteaKey = XteaKey.ZERO) {
return removeNamed(group.krHashCode(), file.krHashCode(), key)
}
@JvmOverloads
public fun removeNamedGroup(groupNameHash: Int, file: Int, key: SymmetricKey = SymmetricKey.ZERO) {
public fun removeNamedGroup(groupNameHash: Int, file: Int, key: XteaKey = XteaKey.ZERO) {
require(file >= 0)
val entry = index.getNamed(groupNameHash) ?: return
@ -347,7 +347,7 @@ public abstract class Archive internal constructor(
}
@JvmOverloads
public fun remove(group: String, file: Int, key: SymmetricKey = SymmetricKey.ZERO) {
public fun remove(group: String, file: Int, key: XteaKey = XteaKey.ZERO) {
removeNamedGroup(group.krHashCode(), file, key)
}
@ -388,7 +388,7 @@ public abstract class Archive internal constructor(
return fileEntry.nameHash == fileNameHash
}
private fun createOrGetUnpacked(entry: Js5Index.MutableGroup, key: SymmetricKey, overwrite: Boolean): Unpacked {
private fun createOrGetUnpacked(entry: Js5Index.MutableGroup, key: XteaKey, overwrite: Boolean): Unpacked {
return if (entry.size == 0 || overwrite) {
val unpacked = Unpacked(entry, key, Int2ObjectAVLTreeMap())
unpackedCache.put(archive, entry.id, unpacked)
@ -398,7 +398,7 @@ public abstract class Archive internal constructor(
}
}
private fun getUnpacked(entry: Js5Index.MutableGroup, key: SymmetricKey): Unpacked {
private fun getUnpacked(entry: Js5Index.MutableGroup, key: XteaKey): Unpacked {
var unpacked = unpackedCache.get(archive, entry.id)
if (unpacked != null) {
/*

@ -3,7 +3,7 @@ package org.openrs2.cache
import io.netty.buffer.ByteBuf
import io.netty.buffer.ByteBufAllocator
import org.openrs2.buffer.use
import org.openrs2.crypto.SymmetricKey
import org.openrs2.crypto.XteaKey
import org.openrs2.util.krHashCode
import java.io.Closeable
import java.io.FileNotFoundException
@ -142,45 +142,35 @@ public class Cache private constructor(
}
@JvmOverloads
public fun read(archive: Int, group: Int, file: Int, key: SymmetricKey = SymmetricKey.ZERO): ByteBuf {
public fun read(archive: Int, group: Int, file: Int, key: XteaKey = XteaKey.ZERO): ByteBuf {
checkArchive(archive)
return archives[archive]?.read(group, file, key) ?: throw FileNotFoundException()
}
@JvmOverloads
public fun readNamed(
archive: Int,
groupNameHash: Int,
fileNameHash: Int,
key: SymmetricKey = SymmetricKey.ZERO
): ByteBuf {
public fun readNamed(archive: Int, groupNameHash: Int, fileNameHash: Int, key: XteaKey = XteaKey.ZERO): ByteBuf {
checkArchive(archive)
return archives[archive]?.readNamed(groupNameHash, fileNameHash, key) ?: throw FileNotFoundException()
}
@JvmOverloads
public fun read(archive: Int, group: String, file: String, key: SymmetricKey = SymmetricKey.ZERO): ByteBuf {
public fun read(archive: Int, group: String, file: String, key: XteaKey = XteaKey.ZERO): ByteBuf {
return readNamed(archive, group.krHashCode(), file.krHashCode(), key)
}
@JvmOverloads
public fun readNamedGroup(
archive: Int,
groupNameHash: Int,
file: Int,
key: SymmetricKey = SymmetricKey.ZERO
): ByteBuf {
public fun readNamedGroup(archive: Int, groupNameHash: Int, file: Int, key: XteaKey = XteaKey.ZERO): ByteBuf {
checkArchive(archive)
return archives[archive]?.readNamedGroup(groupNameHash, file, key) ?: throw FileNotFoundException()
}
@JvmOverloads
public fun read(archive: Int, group: String, file: Int, key: SymmetricKey = SymmetricKey.ZERO): ByteBuf {
public fun read(archive: Int, group: String, file: Int, key: XteaKey = XteaKey.ZERO): ByteBuf {
return readNamedGroup(archive, group.krHashCode(), file, key)
}
@JvmOverloads
public fun write(archive: Int, group: Int, file: Int, buf: ByteBuf, key: SymmetricKey = SymmetricKey.ZERO) {
public fun write(archive: Int, group: Int, file: Int, buf: ByteBuf, key: XteaKey = XteaKey.ZERO) {
checkArchive(archive)
createOrGetArchive(archive).write(group, file, buf, key)
}
@ -191,31 +181,25 @@ public class Cache private constructor(
groupNameHash: Int,
fileNameHash: Int,
buf: ByteBuf,
key: SymmetricKey = SymmetricKey.ZERO
key: XteaKey = XteaKey.ZERO
) {
checkArchive(archive)
createOrGetArchive(archive).writeNamed(groupNameHash, fileNameHash, buf, key)
}
@JvmOverloads
public fun write(archive: Int, group: String, file: String, buf: ByteBuf, key: SymmetricKey = SymmetricKey.ZERO) {
public fun write(archive: Int, group: String, file: String, buf: ByteBuf, key: XteaKey = XteaKey.ZERO) {
writeNamed(archive, group.krHashCode(), file.krHashCode(), buf, key)
}
@JvmOverloads
public fun writeNamedGroup(
archive: Int,
groupNameHash: Int,
file: Int,
buf: ByteBuf,
key: SymmetricKey = SymmetricKey.ZERO
) {
public fun writeNamedGroup(archive: Int, groupNameHash: Int, file: Int, buf: ByteBuf, key: XteaKey = XteaKey.ZERO) {
checkArchive(archive)
createOrGetArchive(archive).writeNamedGroup(groupNameHash, file, buf, key)
}
@JvmOverloads
public fun write(archive: Int, group: String, file: Int, buf: ByteBuf, key: SymmetricKey = SymmetricKey.ZERO) {
public fun write(archive: Int, group: String, file: Int, buf: ByteBuf, key: XteaKey = XteaKey.ZERO) {
writeNamedGroup(archive, group.krHashCode(), file, buf, key)
}
@ -249,30 +233,30 @@ public class Cache private constructor(
}
@JvmOverloads
public fun remove(archive: Int, group: Int, file: Int, key: SymmetricKey = SymmetricKey.ZERO) {
public fun remove(archive: Int, group: Int, file: Int, key: XteaKey = XteaKey.ZERO) {
checkArchive(archive)
archives[archive]?.remove(group, file, key)
}
@JvmOverloads
public fun removeNamed(archive: Int, groupNameHash: Int, fileNameHash: Int, key: SymmetricKey = SymmetricKey.ZERO) {
public fun removeNamed(archive: Int, groupNameHash: Int, fileNameHash: Int, key: XteaKey = XteaKey.ZERO) {
checkArchive(archive)
archives[archive]?.removeNamed(groupNameHash, fileNameHash, key)
}
@JvmOverloads
public fun remove(archive: Int, group: String, file: String, key: SymmetricKey = SymmetricKey.ZERO) {
public fun remove(archive: Int, group: String, file: String, key: XteaKey = XteaKey.ZERO) {
return removeNamed(archive, group.krHashCode(), file.krHashCode(), key)
}
@JvmOverloads
public fun removeNamedGroup(archive: Int, groupNameHash: Int, file: Int, key: SymmetricKey = SymmetricKey.ZERO) {
public fun removeNamedGroup(archive: Int, groupNameHash: Int, file: Int, key: XteaKey = XteaKey.ZERO) {
checkArchive(archive)
archives[archive]?.removeNamedGroup(groupNameHash, file, key)
}
@JvmOverloads
public fun remove(archive: Int, group: String, file: Int, key: SymmetricKey = SymmetricKey.ZERO) {
public fun remove(archive: Int, group: String, file: Int, key: XteaKey = XteaKey.ZERO) {
removeNamedGroup(archive, group.krHashCode(), file, key)
}

@ -4,7 +4,7 @@ import io.netty.buffer.ByteBuf
import io.netty.buffer.ByteBufInputStream
import io.netty.buffer.ByteBufOutputStream
import org.openrs2.buffer.use
import org.openrs2.crypto.SymmetricKey
import org.openrs2.crypto.XteaKey
import org.openrs2.crypto.xteaDecrypt
import org.openrs2.crypto.xteaEncrypt
import java.io.IOException
@ -19,7 +19,7 @@ public object Js5Compression {
private const val LZMA_PRESET_DICT_SIZE_MAX = 1 shl 26
@JvmOverloads
public fun compress(input: ByteBuf, type: Js5CompressionType, key: SymmetricKey = SymmetricKey.ZERO): ByteBuf {
public fun compress(input: ByteBuf, type: Js5CompressionType, key: XteaKey = XteaKey.ZERO): ByteBuf {
input.alloc().buffer().use { output ->
output.writeByte(type.ordinal)
@ -62,7 +62,7 @@ public object Js5Compression {
input: ByteBuf,
enableLzma: Boolean = false,
enableUncompressedEncryption: Boolean = false,
key: SymmetricKey = SymmetricKey.ZERO
key: XteaKey = XteaKey.ZERO
): ByteBuf {
val types = mutableListOf(Js5CompressionType.BZIP2, Js5CompressionType.GZIP)
if (enableLzma) {
@ -109,7 +109,7 @@ public object Js5Compression {
}
@JvmOverloads
public fun uncompress(input: ByteBuf, key: SymmetricKey = SymmetricKey.ZERO): ByteBuf {
public fun uncompress(input: ByteBuf, key: XteaKey = XteaKey.ZERO): ByteBuf {
if (input.readableBytes() < 5) {
throw IOException("Missing header")
}
@ -169,10 +169,10 @@ public object Js5Compression {
}
public fun uncompressUnlessEncrypted(input: ByteBuf): ByteBuf? {
return uncompressIfKeyValid(input, SymmetricKey.ZERO)
return uncompressIfKeyValid(input, XteaKey.ZERO)
}
public fun uncompressIfKeyValid(input: ByteBuf, key: SymmetricKey): ByteBuf? {
public fun uncompressIfKeyValid(input: ByteBuf, key: XteaKey): ByteBuf? {
if (input.readableBytes() < 5) {
throw IOException("Missing header")
}
@ -187,9 +187,8 @@ public object Js5Compression {
}
if (type == Js5CompressionType.UNCOMPRESSED) {
val n = input.readableBytes()
if (n < len) {
throw IOException("Data truncated ($n bytes, expecting $len)")
if (input.readableBytes() < len) {
throw IOException("Data truncated")
}
/*
@ -214,9 +213,8 @@ public object Js5Compression {
}
val lenWithUncompressedLen = len + 4
val n = input.readableBytes()
if (n < lenWithUncompressedLen) {
throw IOException("Compressed data truncated ($n bytes, expecting $lenWithUncompressedLen)")
if (input.readableBytes() < lenWithUncompressedLen) {
throw IOException("Compressed data truncated")
}
/*
@ -360,7 +358,7 @@ public object Js5Compression {
}
}
private fun decrypt(buf: ByteBuf, len: Int, key: SymmetricKey): ByteBuf {
private fun decrypt(buf: ByteBuf, len: Int, key: XteaKey): ByteBuf {
if (key.isZero) {
return buf.readRetainedSlice(len)
}

@ -141,7 +141,7 @@ public data class Js5MasterIndex(
}
val version = index.version
val groups = index.capacity
val groups = index.size
val totalUncompressedLength = index.sumOf(Js5Index.Group<*>::uncompressedLength)
// TODO(gpe): should we throw an exception if there are trailing bytes here or in the block above?

@ -2,13 +2,13 @@ package org.openrs2.cache
import io.netty.buffer.ByteBufAllocator
import io.netty.buffer.Unpooled
import jakarta.inject.Inject
import jakarta.inject.Singleton
import org.openrs2.buffer.crc32
import org.openrs2.buffer.use
import java.nio.file.Files
import java.nio.file.Path
import java.util.Base64
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.io.path.name
@Singleton

@ -4,7 +4,7 @@ import io.netty.buffer.ByteBuf
import io.netty.buffer.Unpooled
import org.openrs2.buffer.copiedBuffer
import org.openrs2.buffer.use
import org.openrs2.crypto.SymmetricKey
import org.openrs2.crypto.XteaKey
import java.io.IOException
import kotlin.test.Test
import kotlin.test.assertEquals
@ -276,7 +276,7 @@ class Js5CompressionTest {
}
assertFailsWith<IOException> {
Js5Compression.uncompressIfKeyValid(compressed.slice(), SymmetricKey.ZERO)?.release()
Js5Compression.uncompressIfKeyValid(compressed.slice(), XteaKey.ZERO)?.release()
}
}
}
@ -289,7 +289,7 @@ class Js5CompressionTest {
}
assertFailsWith<IOException> {
Js5Compression.uncompressIfKeyValid(compressed.slice(), SymmetricKey.ZERO)?.release()
Js5Compression.uncompressIfKeyValid(compressed.slice(), XteaKey.ZERO)?.release()
}
}
}
@ -301,7 +301,7 @@ class Js5CompressionTest {
Js5Compression.uncompress(compressed.slice()).release()
}
Js5Compression.uncompressIfKeyValid(compressed.slice(), SymmetricKey.ZERO).use { uncompressed ->
Js5Compression.uncompressIfKeyValid(compressed.slice(), XteaKey.ZERO).use { uncompressed ->
assertNull(uncompressed)
}
}
@ -315,7 +315,7 @@ class Js5CompressionTest {
}
assertFailsWith<IOException> {
Js5Compression.uncompressIfKeyValid(compressed.slice(), SymmetricKey.ZERO)?.release()
Js5Compression.uncompressIfKeyValid(compressed.slice(), XteaKey.ZERO)?.release()
}
}
}
@ -327,7 +327,7 @@ class Js5CompressionTest {
Js5Compression.uncompress(compressed.slice()).release()
}
Js5Compression.uncompressIfKeyValid(compressed.slice(), SymmetricKey.ZERO).use { uncompressed ->
Js5Compression.uncompressIfKeyValid(compressed.slice(), XteaKey.ZERO).use { uncompressed ->
assertNull(uncompressed)
}
}
@ -340,7 +340,7 @@ class Js5CompressionTest {
Js5Compression.uncompress(compressed.slice()).release()
}
Js5Compression.uncompressIfKeyValid(compressed.slice(), SymmetricKey.ZERO).use { uncompressed ->
Js5Compression.uncompressIfKeyValid(compressed.slice(), XteaKey.ZERO).use { uncompressed ->
assertNull(uncompressed)
}
}
@ -353,7 +353,7 @@ class Js5CompressionTest {
Js5Compression.uncompress(compressed.slice()).release()
}
Js5Compression.uncompressIfKeyValid(compressed.slice(), SymmetricKey.ZERO).use { uncompressed ->
Js5Compression.uncompressIfKeyValid(compressed.slice(), XteaKey.ZERO).use { uncompressed ->
assertNull(uncompressed)
}
}
@ -366,7 +366,7 @@ class Js5CompressionTest {
Js5Compression.uncompress(compressed.slice()).release()
}
Js5Compression.uncompressIfKeyValid(compressed.slice(), SymmetricKey.ZERO).use { uncompressed ->
Js5Compression.uncompressIfKeyValid(compressed.slice(), XteaKey.ZERO).use { uncompressed ->
assertNull(uncompressed)
}
}
@ -379,7 +379,7 @@ class Js5CompressionTest {
Js5Compression.uncompress(compressed.slice()).release()
}
Js5Compression.uncompressIfKeyValid(compressed.slice(), SymmetricKey.ZERO).use { uncompressed ->
Js5Compression.uncompressIfKeyValid(compressed.slice(), XteaKey.ZERO).use { uncompressed ->
assertNull(uncompressed)
}
}
@ -392,7 +392,7 @@ class Js5CompressionTest {
Js5Compression.uncompress(compressed.slice()).release()
}
Js5Compression.uncompressIfKeyValid(compressed.slice(), SymmetricKey.ZERO).use { uncompressed ->
Js5Compression.uncompressIfKeyValid(compressed.slice(), XteaKey.ZERO).use { uncompressed ->
assertNull(uncompressed)
}
}
@ -406,7 +406,7 @@ class Js5CompressionTest {
assertEquals(expected, actual)
}
Js5Compression.uncompressIfKeyValid(compressed.slice(), SymmetricKey.ZERO).use { actual ->
Js5Compression.uncompressIfKeyValid(compressed.slice(), XteaKey.ZERO).use { actual ->
assertEquals(expected, actual)
}
@ -429,7 +429,7 @@ class Js5CompressionTest {
assertEquals(expected, actual)
}
Js5Compression.uncompressIfKeyValid(compressed.slice(), SymmetricKey.ZERO).use { actual ->
Js5Compression.uncompressIfKeyValid(compressed.slice(), XteaKey.ZERO).use { actual ->
assertEquals(expected, actual)
}
@ -447,7 +447,7 @@ class Js5CompressionTest {
assertNull(actual)
}
Js5Compression.uncompressIfKeyValid(compressed.slice(), SymmetricKey.ZERO).use { actual ->
Js5Compression.uncompressIfKeyValid(compressed.slice(), XteaKey.ZERO).use { actual ->
assertNull(actual)
}
@ -462,7 +462,7 @@ class Js5CompressionTest {
}
read("bzip2-invalid-magic.dat").use { compressed ->
Js5Compression.uncompressIfKeyValid(compressed, SymmetricKey.ZERO).use { actual ->
Js5Compression.uncompressIfKeyValid(compressed, XteaKey.ZERO).use { actual ->
assertNull(actual)
}
}
@ -476,7 +476,7 @@ class Js5CompressionTest {
assertEquals(expected, actual)
}
Js5Compression.uncompressIfKeyValid(compressed.slice(), SymmetricKey.ZERO).use { actual ->
Js5Compression.uncompressIfKeyValid(compressed.slice(), XteaKey.ZERO).use { actual ->
assertEquals(expected, actual)
}
@ -494,7 +494,7 @@ class Js5CompressionTest {
assertNull(actual)
}
Js5Compression.uncompressIfKeyValid(compressed.slice(), SymmetricKey.ZERO).use { actual ->
Js5Compression.uncompressIfKeyValid(compressed.slice(), XteaKey.ZERO).use { actual ->
assertNull(actual)
}
@ -509,13 +509,13 @@ class Js5CompressionTest {
}
read("gzip-invalid-magic.dat").use { compressed ->
Js5Compression.uncompressIfKeyValid(compressed, SymmetricKey.ZERO).use { actual ->
Js5Compression.uncompressIfKeyValid(compressed, XteaKey.ZERO).use { actual ->
assertNull(actual)
}
}
read("gzip-invalid-method.dat").use { compressed ->
Js5Compression.uncompressIfKeyValid(compressed, SymmetricKey.ZERO).use { actual ->
Js5Compression.uncompressIfKeyValid(compressed, XteaKey.ZERO).use { actual ->
assertNull(actual)
}
}
@ -529,7 +529,7 @@ class Js5CompressionTest {
assertEquals(expected, actual)
}
Js5Compression.uncompressIfKeyValid(compressed.slice(), SymmetricKey.ZERO).use { actual ->
Js5Compression.uncompressIfKeyValid(compressed.slice(), XteaKey.ZERO).use { actual ->
assertEquals(expected, actual)
}
@ -547,7 +547,7 @@ class Js5CompressionTest {
assertNull(actual)
}
Js5Compression.uncompressIfKeyValid(compressed.slice(), SymmetricKey.ZERO).use { actual ->
Js5Compression.uncompressIfKeyValid(compressed.slice(), XteaKey.ZERO).use { actual ->
assertNull(actual)
}
@ -562,19 +562,19 @@ class Js5CompressionTest {
}
read("lzma-dict-size-negative.dat").use { compressed ->
Js5Compression.uncompressIfKeyValid(compressed, SymmetricKey.ZERO).use { actual ->
Js5Compression.uncompressIfKeyValid(compressed, XteaKey.ZERO).use { actual ->
assertNull(actual)
}
}
read("lzma-dict-size-larger-than-preset.dat").use { compressed ->
Js5Compression.uncompressIfKeyValid(compressed, SymmetricKey.ZERO).use { actual ->
Js5Compression.uncompressIfKeyValid(compressed, XteaKey.ZERO).use { actual ->
assertNull(actual)
}
}
read("lzma-invalid-pb.dat").use { compressed ->
Js5Compression.uncompressIfKeyValid(compressed, SymmetricKey.ZERO).use { actual ->
Js5Compression.uncompressIfKeyValid(compressed, XteaKey.ZERO).use { actual ->
assertNull(actual)
}
}
@ -584,7 +584,7 @@ class Js5CompressionTest {
fun testKeyValidShorterThanTwoBlocks() {
read("shorter-than-two-blocks.dat").use { compressed ->
assertFailsWith<IOException> {
Js5Compression.uncompressIfKeyValid(compressed, SymmetricKey.ZERO)?.release()
Js5Compression.uncompressIfKeyValid(compressed, XteaKey.ZERO)?.release()
}
}
}
@ -597,7 +597,7 @@ class Js5CompressionTest {
}
assertFailsWith<IOException> {
Js5Compression.uncompressIfKeyValid(compressed.slice(), SymmetricKey.ZERO)?.release()
Js5Compression.uncompressIfKeyValid(compressed.slice(), XteaKey.ZERO)?.release()
}
}
}
@ -609,7 +609,7 @@ class Js5CompressionTest {
Js5Compression.uncompress(compressed.slice()).release()
}
Js5Compression.uncompressIfKeyValid(compressed.slice(), SymmetricKey.ZERO).use { actual ->
Js5Compression.uncompressIfKeyValid(compressed.slice(), XteaKey.ZERO).use { actual ->
assertNull(actual)
}
}
@ -622,7 +622,7 @@ class Js5CompressionTest {
Js5Compression.uncompress(compressed.slice()).release()
}
Js5Compression.uncompressIfKeyValid(compressed.slice(), SymmetricKey.ZERO).use { actual ->
Js5Compression.uncompressIfKeyValid(compressed.slice(), XteaKey.ZERO).use { actual ->
assertNull(actual)
}
}
@ -719,7 +719,7 @@ class Js5CompressionTest {
read("missing-header.dat").use { compressed ->
assertFailsWith<IOException> {
Js5Compression.uncompressIfKeyValid(compressed, SymmetricKey.ZERO)
Js5Compression.uncompressIfKeyValid(compressed, XteaKey.ZERO)
}
}
@ -737,7 +737,7 @@ class Js5CompressionTest {
}
private companion object {
private val KEY = SymmetricKey.fromHex("00112233445566778899AABBCCDDEEFF")
private val INVALID_KEY = SymmetricKey.fromHex("0123456789ABCDEF0123456789ABCDEF")
private val KEY = XteaKey.fromHex("00112233445566778899AABBCCDDEEFF")
private val INVALID_KEY = XteaKey.fromHex("0123456789ABCDEF0123456789ABCDEF")
}
}

@ -114,9 +114,9 @@ class Js5MasterIndexTest {
MasterIndexFormat.LENGTHS,
mutableListOf(
Js5MasterIndex.Entry(
0x12345678, -817299640, 13, 123, ByteBufUtil.decodeHexDump(
"7c2eb43256e9350b637d0ca819813fa3397bc6035a72c6c7e41b714b74301c7e" +
"bba0e6b5408cb2514656181b75f2040397e4eb6944456ce60ca337a32a1c4f05"
0x12345678, -1080883457, 3, 123, ByteBufUtil.decodeHexDump(
"0bf30b80b7213154ada5c3797be15a8fbb6a96a80432e2093e10617bcb4e67de" +
"9a858211cabe844c6fa3a1fbfe3164a3e4e1918983c69597dff3fc3c53096884"
)
)
)

@ -7,7 +7,7 @@ import com.github.ajalt.clikt.parameters.types.defaultStdout
import com.github.ajalt.clikt.parameters.types.inputStream
import com.github.ajalt.clikt.parameters.types.outputStream
import org.openrs2.compress.gzip.Gzip
import java.util.jar.JarOutputStream
import org.openrs2.util.io.DeterministicJarOutputStream
import java.util.jar.Pack200
public class Unpack200Command : CliktCommand(name = "unpack200") {
@ -16,7 +16,7 @@ public class Unpack200Command : CliktCommand(name = "unpack200") {
override fun run() {
Gzip.createHeaderlessInputStream(input).use { gzipInput ->
JarOutputStream(output).use { jarOutput ->
DeterministicJarOutputStream(output).use { jarOutput ->
Pack200.newUnpacker().unpack(gzipInput, jarOutput)
}
}

@ -11,8 +11,7 @@ public data class Config(
val members: Boolean,
val quickChat: Boolean,
val pvp: Boolean,
val lootShare: Boolean,
val dedicatedActivity: Boolean
val lootShare: Boolean
) {
val internalGame: String = game.toInternalName()
val internalOperator: String = operator.toInternalName()

@ -1,11 +1,11 @@
package org.openrs2.conf
import com.fasterxml.jackson.databind.ObjectMapper
import jakarta.inject.Inject
import jakarta.inject.Provider
import org.openrs2.yaml.Yaml
import java.nio.file.Files
import java.nio.file.Path
import javax.inject.Inject
import javax.inject.Provider
public class ConfigProvider @Inject constructor(@Yaml private val mapper: ObjectMapper) : Provider<Config> {
override fun get(): Config {

@ -1,10 +1,10 @@
package org.openrs2.conf
import jakarta.inject.Provider
import org.bouncycastle.crypto.params.RSAPrivateCrtKeyParameters
import org.openrs2.crypto.Rsa
import java.nio.file.Files
import java.nio.file.Path
import javax.inject.Provider
public class RsaKeyProvider : Provider<RSAPrivateCrtKeyParameters> {
override fun get(): RSAPrivateCrtKeyParameters {

@ -1,12 +1,12 @@
package org.openrs2.crypto
import com.fasterxml.jackson.databind.module.SimpleModule
import jakarta.inject.Singleton
import javax.inject.Singleton
@Singleton
public class CryptoJacksonModule : SimpleModule() {
init {
addDeserializer(SymmetricKey::class.java, SymmetricKeyDeserializer)
addSerializer(SymmetricKey::class.java, SymmetricKeySerializer)
addDeserializer(XteaKey::class.java, XteaKeyDeserializer)
addSerializer(XteaKey::class.java, XteaKeySerializer)
}
}

@ -1,19 +0,0 @@
package org.openrs2.crypto
import io.netty.buffer.ByteBuf
import io.netty.buffer.ByteBufUtil
import java.security.MessageDigest
public fun ByteBuf.sha1(): ByteArray {
return sha1(readerIndex(), readableBytes())
}
public fun ByteBuf.sha1(index: Int, len: Int): ByteArray {
val digest = MessageDigest.getInstance("SHA-1")
if (hasArray()) {
digest.update(array(), arrayOffset() + index, len)
} else {
digest.update(ByteBufUtil.getBytes(this, index, len, false))
}
return digest.digest()
}

@ -1,69 +0,0 @@
package org.openrs2.crypto
import java.security.SecureRandom
public data class SymmetricKey(
public val k0: Int,
public val k1: Int,
public val k2: Int,
public val k3: Int
) {
public val isZero: Boolean
get() = k0 == 0 && k1 == 0 && k2 == 0 && k3 == 0
public fun toIntArray(): IntArray {
return intArrayOf(k0, k1, k2, k3)
}
public fun toHex(): String {
return Integer.toUnsignedString(k0, 16).padStart(8, '0') +
Integer.toUnsignedString(k1, 16).padStart(8, '0') +
Integer.toUnsignedString(k2, 16).padStart(8, '0') +
Integer.toUnsignedString(k3, 16).padStart(8, '0')
}
override fun toString(): String {
return toHex()
}
public companion object {
@JvmStatic
public val ZERO: SymmetricKey = SymmetricKey(0, 0, 0, 0)
@JvmStatic
@JvmOverloads
public fun generate(r: SecureRandom = secureRandom): SymmetricKey {
return SymmetricKey(r.nextInt(), r.nextInt(), r.nextInt(), r.nextInt())
}
@JvmStatic
public fun fromIntArray(a: IntArray): SymmetricKey {
require(a.size == 4)
return SymmetricKey(a[0], a[1], a[2], a[3])
}
@JvmStatic
public fun fromHex(s: String): SymmetricKey {
return fromHexOrNull(s) ?: throw IllegalArgumentException()
}
@JvmStatic
public fun fromHexOrNull(s: String): SymmetricKey? {
if (s.length != 32) {
return null
}
return try {
val k0 = Integer.parseUnsignedInt(s, 0, 8, 16)
val k1 = Integer.parseUnsignedInt(s, 8, 16, 16)
val k2 = Integer.parseUnsignedInt(s, 16, 24, 16)
val k3 = Integer.parseUnsignedInt(s, 24, 32, 16)
SymmetricKey(k0, k1, k2, k3)
} catch (ex: NumberFormatException) {
null
}
}
}
}

@ -281,7 +281,7 @@ public class Whirlpool {
val whirlpool = Whirlpool()
whirlpool.NESSIEinit()
whirlpool.NESSIEadd(source, len.toLong() * 8)
whirlpool.NESSIEadd(source, (len * 8).toLong())
val digest = ByteArray(DIGESTBYTES)
whirlpool.NESSIEfinalize(digest)

@ -1,13 +1,80 @@
package org.openrs2.crypto
import io.netty.buffer.ByteBuf
import java.security.SecureRandom
private const val GOLDEN_RATIO = 0x9E3779B9.toInt()
private const val ROUNDS = 32
public const val XTEA_BLOCK_SIZE: Int = 8
private const val BLOCK_SIZE_MASK = XTEA_BLOCK_SIZE - 1
public fun ByteBuf.xteaEncrypt(index: Int, length: Int, key: SymmetricKey) {
public data class XteaKey(
public val k0: Int,
public val k1: Int,
public val k2: Int,
public val k3: Int
) {
public val isZero: Boolean
get() = k0 == 0 && k1 == 0 && k2 == 0 && k3 == 0
public fun toIntArray(): IntArray {
return intArrayOf(k0, k1, k2, k3)
}
public fun toHex(): String {
return Integer.toUnsignedString(k0, 16).padStart(8, '0') +
Integer.toUnsignedString(k1, 16).padStart(8, '0') +
Integer.toUnsignedString(k2, 16).padStart(8, '0') +
Integer.toUnsignedString(k3, 16).padStart(8, '0')
}
override fun toString(): String {
return toHex()
}
public companion object {
@JvmStatic
public val ZERO: XteaKey = XteaKey(0, 0, 0, 0)
@JvmStatic
@JvmOverloads
public fun generate(r: SecureRandom = secureRandom): XteaKey {
return XteaKey(r.nextInt(), r.nextInt(), r.nextInt(), r.nextInt())
}
@JvmStatic
public fun fromIntArray(a: IntArray): XteaKey {
require(a.size == 4)
return XteaKey(a[0], a[1], a[2], a[3])
}
@JvmStatic
public fun fromHex(s: String): XteaKey {
return fromHexOrNull(s) ?: throw IllegalArgumentException()
}
@JvmStatic
public fun fromHexOrNull(s: String): XteaKey? {
if (s.length != 32) {
return null
}
return try {
val k0 = Integer.parseUnsignedInt(s, 0, 8, 16)
val k1 = Integer.parseUnsignedInt(s, 8, 16, 16)
val k2 = Integer.parseUnsignedInt(s, 16, 24, 16)
val k3 = Integer.parseUnsignedInt(s, 24, 32, 16)
XteaKey(k0, k1, k2, k3)
} catch (ex: NumberFormatException) {
null
}
}
}
}
public fun ByteBuf.xteaEncrypt(index: Int, length: Int, key: XteaKey) {
val k = key.toIntArray()
val end = index + (length and BLOCK_SIZE_MASK.inv())
@ -27,7 +94,7 @@ public fun ByteBuf.xteaEncrypt(index: Int, length: Int, key: SymmetricKey) {
}
}
public fun ByteBuf.xteaDecrypt(index: Int, length: Int, key: SymmetricKey) {
public fun ByteBuf.xteaDecrypt(index: Int, length: Int, key: XteaKey) {
val k = key.toIntArray()
val end = index + (length and BLOCK_SIZE_MASK.inv())

@ -4,8 +4,8 @@ import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
public object SymmetricKeyDeserializer : StdDeserializer<SymmetricKey>(SymmetricKey::class.java) {
override fun deserialize(parser: JsonParser, ctx: DeserializationContext): SymmetricKey {
return SymmetricKey.fromIntArray(ctx.readValue(parser, IntArray::class.java))
public object XteaKeyDeserializer : StdDeserializer<XteaKey>(XteaKey::class.java) {
override fun deserialize(parser: JsonParser, ctx: DeserializationContext): XteaKey {
return XteaKey.fromIntArray(ctx.readValue(parser, IntArray::class.java))
}
}

@ -4,8 +4,8 @@ import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.ser.std.StdSerializer
public object SymmetricKeySerializer : StdSerializer<SymmetricKey>(SymmetricKey::class.java) {
override fun serialize(value: SymmetricKey, gen: JsonGenerator, provider: SerializerProvider) {
public object XteaKeySerializer : StdSerializer<XteaKey>(XteaKey::class.java) {
override fun serialize(value: XteaKey, gen: JsonGenerator, provider: SerializerProvider) {
gen.writeStartArray()
gen.writeNumber(value.k0)
gen.writeNumber(value.k1)

@ -8,7 +8,7 @@ import kotlin.test.assertEquals
class XteaTest {
private class TestVector(key: String, plaintext: String, ciphertext: String) {
val key = SymmetricKey.fromHex(key)
val key = XteaKey.fromHex(key)
val plaintext: ByteArray = ByteBufUtil.decodeHexDump(plaintext)
val ciphertext: ByteArray = ByteBufUtil.decodeHexDump(ciphertext)
}

@ -1,9 +1,9 @@
package org.openrs2.decompiler
import jakarta.inject.Singleton
import org.jetbrains.java.decompiler.main.Fernflower
import org.jetbrains.java.decompiler.main.extern.IFernflowerPreferences
import org.openrs2.deob.util.Module
import javax.inject.Singleton
@Singleton
public class Decompiler {

@ -1,10 +1,10 @@
package org.openrs2.deob.ast
import com.github.michaelbull.logging.InlineLogger
import jakarta.inject.Inject
import jakarta.inject.Singleton
import org.openrs2.deob.ast.transform.Transformer
import org.openrs2.deob.util.Module
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
public class AstDeobfuscator @Inject constructor(

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save