Compare commits

..

1 Commits

Author SHA1 Message Date
Graham 2d19d3f90d Add initial libbzip2 Java port 2 years ago
  1. 30
      .drone.yml
  2. 17
      .editorconfig
  3. 34
      .github/workflows/build.yaml
  4. 1
      .gitignore
  5. 9
      .idea/runConfigurations/AstDeobfuscator.xml
  6. 9
      .idea/runConfigurations/BytecodeDeobfuscator.xml
  7. 8
      .idea/runConfigurations/Decompiler.xml
  8. 8
      .idea/runConfigurations/Deobfuscator.xml
  9. 8
      .idea/runConfigurations/GameServer.xml
  10. 9
      .idea/runConfigurations/GenerateBuffer.xml
  11. 9
      .idea/runConfigurations/Patcher.xml
  12. 2
      LICENSE
  13. 12
      README.md
  14. 11
      all/build.gradle.kts
  15. 2
      all/src/main/kotlin/org/openrs2/Command.kt
  16. 5
      archive/build.gradle.kts
  17. 2
      archive/src/main/kotlin/org/openrs2/archive/ArchiveCommand.kt
  18. 4
      archive/src/main/kotlin/org/openrs2/archive/ArchiveConfigProvider.kt
  19. 14
      archive/src/main/kotlin/org/openrs2/archive/ArchiveModule.kt
  20. 4
      archive/src/main/kotlin/org/openrs2/archive/DataSourceProvider.kt
  21. 4
      archive/src/main/kotlin/org/openrs2/archive/DatabaseProvider.kt
  22. 3
      archive/src/main/kotlin/org/openrs2/archive/cache/CacheCommand.kt
  23. 90
      archive/src/main/kotlin/org/openrs2/archive/cache/CacheDownloader.kt
  24. 367
      archive/src/main/kotlin/org/openrs2/archive/cache/CacheExporter.kt
  25. 333
      archive/src/main/kotlin/org/openrs2/archive/cache/CacheImporter.kt
  26. 16
      archive/src/main/kotlin/org/openrs2/archive/cache/CrossPollinateCommand.kt
  27. 223
      archive/src/main/kotlin/org/openrs2/archive/cache/CrossPollinator.kt
  28. 5
      archive/src/main/kotlin/org/openrs2/archive/cache/ExportCommand.kt
  29. 32
      archive/src/main/kotlin/org/openrs2/archive/cache/Js5ChannelHandler.kt
  30. 4
      archive/src/main/kotlin/org/openrs2/archive/cache/NxtJs5ChannelHandler.kt
  31. 24
      archive/src/main/kotlin/org/openrs2/archive/cache/OsrsJs5ChannelHandler.kt
  32. 7
      archive/src/main/kotlin/org/openrs2/archive/cache/OsrsJs5ChannelInitializer.kt
  33. 149
      archive/src/main/kotlin/org/openrs2/archive/cache/finder/CacheFinderExtractor.kt
  34. 25
      archive/src/main/kotlin/org/openrs2/archive/cache/finder/ExtractCommand.kt
  35. 10
      archive/src/main/kotlin/org/openrs2/archive/cache/nxt/InitJs5RemoteConnectionCodec.kt
  36. 25
      archive/src/main/kotlin/org/openrs2/archive/cache/nxt/Js5OkCodec.kt
  37. 1
      archive/src/main/kotlin/org/openrs2/archive/cache/nxt/Js5RequestEncoder.kt
  38. 6
      archive/src/main/kotlin/org/openrs2/archive/cache/nxt/Js5ResponseDecoder.kt
  39. 7
      archive/src/main/kotlin/org/openrs2/archive/cache/nxt/LoginResponse.kt
  40. 11
      archive/src/main/kotlin/org/openrs2/archive/client/Architecture.kt
  41. 35
      archive/src/main/kotlin/org/openrs2/archive/client/Artifact.kt
  42. 46
      archive/src/main/kotlin/org/openrs2/archive/client/ArtifactFormat.kt
  43. 16
      archive/src/main/kotlin/org/openrs2/archive/client/ArtifactType.kt
  44. 14
      archive/src/main/kotlin/org/openrs2/archive/client/ClientCommand.kt
  45. 455
      archive/src/main/kotlin/org/openrs2/archive/client/ClientExporter.kt
  46. 997
      archive/src/main/kotlin/org/openrs2/archive/client/ClientImporter.kt
  47. 30
      archive/src/main/kotlin/org/openrs2/archive/client/ExportCommand.kt
  48. 32
      archive/src/main/kotlin/org/openrs2/archive/client/ImportCommand.kt
  49. 7
      archive/src/main/kotlin/org/openrs2/archive/client/Jvm.kt
  50. 116
      archive/src/main/kotlin/org/openrs2/archive/client/MachO.kt
  51. 43
      archive/src/main/kotlin/org/openrs2/archive/client/OperatingSystem.kt
  52. 16
      archive/src/main/kotlin/org/openrs2/archive/client/RefreshCommand.kt
  53. 3
      archive/src/main/kotlin/org/openrs2/archive/game/Game.kt
  54. 11
      archive/src/main/kotlin/org/openrs2/archive/game/GameDatabase.kt
  55. 2
      archive/src/main/kotlin/org/openrs2/archive/jav/JavConfig.kt
  56. 20
      archive/src/main/kotlin/org/openrs2/archive/key/BinaryKeyReader.kt
  57. 57
      archive/src/main/kotlin/org/openrs2/archive/key/HdosKeyDownloader.kt
  58. 6
      archive/src/main/kotlin/org/openrs2/archive/key/HexKeyReader.kt
  59. 4
      archive/src/main/kotlin/org/openrs2/archive/key/JsonKeyDownloader.kt
  60. 16
      archive/src/main/kotlin/org/openrs2/archive/key/JsonKeyReader.kt
  61. 44
      archive/src/main/kotlin/org/openrs2/archive/key/KeyBruteForcer.kt
  62. 4
      archive/src/main/kotlin/org/openrs2/archive/key/KeyDownloader.kt
  63. 20
      archive/src/main/kotlin/org/openrs2/archive/key/KeyExporter.kt
  64. 34
      archive/src/main/kotlin/org/openrs2/archive/key/KeyImporter.kt
  65. 4
      archive/src/main/kotlin/org/openrs2/archive/key/KeyReader.kt
  66. 3
      archive/src/main/kotlin/org/openrs2/archive/key/KeySource.kt
  67. 19
      archive/src/main/kotlin/org/openrs2/archive/key/OpenOsrsKeyDownloader.kt
  68. 50
      archive/src/main/kotlin/org/openrs2/archive/key/PolarKeyDownloader.kt
  69. 4
      archive/src/main/kotlin/org/openrs2/archive/key/RuneLiteKeyDownloader.kt
  70. 6
      archive/src/main/kotlin/org/openrs2/archive/key/TextKeyReader.kt
  71. 117
      archive/src/main/kotlin/org/openrs2/archive/map/MapRenderer.kt
  72. 6
      archive/src/main/kotlin/org/openrs2/archive/name/NameImporter.kt
  73. 16
      archive/src/main/kotlin/org/openrs2/archive/name/RuneStarNameDownloader.kt
  74. 156
      archive/src/main/kotlin/org/openrs2/archive/web/CachesController.kt
  75. 82
      archive/src/main/kotlin/org/openrs2/archive/web/ClientsController.kt
  76. 16
      archive/src/main/kotlin/org/openrs2/archive/web/KeysController.kt
  77. 129
      archive/src/main/kotlin/org/openrs2/archive/web/WebServer.kt
  78. 31
      archive/src/main/kotlin/org/openrs2/archive/world/World.kt
  79. 22
      archive/src/main/kotlin/org/openrs2/archive/world/WorldList.kt
  80. 2
      archive/src/main/resources/org/openrs2/archive/migrations/V10__variants.sql
  81. 176
      archive/src/main/resources/org/openrs2/archive/migrations/V12__scopes.sql
  82. 2
      archive/src/main/resources/org/openrs2/archive/migrations/V13__hidden_flag.sql
  83. 95
      archive/src/main/resources/org/openrs2/archive/migrations/V14__scopes_fix.sql
  84. 95
      archive/src/main/resources/org/openrs2/archive/migrations/V15__empty_index_stats.sql
  85. 95
      archive/src/main/resources/org/openrs2/archive/migrations/V16__empty_index_size.sql
  86. 53
      archive/src/main/resources/org/openrs2/archive/migrations/V17__split_version_list_stats.sql
  87. 2
      archive/src/main/resources/org/openrs2/archive/migrations/V18__hdos.sql
  88. 3
      archive/src/main/resources/org/openrs2/archive/migrations/V19__source_type_cross_pollination.sql
  89. 7
      archive/src/main/resources/org/openrs2/archive/migrations/V20__cross_pollination.sql
  90. 3
      archive/src/main/resources/org/openrs2/archive/migrations/V21__manual_source.sql
  91. 95
      archive/src/main/resources/org/openrs2/archive/migrations/V22__clients.sql
  92. 11
      archive/src/main/resources/org/openrs2/archive/migrations/V23__client_sources.sql
  93. 7
      archive/src/main/resources/org/openrs2/archive/migrations/V24__loginapplet_passapplet.sql
  94. 5
      archive/src/main/resources/org/openrs2/archive/migrations/V25__client_source_file_metadata.sql
  95. 1
      archive/src/main/resources/org/openrs2/archive/migrations/V2__game_url.sql
  96. 1
      archive/src/main/resources/org/openrs2/archive/migrations/V5__keys.sql
  97. 43
      archive/src/main/resources/org/openrs2/archive/static/js/openrs2.js
  98. 285
      archive/src/main/resources/org/openrs2/archive/templates/api/index.html
  99. 36
      archive/src/main/resources/org/openrs2/archive/templates/caches/index.html
  100. 110
      archive/src/main/resources/org/openrs2/archive/templates/caches/show.html
  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,26 +13,13 @@ indent_style = tab
# @formatter:on # @formatter:on
indent_style = space indent_style = space
indent_size = 4 indent_size = 4
# see https://github.com/pinterest/ktlint/issues/764
# noinspection EditorConfigKeyCorrectness # noinspection EditorConfigKeyCorrectness
ktlint_standard_argument-list-wrapping = disabled disabled_rules = indent, parameter-list-wrapping
# 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
[*.md] [*.md]
max_line_length = 80 max_line_length = 80
[*.sql]
indent_style = space
indent_size = 4
# @formatter:off # @formatter:off
[*.{json,xml,yaml,yml}] [*.{json,xml,yaml,yml}]
# @formatter:on # @formatter:on

@ -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 !.editorconfig
!.git* !.git*
!.idea !.idea

@ -1,8 +1,13 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="AstDeobfuscator" type="JetRunConfigurationType"> <configuration default="false" name="AstDeobfuscator" type="JetRunConfigurationType">
<option name="MAIN_CLASS_NAME" value="org.openrs2.deob.ast.DeobfuscateAstCommandKt" />
<module name="openrs2.deob-ast.main" /> <module name="openrs2.deob-ast.main" />
<shortenClasspath name="NONE" /> <option name="VM_PARAMETERS" value="" />
<option name="PROGRAM_PARAMETERS" value="" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
<option name="ALTERNATIVE_JRE_PATH" />
<option name="PASS_PARENT_ENVS" value="true" />
<option name="MAIN_CLASS_NAME" value="org.openrs2.deob.ast.DeobfuscateAstCommandKt" />
<option name="WORKING_DIRECTORY" value="" />
<method v="2"> <method v="2">
<option name="Make" enabled="true" /> <option name="Make" enabled="true" />
</method> </method>

@ -1,8 +1,13 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="BytecodeDeobfuscator" type="JetRunConfigurationType"> <configuration default="false" name="BytecodeDeobfuscator" type="JetRunConfigurationType">
<option name="MAIN_CLASS_NAME" value="org.openrs2.deob.bytecode.DeobfuscateBytecodeCommandKt" />
<module name="openrs2.deob-bytecode.main" /> <module name="openrs2.deob-bytecode.main" />
<shortenClasspath name="NONE" /> <option name="VM_PARAMETERS" value="" />
<option name="PROGRAM_PARAMETERS" value="" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
<option name="ALTERNATIVE_JRE_PATH" />
<option name="PASS_PARENT_ENVS" value="true" />
<option name="MAIN_CLASS_NAME" value="org.openrs2.deob.bytecode.DeobfuscateBytecodeCommandKt" />
<option name="WORKING_DIRECTORY" value="" />
<method v="2"> <method v="2">
<option name="Make" enabled="true" /> <option name="Make" enabled="true" />
</method> </method>

@ -1,9 +1,13 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="Decompiler" type="JetRunConfigurationType"> <configuration default="false" name="Decompiler" type="JetRunConfigurationType">
<option name="MAIN_CLASS_NAME" value="org.openrs2.decompiler.DecompileCommandKt" />
<module name="openrs2.decompiler.main" /> <module name="openrs2.decompiler.main" />
<shortenClasspath name="NONE" />
<option name="VM_PARAMETERS" value="-Xmx3G" /> <option name="VM_PARAMETERS" value="-Xmx3G" />
<option name="PROGRAM_PARAMETERS" value="" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
<option name="ALTERNATIVE_JRE_PATH" />
<option name="PASS_PARENT_ENVS" value="true" />
<option name="MAIN_CLASS_NAME" value="org.openrs2.decompiler.DecompileCommandKt" />
<option name="WORKING_DIRECTORY" value="" />
<method v="2"> <method v="2">
<option name="Make" enabled="true" /> <option name="Make" enabled="true" />
</method> </method>

@ -1,9 +1,13 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="Deobfuscator" type="JetRunConfigurationType"> <configuration default="false" name="Deobfuscator" type="JetRunConfigurationType">
<option name="MAIN_CLASS_NAME" value="org.openrs2.deob.DeobfuscateCommandKt" />
<module name="openrs2.deob.main" /> <module name="openrs2.deob.main" />
<shortenClasspath name="NONE" />
<option name="VM_PARAMETERS" value="-Xmx3G" /> <option name="VM_PARAMETERS" value="-Xmx3G" />
<option name="PROGRAM_PARAMETERS" value="" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
<option name="ALTERNATIVE_JRE_PATH" />
<option name="PASS_PARENT_ENVS" value="true" />
<option name="MAIN_CLASS_NAME" value="org.openrs2.deob.DeobfuscateCommandKt" />
<option name="WORKING_DIRECTORY" value="" />
<method v="2"> <method v="2">
<option name="Make" enabled="true" /> <option name="Make" enabled="true" />
</method> </method>

@ -1,9 +1,13 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="GameServer" type="JetRunConfigurationType"> <configuration default="false" name="GameServer" type="JetRunConfigurationType">
<option name="MAIN_CLASS_NAME" value="org.openrs2.game.GameCommandKt" />
<module name="openrs2.game.main" /> <module name="openrs2.game.main" />
<shortenClasspath name="NONE" />
<option name="VM_PARAMETERS" value="-Dio.netty.leakDetection.level=PARANOID" /> <option name="VM_PARAMETERS" value="-Dio.netty.leakDetection.level=PARANOID" />
<option name="PROGRAM_PARAMETERS" value="" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
<option name="ALTERNATIVE_JRE_PATH" />
<option name="PASS_PARENT_ENVS" value="true" />
<option name="MAIN_CLASS_NAME" value="org.openrs2.game.GameCommandKt" />
<option name="WORKING_DIRECTORY" value="" />
<method v="2"> <method v="2">
<option name="Make" enabled="true" /> <option name="Make" enabled="true" />
</method> </method>

@ -1,8 +1,13 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="GenerateBuffer" type="JetRunConfigurationType"> <configuration default="false" name="GenerateBuffer" type="JetRunConfigurationType">
<option name="MAIN_CLASS_NAME" value="org.openrs2.buffer.generator.GenerateBufferCommandKt" />
<module name="openrs2.buffer-generator.main" /> <module name="openrs2.buffer-generator.main" />
<shortenClasspath name="NONE" /> <option name="VM_PARAMETERS" value="" />
<option name="PROGRAM_PARAMETERS" value="" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
<option name="ALTERNATIVE_JRE_PATH" />
<option name="PASS_PARENT_ENVS" value="true" />
<option name="MAIN_CLASS_NAME" value="org.openrs2.buffer.generator.GenerateBufferCommandKt" />
<option name="WORKING_DIRECTORY" value="" />
<method v="2"> <method v="2">
<option name="Make" enabled="true" /> <option name="Make" enabled="true" />
</method> </method>

@ -1,8 +1,13 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="Patcher" type="JetRunConfigurationType"> <configuration default="false" name="Patcher" type="JetRunConfigurationType">
<option name="MAIN_CLASS_NAME" value="org.openrs2.patcher.PatchCommandKt" />
<module name="openrs2.patcher.main" /> <module name="openrs2.patcher.main" />
<shortenClasspath name="NONE" /> <option name="VM_PARAMETERS" value="" />
<option name="PROGRAM_PARAMETERS" value="" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
<option name="ALTERNATIVE_JRE_PATH" />
<option name="PASS_PARENT_ENVS" value="true" />
<option name="MAIN_CLASS_NAME" value="org.openrs2.patcher.PatchCommandKt" />
<option name="WORKING_DIRECTORY" value="" />
<method v="2"> <method v="2">
<option name="Make" enabled="true" /> <option name="Make" enabled="true" />
</method> </method>

@ -1,4 +1,4 @@
Copyright (c) 2019-2023 OpenRS2 Authors Copyright (c) 2019-2022 OpenRS2 Authors
Permission to use, copy, modify, and/or distribute this software for any Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above purpose with or without fee is hereby granted, provided that the above

@ -1,6 +1,6 @@
# OpenRS2 # 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 ## Introduction
@ -76,10 +76,16 @@ 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 to the 2-clause BSD license. The full copyright notice and terms are available
in the `LICENSE` file. in the `LICENSE` file.
[actions-badge]: https://github.com/openrs2/openrs2/actions/workflows/build.yaml/badge.svg?branch=master The `compress-bzip2` module is derived from the reference [bzip2][bzip2]
[actions]: https://github.com/openrs2/openrs2/actions implementation. The reference implementation's license is available in
`compress-bzip2/LICENSE`. Both this license and OpenRS2's license apply to the
derived work.
[bzip2]: https://sourceware.org/bzip2/
[discord-badge]: https://img.shields.io/discord/684495254145335298 [discord-badge]: https://img.shields.io/discord/684495254145335298
[discord]: https://chat.openrs2.org/ [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-badge]: https://img.shields.io/badge/license-ISC-informational
[isc]: https://opensource.org/licenses/ISC [isc]: https://opensource.org/licenses/ISC
[issue-tracker]: https://git.openrs2.org/openrs2/openrs2/issues [issue-tracker]: https://git.openrs2.org/openrs2/openrs2/issues

@ -17,7 +17,6 @@ application {
dependencies { dependencies {
implementation(projects.archive) implementation(projects.archive)
implementation(projects.bufferGenerator) implementation(projects.bufferGenerator)
implementation(projects.cacheCli)
implementation(projects.compressCli) implementation(projects.compressCli)
implementation(projects.crc32) implementation(projects.crc32)
implementation(projects.deob) implementation(projects.deob)
@ -30,8 +29,6 @@ tasks.shadowJar {
archiveFileName.set("openrs2.jar") archiveFileName.set("openrs2.jar")
minimize { minimize {
exclude(dependency("ch.qos.logback:logback-classic"))
exclude(dependency("com.github.jnr:jnr-ffi"))
exclude(dependency("org.flywaydb:flyway-core")) exclude(dependency("org.flywaydb:flyway-core"))
exclude(dependency("org.jetbrains.kotlin:kotlin-reflect")) exclude(dependency("org.jetbrains.kotlin:kotlin-reflect"))
} }
@ -39,10 +36,10 @@ tasks.shadowJar {
tasks.register("generateAuthors") { tasks.register("generateAuthors") {
inputs.dir("$rootDir/.git") inputs.dir("$rootDir/.git")
outputs.file(layout.buildDirectory.file("AUTHORS")) outputs.file("$buildDir/AUTHORS")
doLast { doLast {
Files.newOutputStream(layout.buildDirectory.file("AUTHORS").get().asFile.toPath()).use { out -> Files.newOutputStream(buildDir.toPath().resolve("AUTHORS")).use { out ->
exec { exec {
commandLine("git", "shortlog", "-esn", "HEAD") commandLine("git", "shortlog", "-esn", "HEAD")
standardOutput = out standardOutput = out
@ -80,7 +77,7 @@ distributions {
distributionBaseName.set("openrs2") distributionBaseName.set("openrs2")
contents { contents {
from(layout.buildDirectory.file("AUTHORS")) from("$buildDir/AUTHORS")
from("$rootDir/CONTRIBUTING.md") from("$rootDir/CONTRIBUTING.md")
from("$rootDir/DCO") from("$rootDir/DCO")
from("$rootDir/LICENSE") from("$rootDir/LICENSE")
@ -97,7 +94,7 @@ distributions {
exclude(".*", "*~") exclude(".*", "*~")
into("share") 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" } rename { "third-party-licenses.txt" }
into("share/doc") into("share/doc")
} }

@ -4,7 +4,6 @@ import com.github.ajalt.clikt.core.NoOpCliktCommand
import com.github.ajalt.clikt.core.subcommands import com.github.ajalt.clikt.core.subcommands
import org.openrs2.archive.ArchiveCommand import org.openrs2.archive.ArchiveCommand
import org.openrs2.buffer.generator.GenerateBufferCommand import org.openrs2.buffer.generator.GenerateBufferCommand
import org.openrs2.cache.cli.CacheCommand
import org.openrs2.compress.cli.CompressCommand import org.openrs2.compress.cli.CompressCommand
import org.openrs2.crc32.Crc32Command import org.openrs2.crc32.Crc32Command
import org.openrs2.deob.DeobfuscateCommand import org.openrs2.deob.DeobfuscateCommand
@ -17,7 +16,6 @@ public class Command : NoOpCliktCommand(name = "openrs2") {
init { init {
subcommands( subcommands(
ArchiveCommand(), ArchiveCommand(),
CacheCommand(),
CompressCommand(), CompressCommand(),
Crc32Command(), Crc32Command(),
DeobfuscateCommand(), DeobfuscateCommand(),

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

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

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

@ -5,12 +5,12 @@ import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.google.inject.AbstractModule import com.google.inject.AbstractModule
import com.google.inject.Scopes import com.google.inject.Scopes
import com.google.inject.multibindings.Multibinder import com.google.inject.multibindings.Multibinder
import org.openrs2.archive.key.HdosKeyDownloader
import org.openrs2.archive.key.KeyDownloader import org.openrs2.archive.key.KeyDownloader
import org.openrs2.archive.key.OpenOsrsKeyDownloader
import org.openrs2.archive.key.PolarKeyDownloader
import org.openrs2.archive.key.RuneLiteKeyDownloader import org.openrs2.archive.key.RuneLiteKeyDownloader
import org.openrs2.archive.name.NameDownloader import org.openrs2.archive.name.NameDownloader
import org.openrs2.archive.name.RuneStarNameDownloader import org.openrs2.archive.name.RuneStarNameDownloader
import org.openrs2.asm.AsmModule
import org.openrs2.buffer.BufferModule import org.openrs2.buffer.BufferModule
import org.openrs2.cache.CacheModule import org.openrs2.cache.CacheModule
import org.openrs2.db.Database import org.openrs2.db.Database
@ -22,7 +22,6 @@ import javax.sql.DataSource
public object ArchiveModule : AbstractModule() { public object ArchiveModule : AbstractModule() {
override fun configure() { override fun configure() {
install(AsmModule)
install(BufferModule) install(BufferModule)
install(CacheModule) install(CacheModule)
install(HttpModule) install(HttpModule)
@ -42,13 +41,14 @@ public object ArchiveModule : AbstractModule() {
.toProvider(DatabaseProvider::class.java) .toProvider(DatabaseProvider::class.java)
.`in`(Scopes.SINGLETON) .`in`(Scopes.SINGLETON)
Multibinder.newSetBinder(binder(), Module::class.java)
.addBinding().to(JavaTimeModule::class.java)
val keyBinder = Multibinder.newSetBinder(binder(), KeyDownloader::class.java) val keyBinder = Multibinder.newSetBinder(binder(), KeyDownloader::class.java)
keyBinder.addBinding().to(HdosKeyDownloader::class.java) keyBinder.addBinding().to(OpenOsrsKeyDownloader::class.java)
keyBinder.addBinding().to(PolarKeyDownloader::class.java)
keyBinder.addBinding().to(RuneLiteKeyDownloader::class.java) keyBinder.addBinding().to(RuneLiteKeyDownloader::class.java)
val moduleBinder = Multibinder.newSetBinder(binder(), Module::class.java)
moduleBinder.addBinding().to(JavaTimeModule::class.java)
val nameBinder = Multibinder.newSetBinder(binder(), NameDownloader::class.java) val nameBinder = Multibinder.newSetBinder(binder(), NameDownloader::class.java)
nameBinder.addBinding().to(RuneStarNameDownloader::class.java) nameBinder.addBinding().to(RuneStarNameDownloader::class.java)
} }

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

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

@ -2,14 +2,11 @@ package org.openrs2.archive.cache
import com.github.ajalt.clikt.core.NoOpCliktCommand import com.github.ajalt.clikt.core.NoOpCliktCommand
import com.github.ajalt.clikt.core.subcommands import com.github.ajalt.clikt.core.subcommands
import org.openrs2.archive.cache.finder.ExtractCommand
public class CacheCommand : NoOpCliktCommand(name = "cache") { public class CacheCommand : NoOpCliktCommand(name = "cache") {
init { init {
subcommands( subcommands(
CrossPollinateCommand(),
DownloadCommand(), DownloadCommand(),
ExtractCommand(),
ImportCommand(), ImportCommand(),
ImportMasterIndexCommand(), ImportMasterIndexCommand(),
ExportCommand(), ExportCommand(),

@ -1,20 +1,15 @@
package org.openrs2.archive.cache package org.openrs2.archive.cache
import jakarta.inject.Inject
import jakarta.inject.Singleton
import org.openrs2.archive.cache.nxt.MusicStreamClient import org.openrs2.archive.cache.nxt.MusicStreamClient
import org.openrs2.archive.game.GameDatabase import org.openrs2.archive.game.GameDatabase
import org.openrs2.archive.jav.JavConfig 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.ByteBufBodyHandler
import org.openrs2.buffer.use
import org.openrs2.net.BootstrapFactory import org.openrs2.net.BootstrapFactory
import org.openrs2.net.awaitSuspend import org.openrs2.net.awaitSuspend
import java.net.URI import java.net.URI
import java.net.http.HttpClient import java.net.http.HttpClient
import java.net.http.HttpRequest import javax.inject.Inject
import kotlin.coroutines.resumeWithException import javax.inject.Singleton
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
@Singleton @Singleton
@ -27,84 +22,55 @@ public class CacheDownloader @Inject constructor(
) { ) {
public suspend fun download(gameName: String, environment: String, language: String) { public suspend fun download(gameName: String, environment: String, language: String) {
val game = gameDatabase.getGame(gameName, environment, language) ?: throw Exception("Game not found") val game = gameDatabase.getGame(gameName, environment, language) ?: throw Exception("Game not found")
val url = game.url ?: throw Exception("URL not set") 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 config = JavConfig.download(client, url)
val group = bootstrapFactory.createEventLoopGroup() val group = bootstrapFactory.createEventLoopGroup()
try { try {
suspendCoroutine { continuation -> suspendCoroutine<Unit> { continuation ->
val bootstrap = bootstrapFactory.createBootstrap(group) val bootstrap = bootstrapFactory.createBootstrap(group)
val hostname: String val hostname: String
val initializer = when (gameName) { val initializer = when (gameName) {
"oldschool" -> { "oldschool" -> {
var buildMajor = game.buildMajor val codebase = config.config[CODEBASE] ?: throw Exception("Codebase missing")
hostname = URI(codebase).host ?: throw Exception("Hostname missing")
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()
}
OsrsJs5ChannelInitializer( OsrsJs5ChannelInitializer(
OsrsJs5ChannelHandler( OsrsJs5ChannelHandler(
bootstrap, bootstrap,
game.scopeId,
game.id, game.id,
hostname, hostname,
PORT, PORT,
buildMajor ?: throw Exception("Current major build not set"), buildMajor,
game.lastMasterIndexId, game.lastMasterIndexId,
continuation, continuation,
importer importer,
) )
) )
} }
"runescape" -> { "runescape" -> {
var buildMajor = game.buildMajor val buildMinor = game.buildMinor ?: throw Exception("Current minor build not set")
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 tokens = config.params.values.filter { TOKEN_REGEX.matches(it) } val tokens = config.params.values.filter { TOKEN_REGEX.matches(it) }
val token = tokens.singleOrNull() ?: throw Exception("Multiple candidate tokens: $tokens") val token = tokens.singleOrNull() ?: throw Exception("Multiple candidate tokens: $tokens")
hostname = if (environment == "beta") { hostname = NXT_HOSTNAME
NXT_BETA_HOSTNAME
} else {
NXT_LIVE_HOSTNAME
}
val musicStreamClient = MusicStreamClient(client, byteBufBodyHandler, "http://$hostname") val musicStreamClient = MusicStreamClient(client, byteBufBodyHandler, "http://$hostname")
NxtJs5ChannelInitializer( NxtJs5ChannelInitializer(
NxtJs5ChannelHandler( NxtJs5ChannelHandler(
bootstrap, bootstrap,
game.scopeId,
game.id, game.id,
hostname, hostname,
PORT, PORT,
buildMajor ?: throw Exception("Current major build not set"), buildMajor,
buildMinor ?: throw Exception("Current minor build not set"), buildMinor,
game.lastMasterIndexId, game.lastMasterIndexId,
continuation, continuation,
importer, importer,
@ -114,44 +80,20 @@ public class CacheDownloader @Inject constructor(
) )
) )
} }
else -> throw UnsupportedOperationException() else -> throw UnsupportedOperationException()
} }
bootstrap.handler(initializer) bootstrap.handler(initializer)
.connect(hostname, PORT) .connect(hostname, PORT)
.addListener { future ->
if (!future.isSuccess) {
continuation.resumeWithException(future.cause())
}
}
} }
} finally { } finally {
group.shutdownGracefully().awaitSuspend() 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 companion object {
private const val CODEBASE = "codebase" private const val CODEBASE = "codebase"
private const val OSRS_WORLD_LIST_URL = "17" private const val NXT_HOSTNAME = "content.runescape.com"
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 private const val PORT = 443
private val TOKEN_REGEX = Regex("[A-Za-z0-9*-]{32}") private val TOKEN_REGEX = Regex("[A-Za-z0-9*-]{32}")
} }

@ -3,11 +3,8 @@ package org.openrs2.archive.cache
import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.annotation.JsonUnwrapped import com.fasterxml.jackson.annotation.JsonUnwrapped
import io.netty.buffer.ByteBuf
import io.netty.buffer.ByteBufAllocator import io.netty.buffer.ByteBufAllocator
import io.netty.buffer.Unpooled import io.netty.buffer.Unpooled
import jakarta.inject.Inject
import jakarta.inject.Singleton
import org.openrs2.buffer.use import org.openrs2.buffer.use
import org.openrs2.cache.ChecksumTable import org.openrs2.cache.ChecksumTable
import org.openrs2.cache.DiskStore import org.openrs2.cache.DiskStore
@ -16,14 +13,14 @@ import org.openrs2.cache.Js5Compression
import org.openrs2.cache.Js5MasterIndex import org.openrs2.cache.Js5MasterIndex
import org.openrs2.cache.MasterIndexFormat import org.openrs2.cache.MasterIndexFormat
import org.openrs2.cache.Store import org.openrs2.cache.Store
import org.openrs2.crypto.SymmetricKey import org.openrs2.crypto.XteaKey
import org.openrs2.db.Database import org.openrs2.db.Database
import org.postgresql.util.PGobject import org.postgresql.util.PGobject
import java.sql.Connection import java.sql.Connection
import java.time.Instant import java.time.Instant
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.util.SortedSet import java.util.SortedSet
import javax.inject.Inject
import javax.inject.Singleton
@Singleton @Singleton
public class CacheExporter @Inject constructor( public class CacheExporter @Inject constructor(
@ -77,51 +74,6 @@ public class CacheExporter @Inject constructor(
public val diskStoreValid: Boolean = blocks <= DiskStore.MAX_BLOCK public val diskStoreValid: Boolean = blocks <= DiskStore.MAX_BLOCK
} }
public data class Archive(
val resolved: Boolean,
val stats: ArchiveStats?
)
public data class ArchiveStats(
val validGroups: Long,
val groups: Long,
val validKeys: Long,
val keys: Long,
val size: Long,
val blocks: Long
) {
public val allGroupsValid: Boolean = groups == validGroups
public val validGroupsFraction: Double = if (groups == 0L) {
1.0
} else {
validGroups.toDouble() / groups
}
public val allKeysValid: Boolean = keys == validKeys
public val validKeysFraction: Double = if (keys == 0L) {
1.0
} else {
validKeys.toDouble() / keys
}
}
public data class IndexStats(
val validFiles: Long,
val files: Long,
val size: Long,
val blocks: Long
) {
public val allFilesValid: Boolean = files == validFiles
public val validFilesFraction: Double = if (files == 0L) {
1.0
} else {
validFiles.toDouble() / files
}
}
public data class Build(val major: Int, val minor: Int?) : Comparable<Build> { public data class Build(val major: Int, val minor: Int?) : Comparable<Build> {
override fun compareTo(other: Build): Int { override fun compareTo(other: Build): Int {
return compareValuesBy(this, other, Build::major, Build::minor) return compareValuesBy(this, other, Build::major, Build::minor)
@ -157,7 +109,6 @@ public class CacheExporter @Inject constructor(
public data class CacheSummary( public data class CacheSummary(
val id: Int, val id: Int,
val scope: String,
val game: String, val game: String,
val environment: String, val environment: String,
val language: String, val language: String,
@ -173,8 +124,6 @@ public class CacheExporter @Inject constructor(
val sources: List<Source>, val sources: List<Source>,
val updates: List<String>, val updates: List<String>,
val stats: Stats?, val stats: Stats?,
val archives: List<Archive>,
val indexes: List<IndexStats>?,
val masterIndex: Js5MasterIndex?, val masterIndex: Js5MasterIndex?,
val checksumTable: ChecksumTable? val checksumTable: ChecksumTable?
) )
@ -196,28 +145,9 @@ public class CacheExporter @Inject constructor(
val nameHash: Int?, val nameHash: Int?,
val name: String?, val name: String?,
@JsonProperty("mapsquare") val mapSquare: Int?, @JsonProperty("mapsquare") val mapSquare: Int?,
val key: SymmetricKey val key: XteaKey
) )
public suspend fun totalSize(): Long {
return database.execute { connection ->
connection.prepareStatement(
"""
SELECT SUM(size)
FROM cache_stats
""".trimIndent()
).use { stmt ->
stmt.executeQuery().use { rows ->
if (rows.next()) {
rows.getLong(1)
} else {
0
}
}
}
}
}
public suspend fun list(): List<CacheSummary> { public suspend fun list(): List<CacheSummary> {
return database.execute { connection -> return database.execute { connection ->
connection.prepareStatement( connection.prepareStatement(
@ -227,7 +157,6 @@ public class CacheExporter @Inject constructor(
SELECT SELECT
c.id, c.id,
g.name AS game, g.name AS game,
sc.name AS scope,
e.name AS environment, e.name AS environment,
l.iso_code AS language, l.iso_code AS language,
array_remove(array_agg(DISTINCT ROW(s.build_major, s.build_minor)::build ORDER BY ROW(s.build_major, s.build_minor)::build ASC), NULL) builds, array_remove(array_agg(DISTINCT ROW(s.build_major, s.build_minor)::build ORDER BY ROW(s.build_major, s.build_minor)::build ASC), NULL) builds,
@ -245,16 +174,14 @@ public class CacheExporter @Inject constructor(
JOIN sources s ON s.cache_id = c.id JOIN sources s ON s.cache_id = c.id
JOIN game_variants v ON v.id = s.game_id JOIN game_variants v ON v.id = s.game_id
JOIN games g ON g.id = v.game_id JOIN games g ON g.id = v.game_id
JOIN scopes sc ON sc.id = g.scope_id
JOIN environments e ON e.id = v.environment_id JOIN environments e ON e.id = v.environment_id
JOIN languages l ON l.id = v.language_id JOIN languages l ON l.id = v.language_id
LEFT JOIN cache_stats cs ON cs.scope_id = sc.id AND cs.cache_id = c.id LEFT JOIN cache_stats cs ON cs.cache_id = c.id
WHERE NOT c.hidden GROUP BY c.id, g.name, e.name, l.iso_code, cs.valid_indexes, cs.indexes, cs.valid_groups, cs.groups,
GROUP BY sc.name, c.id, g.name, e.name, l.iso_code, cs.valid_indexes, cs.indexes, cs.valid_groups, cs.valid_keys, cs.keys, cs.size, cs.blocks
cs.groups, cs.valid_keys, cs.keys, cs.size, cs.blocks
) t ) t
ORDER BY t.game ASC, t.environment ASC, t.language ASC, t.builds[1] ASC, t.timestamp ASC ORDER BY t.game ASC, t.environment ASC, t.language ASC, t.builds[1] ASC, t.timestamp ASC
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.executeQuery().use { rows -> stmt.executeQuery().use { rows ->
val caches = mutableListOf<CacheSummary>() val caches = mutableListOf<CacheSummary>()
@ -262,24 +189,21 @@ public class CacheExporter @Inject constructor(
while (rows.next()) { while (rows.next()) {
val id = rows.getInt(1) val id = rows.getInt(1)
val game = rows.getString(2) val game = rows.getString(2)
val scope = rows.getString(3) val environment = rows.getString(3)
val environment = rows.getString(4) val language = rows.getString(4)
val language = rows.getString(5) val builds = rows.getArray(5).array as Array<*>
val builds = rows.getArray(6).array as Array<*> val timestamp = rows.getTimestamp(6)?.toInstant()
val timestamp = rows.getTimestamp(7)?.toInstant() @Suppress("UNCHECKED_CAST") val sources = rows.getArray(7).array as Array<String>
@Suppress("UNCHECKED_CAST")
val sources = rows.getArray(8).array as Array<String>
val validIndexes = rows.getLong(9) val validIndexes = rows.getLong(8)
val stats = if (!rows.wasNull()) { val stats = if (!rows.wasNull()) {
val indexes = rows.getLong(10) val indexes = rows.getLong(9)
val validGroups = rows.getLong(11) val validGroups = rows.getLong(10)
val groups = rows.getLong(12) val groups = rows.getLong(11)
val validKeys = rows.getLong(13) val validKeys = rows.getLong(12)
val keys = rows.getLong(14) val keys = rows.getLong(13)
val size = rows.getLong(15) val size = rows.getLong(14)
val blocks = rows.getLong(16) val blocks = rows.getLong(15)
Stats(validIndexes, indexes, validGroups, groups, validKeys, keys, size, blocks) Stats(validIndexes, indexes, validGroups, groups, validKeys, keys, size, blocks)
} else { } else {
null null
@ -287,7 +211,6 @@ public class CacheExporter @Inject constructor(
caches += CacheSummary( caches += CacheSummary(
id, id,
scope,
game, game,
environment, environment,
language, language,
@ -304,7 +227,7 @@ public class CacheExporter @Inject constructor(
} }
} }
public suspend fun get(scope: String, id: Int): Cache? { public suspend fun get(id: Int): Cache? {
return database.execute { connection -> return database.execute { connection ->
val masterIndex: Js5MasterIndex? val masterIndex: Js5MasterIndex?
val checksumTable: ChecksumTable? val checksumTable: ChecksumTable?
@ -325,17 +248,15 @@ public class CacheExporter @Inject constructor(
cs.size, cs.size,
cs.blocks cs.blocks
FROM caches c FROM caches c
CROSS JOIN scopes s
LEFT JOIN master_indexes m ON m.id = c.id LEFT JOIN master_indexes m ON m.id = c.id
LEFT JOIN containers mc ON mc.id = m.container_id LEFT JOIN containers mc ON mc.id = m.container_id
LEFT JOIN crc_tables t ON t.id = c.id LEFT JOIN crc_tables t ON t.id = c.id
LEFT JOIN blobs b ON b.id = t.blob_id LEFT JOIN blobs b ON b.id = t.blob_id
LEFT JOIN cache_stats cs ON cs.scope_id = s.id AND cs.cache_id = c.id LEFT JOIN cache_stats cs ON cs.cache_id = c.id
WHERE s.name = ? AND c.id = ? WHERE c.id = ?
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.setString(1, scope) stmt.setInt(1, id)
stmt.setInt(2, id)
stmt.executeQuery().use { rows -> stmt.executeQuery().use { rows ->
if (!rows.next()) { if (!rows.next()) {
@ -387,15 +308,13 @@ public class CacheExporter @Inject constructor(
FROM sources s FROM sources s
JOIN game_variants v ON v.id = s.game_id JOIN game_variants v ON v.id = s.game_id
JOIN games g ON g.id = v.game_id JOIN games g ON g.id = v.game_id
JOIN scopes sc ON sc.id = g.scope_id
JOIN environments e ON e.id = v.environment_id JOIN environments e ON e.id = v.environment_id
JOIN languages l ON l.id = v.language_id JOIN languages l ON l.id = v.language_id
WHERE sc.name = ? AND s.cache_id = ? WHERE s.cache_id = ?
ORDER BY s.name ASC ORDER BY s.name ASC
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.setString(1, scope) stmt.setInt(1, id)
stmt.setInt(2, id)
stmt.executeQuery().use { rows -> stmt.executeQuery().use { rows ->
while (rows.next()) { while (rows.next()) {
@ -436,7 +355,7 @@ public class CacheExporter @Inject constructor(
SELECT url SELECT url
FROM updates FROM updates
WHERE cache_id = ? WHERE cache_id = ?
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.setInt(1, id) stmt.setInt(1, id)
@ -447,208 +366,18 @@ public class CacheExporter @Inject constructor(
} }
} }
val archives = mutableListOf<Archive>() Cache(id, sources, updates, stats, masterIndex, checksumTable)
connection.prepareStatement(
"""
SELECT a.archive_id, c.id IS NOT NULL, s.valid_groups, s.groups, s.valid_keys, s.keys, s.size, s.blocks
FROM master_index_archives a
LEFT JOIN resolve_index((SELECT id FROM scopes WHERE name = ?), a.archive_id, a.crc32, a.version) c ON TRUE
LEFT JOIN index_stats s ON s.container_id = c.id
WHERE a.master_index_id = ?
UNION ALL
SELECT a.archive_id, b.id IS NOT NULL, NULL, NULL, NULL, NULL, length(b.data), group_blocks(a.archive_id, length(b.data))
FROM crc_table_archives a
LEFT JOIN resolve_archive(a.archive_id, a.crc32) b ON TRUE
WHERE a.crc_table_id = ?
ORDER BY archive_id ASC
""".trimIndent()
).use { stmt ->
stmt.setString(1, scope)
stmt.setInt(2, id)
stmt.setInt(3, id)
stmt.executeQuery().use { rows ->
while (rows.next()) {
val resolved = rows.getBoolean(2)
val size = rows.getLong(7)
val archiveStats = if (!rows.wasNull()) {
val validGroups = rows.getLong(3)
val groups = rows.getLong(4)
val validKeys = rows.getLong(5)
val keys = rows.getLong(6)
val blocks = rows.getLong(8)
ArchiveStats(validGroups, groups, validKeys, keys, size, blocks)
} else {
null
}
archives += Archive(resolved, archiveStats)
}
}
}
val indexes = if (checksumTable != null && archives[5].resolved) {
connection.prepareStatement(
"""
SELECT s.valid_files, s.files, s.size, s.blocks
FROM crc_table_archives a
JOIN resolve_archive(a.archive_id, a.crc32) b ON TRUE
JOIN version_list_stats s ON s.blob_id = b.id
WHERE a.crc_table_id = ? AND a.archive_id = 5
ORDER BY s.index_id ASC
""".trimIndent()
).use { stmt ->
stmt.setInt(1, id)
stmt.executeQuery().use { rows ->
val indexes = mutableListOf<IndexStats>()
while (rows.next()) {
val validFiles = rows.getLong(1)
val files = rows.getLong(2)
val size = rows.getLong(3)
val blocks = rows.getLong(4)
indexes += IndexStats(validFiles, files, size, blocks)
}
indexes
}
}
} else {
null
}
Cache(id, sources, updates, stats, archives, indexes, masterIndex, checksumTable)
} }
} }
public suspend fun getFileName(scope: String, id: Int): String? { public fun export(id: Int, storeFactory: (Boolean) -> Store) {
return database.execute { connection ->
// TODO(gpe): what if a cache is from multiple games?
connection.prepareStatement(
"""
SELECT
g.name AS game,
e.name AS environment,
l.iso_code AS language,
array_remove(array_agg(DISTINCT ROW(s.build_major, s.build_minor)::build ORDER BY ROW(s.build_major, s.build_minor)::build ASC), NULL) builds,
MIN(s.timestamp) AS timestamp
FROM sources s
JOIN game_variants v ON v.id = s.game_id
JOIN games g ON g.id = v.game_id
JOIN scopes sc ON sc.id = g.scope_id
JOIN environments e ON e.id = v.environment_id
JOIN languages l ON l.id = v.language_id
WHERE sc.name = ? AND s.cache_id = ?
GROUP BY g.name, e.name, l.iso_code
LIMIT 1
""".trimIndent()
).use { stmt ->
stmt.setString(1, scope)
stmt.setInt(2, id)
stmt.executeQuery().use { rows ->
if (!rows.next()) {
return@execute null
}
val game = rows.getString(1)
val environment = rows.getString(2)
val language = rows.getString(3)
val name = StringBuilder("$game-$environment-$language")
val builds = rows.getArray(4).array as Array<*>
for (build in builds.mapNotNull { o -> Build.fromPgObject(o as PGobject) }.toSortedSet()) {
name.append("-b")
name.append(build)
}
val timestamp = rows.getTimestamp(5)
if (!rows.wasNull()) {
name.append('-')
name.append(
timestamp.toInstant()
.atOffset(ZoneOffset.UTC)
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm-ss"))
)
}
name.append("-openrs2#")
name.append(id)
name.toString()
}
}
}
}
public suspend fun exportGroup(scope: String, id: Int, archive: Int, group: Int): ByteBuf? {
return database.execute { connection ->
if (archive == Store.ARCHIVESET && group == Store.ARCHIVESET) {
connection.prepareStatement(
"""
SELECT c.data
FROM master_indexes m
JOIN containers c ON c.id = m.container_id
WHERE m.id = ?
""".trimIndent()
).use { stmt ->
stmt.setInt(1, id)
stmt.executeQuery().use { rows ->
if (rows.next()) {
val data = rows.getBytes(1)
return@execute Unpooled.wrappedBuffer(data)
}
}
}
}
connection.prepareStatement(
"""
SELECT g.data
FROM resolved_groups g
JOIN scopes s ON s.id = g.scope_id
WHERE s.name = ? AND g.master_index_id = ? AND g.archive_id = ? AND g.group_id = ?
UNION ALL
SELECT f.data
FROM resolved_files f
WHERE f.crc_table_id = ? AND f.index_id = ? AND f.file_id = ?
""".trimIndent()
).use { stmt ->
stmt.setString(1, scope)
stmt.setInt(2, id)
stmt.setInt(3, archive)
stmt.setInt(4, group)
stmt.setInt(5, id)
stmt.setInt(6, archive)
stmt.setInt(7, group)
stmt.executeQuery().use { rows ->
if (!rows.next()) {
return@execute null
}
val data = rows.getBytes(1)
return@execute Unpooled.wrappedBuffer(data)
}
}
}
}
public fun export(scope: String, id: Int, storeFactory: (Boolean) -> Store) {
database.executeOnce { connection -> database.executeOnce { connection ->
val legacy = connection.prepareStatement( val legacy = connection.prepareStatement(
""" """
SELECT id SELECT id
FROM crc_tables FROM crc_tables
WHERE id = ? WHERE id = ?
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.setInt(1, id) stmt.setInt(1, id)
@ -661,24 +390,22 @@ public class CacheExporter @Inject constructor(
if (legacy) { if (legacy) {
exportLegacy(connection, id, store) exportLegacy(connection, id, store)
} else { } else {
export(connection, scope, id, store) export(connection, id, store)
} }
} }
} }
} }
private fun export(connection: Connection, scope: String, id: Int, store: Store) { private fun export(connection: Connection, id: Int, store: Store) {
connection.prepareStatement( connection.prepareStatement(
""" """
SELECT g.archive_id, g.group_id, g.data, g.version SELECT archive_id, group_id, data, version
FROM resolved_groups g FROM resolved_groups
JOIN scopes s ON s.id = g.scope_id WHERE master_index_id = ?
WHERE s.name = ? AND g.master_index_id = ? """.trimIndent()
""".trimIndent()
).use { stmt -> ).use { stmt ->
stmt.fetchSize = BATCH_SIZE stmt.fetchSize = BATCH_SIZE
stmt.setString(1, scope) stmt.setInt(1, id)
stmt.setInt(2, id)
stmt.executeQuery().use { rows -> stmt.executeQuery().use { rows ->
alloc.buffer(2, 2).use { versionBuf -> alloc.buffer(2, 2).use { versionBuf ->
@ -716,7 +443,7 @@ public class CacheExporter @Inject constructor(
SELECT index_id, file_id, data, version SELECT index_id, file_id, data, version
FROM resolved_files FROM resolved_files
WHERE crc_table_id = ? WHERE crc_table_id = ?
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.fetchSize = BATCH_SIZE stmt.fetchSize = BATCH_SIZE
stmt.setInt(1, id) stmt.setInt(1, id)
@ -746,20 +473,18 @@ public class CacheExporter @Inject constructor(
} }
} }
public suspend fun exportKeys(scope: String, id: Int): List<Key> { public suspend fun exportKeys(id: Int): List<Key> {
return database.execute { connection -> return database.execute { connection ->
connection.prepareStatement( connection.prepareStatement(
""" """
SELECT g.archive_id, g.group_id, g.name_hash, n.name, (k.key).k0, (k.key).k1, (k.key).k2, (k.key).k3 SELECT g.archive_id, g.group_id, g.name_hash, n.name, (k.key).k0, (k.key).k1, (k.key).k2, (k.key).k3
FROM resolved_groups g FROM resolved_groups g
JOIN scopes s ON s.id = g.scope_id
JOIN keys k ON k.id = g.key_id JOIN keys k ON k.id = g.key_id
LEFT JOIN names n ON n.hash = g.name_hash AND n.name ~ '^l(?:[0-9]|[1-9][0-9])_(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$' LEFT JOIN names n ON n.hash = g.name_hash AND n.name ~ '^l(?:[0-9]|[1-9][0-9])_(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$'
WHERE s.name = ? AND g.master_index_id = ? WHERE g.master_index_id = ?
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.setString(1, scope) stmt.setInt(1, id)
stmt.setInt(2, id)
stmt.executeQuery().use { rows -> stmt.executeQuery().use { rows ->
val keys = mutableListOf<Key>() val keys = mutableListOf<Key>()
@ -779,7 +504,7 @@ public class CacheExporter @Inject constructor(
val k3 = rows.getInt(8) val k3 = rows.getInt(8)
val mapSquare = getMapSquare(name) 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 keys

@ -6,8 +6,6 @@ import io.netty.buffer.ByteBufAllocator
import io.netty.buffer.ByteBufUtil import io.netty.buffer.ByteBufUtil
import io.netty.buffer.DefaultByteBufHolder import io.netty.buffer.DefaultByteBufHolder
import io.netty.buffer.Unpooled import io.netty.buffer.Unpooled
import jakarta.inject.Inject
import jakarta.inject.Singleton
import org.openrs2.buffer.crc32 import org.openrs2.buffer.crc32
import org.openrs2.buffer.use import org.openrs2.buffer.use
import org.openrs2.cache.ChecksumTable import org.openrs2.cache.ChecksumTable
@ -24,8 +22,6 @@ import org.openrs2.cache.StoreCorruptException
import org.openrs2.cache.VersionList import org.openrs2.cache.VersionList
import org.openrs2.cache.VersionTrailer import org.openrs2.cache.VersionTrailer
import org.openrs2.crypto.Whirlpool import org.openrs2.crypto.Whirlpool
import org.openrs2.crypto.sha1
import org.openrs2.crypto.whirlpool
import org.openrs2.db.Database import org.openrs2.db.Database
import org.postgresql.util.PSQLState import org.postgresql.util.PSQLState
import java.io.IOException import java.io.IOException
@ -34,6 +30,8 @@ import java.sql.SQLException
import java.sql.Types import java.sql.Types
import java.time.Instant import java.time.Instant
import java.time.ZoneOffset import java.time.ZoneOffset
import javax.inject.Inject
import javax.inject.Singleton
@Singleton @Singleton
public class CacheImporter @Inject constructor( public class CacheImporter @Inject constructor(
@ -86,8 +84,7 @@ public class CacheImporter @Inject constructor(
) : DefaultByteBufHolder(buf) { ) : DefaultByteBufHolder(buf) {
public val bytes: ByteArray = ByteBufUtil.getBytes(buf, buf.readerIndex(), buf.readableBytes(), false) public val bytes: ByteArray = ByteBufUtil.getBytes(buf, buf.readerIndex(), buf.readableBytes(), false)
public val crc32: Int = buf.crc32() public val crc32: Int = buf.crc32()
public val sha1: ByteArray = buf.sha1() public val whirlpool: ByteArray = Whirlpool.whirlpool(bytes)
public val whirlpool: ByteArray = buf.whirlpool()
} }
public class ChecksumTableBlob( public class ChecksumTableBlob(
@ -108,10 +105,9 @@ public class CacheImporter @Inject constructor(
public val version: Int public val version: Int
) : Blob(buf) ) : Blob(buf)
internal enum class SourceType { private enum class SourceType {
DISK, DISK,
JS5REMOTE, JS5REMOTE
CROSS_POLLINATION
} }
public data class MasterIndexResult( public data class MasterIndexResult(
@ -120,14 +116,9 @@ public class CacheImporter @Inject constructor(
val indexes: List<ByteBuf?> val indexes: List<ByteBuf?>
) )
private data class Game(
val id: Int,
val scopeId: Int
)
public suspend fun import( public suspend fun import(
store: Store, store: Store,
gameName: String, game: String,
environment: String, environment: String,
language: String, language: String,
buildMajor: Int?, buildMajor: Int?,
@ -140,12 +131,12 @@ public class CacheImporter @Inject constructor(
database.execute { connection -> database.execute { connection ->
prepare(connection) prepare(connection)
val game = getGame(connection, gameName, environment, language) val gameId = getGameId(connection, game, environment, language)
if (store is DiskStore && store.legacy) { if (store is DiskStore && store.legacy) {
importLegacy(connection, store, game.id, buildMajor, buildMinor, timestamp, name, description, url) importLegacy(connection, store, gameId, buildMajor, buildMinor, timestamp, name, description, url)
} else { } else {
importJs5(connection, store, game, buildMajor, buildMinor, timestamp, name, description, url) importJs5(connection, store, gameId, buildMajor, buildMinor, timestamp, name, description, url)
} }
} }
} }
@ -153,7 +144,7 @@ public class CacheImporter @Inject constructor(
private fun importJs5( private fun importJs5(
connection: Connection, connection: Connection,
store: Store, store: Store,
game: Game, gameId: Int,
buildMajor: Int?, buildMajor: Int?,
buildMinor: Int?, buildMinor: Int?,
timestamp: Instant?, timestamp: Instant?,
@ -178,7 +169,7 @@ public class CacheImporter @Inject constructor(
connection, connection,
SourceType.DISK, SourceType.DISK,
masterIndexId, masterIndexId,
game.id, gameId,
buildMajor, buildMajor,
buildMinor, buildMinor,
timestamp, timestamp,
@ -203,7 +194,7 @@ public class CacheImporter @Inject constructor(
} }
for (index in indexGroups) { for (index in indexGroups) {
addIndex(connection, game.scopeId, sourceId, index) addIndex(connection, sourceId, index)
} }
} finally { } finally {
indexGroups.forEach(Index::release) indexGroups.forEach(Index::release)
@ -224,7 +215,7 @@ public class CacheImporter @Inject constructor(
groups += group groups += group
if (groups.size >= BATCH_SIZE) { if (groups.size >= BATCH_SIZE) {
addGroups(connection, game.scopeId, sourceId, groups) addGroups(connection, sourceId, groups)
groups.forEach(Group::release) groups.forEach(Group::release)
groups.clear() groups.clear()
@ -233,7 +224,7 @@ public class CacheImporter @Inject constructor(
} }
if (groups.isNotEmpty()) { if (groups.isNotEmpty()) {
addGroups(connection, game.scopeId, sourceId, groups) addGroups(connection, sourceId, groups)
} }
} finally { } finally {
groups.forEach(Group::release) groups.forEach(Group::release)
@ -243,7 +234,7 @@ public class CacheImporter @Inject constructor(
public suspend fun importMasterIndex( public suspend fun importMasterIndex(
buf: ByteBuf, buf: ByteBuf,
format: MasterIndexFormat, format: MasterIndexFormat,
gameName: String, game: String,
environment: String, environment: String,
language: String, language: String,
buildMajor: Int?, buildMajor: Int?,
@ -263,14 +254,14 @@ public class CacheImporter @Inject constructor(
database.execute { connection -> database.execute { connection ->
prepare(connection) prepare(connection)
val game = getGame(connection, gameName, environment, language) val gameId = getGameId(connection, game, environment, language)
val masterIndexId = addMasterIndex(connection, masterIndex) val masterIndexId = addMasterIndex(connection, masterIndex)
addSource( addSource(
connection, connection,
SourceType.DISK, SourceType.DISK,
masterIndexId, masterIndexId,
game.id, gameId,
buildMajor, buildMajor,
buildMinor, buildMinor,
timestamp, timestamp,
@ -287,7 +278,6 @@ public class CacheImporter @Inject constructor(
buf: ByteBuf, buf: ByteBuf,
uncompressed: ByteBuf, uncompressed: ByteBuf,
gameId: Int, gameId: Int,
scopeId: Int,
buildMajor: Int, buildMajor: Int,
buildMinor: Int?, buildMinor: Int?,
lastId: Int?, lastId: Int?,
@ -301,7 +291,7 @@ public class CacheImporter @Inject constructor(
UPDATE game_variants UPDATE game_variants
SET build_major = ?, build_minor = ? SET build_major = ?, build_minor = ?
WHERE id = ? WHERE id = ?
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.setInt(1, buildMajor) stmt.setInt(1, buildMajor)
stmt.setObject(2, buildMinor, Types.INTEGER) stmt.setObject(2, buildMinor, Types.INTEGER)
@ -342,14 +332,13 @@ public class CacheImporter @Inject constructor(
FROM master_index_archives a FROM master_index_archives a
LEFT JOIN master_index_archives a2 ON a2.master_index_id = ? AND a2.archive_id = a.archive_id AND LEFT JOIN master_index_archives a2 ON a2.master_index_id = ? AND a2.archive_id = a.archive_id AND
a2.crc32 = a.crc32 AND a2.version = a.version a2.crc32 = a.crc32 AND a2.version = a.version
LEFT JOIN resolve_index(?, a2.archive_id, a2.crc32, a2.version) c ON TRUE LEFT JOIN resolve_index(a2.archive_id, a2.crc32, a2.version) c ON TRUE
WHERE a.master_index_id = ? WHERE a.master_index_id = ?
ORDER BY a.archive_id ASC ORDER BY a.archive_id ASC
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.setObject(1, lastId, Types.INTEGER) stmt.setObject(1, lastId, Types.INTEGER)
stmt.setInt(2, scopeId) stmt.setInt(2, masterIndexId)
stmt.setInt(3, masterIndexId)
stmt.executeQuery().use { rows -> stmt.executeQuery().use { rows ->
val indexes = mutableListOf<ByteBuf?>() val indexes = mutableListOf<ByteBuf?>()
@ -374,7 +363,6 @@ public class CacheImporter @Inject constructor(
} }
public suspend fun importIndexAndGetMissingGroups( public suspend fun importIndexAndGetMissingGroups(
scopeId: Int,
sourceId: Int, sourceId: Int,
archive: Int, archive: Int,
index: Js5Index, index: Js5Index,
@ -384,7 +372,7 @@ public class CacheImporter @Inject constructor(
): List<Int> { ): List<Int> {
return database.execute { connection -> return database.execute { connection ->
prepare(connection) prepare(connection)
val id = addIndex(connection, scopeId, sourceId, Index(archive, index, buf, uncompressed)) val id = addIndex(connection, sourceId, Index(archive, index, buf, uncompressed))
/* /*
* In order to defend against (crc32, version) collisions, we only * In order to defend against (crc32, version) collisions, we only
@ -402,18 +390,17 @@ public class CacheImporter @Inject constructor(
SELECT ig.group_id SELECT ig.group_id
FROM index_groups ig FROM index_groups ig
LEFT JOIN resolved_indexes i ON i.master_index_id = ? AND LEFT JOIN resolved_indexes i ON i.master_index_id = ? AND
i.archive_id = ? AND i.scope_id = ? i.archive_id = ?
LEFT JOIN index_groups ig2 ON ig2.container_id = i.container_id AND ig2.group_id = ig.group_id AND LEFT JOIN index_groups ig2 ON ig2.container_id = i.container_id AND ig2.group_id = ig.group_id AND
ig2.crc32 = ig.crc32 AND ig2.version = ig.version ig2.crc32 = ig.crc32 AND ig2.version = ig.version
LEFT JOIN resolve_group(i.scope_id, i.archive_id, ig2.group_id, ig2.crc32, ig2.version) c ON TRUE LEFT JOIN resolve_group(i.archive_id, ig2.group_id, ig2.crc32, ig2.version) c ON TRUE
WHERE ig.container_id = ? AND c.id IS NULL WHERE ig.container_id = ? AND c.id IS NULL
ORDER BY ig.group_id ASC ORDER BY ig.group_id ASC
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.setObject(1, lastMasterIndexId, Types.INTEGER) stmt.setObject(1, lastMasterIndexId, Types.INTEGER)
stmt.setInt(2, archive) stmt.setInt(2, archive)
stmt.setInt(3, scopeId) stmt.setLong(3, id)
stmt.setLong(4, id)
stmt.executeQuery().use { rows -> stmt.executeQuery().use { rows ->
val groups = mutableListOf<Int>() val groups = mutableListOf<Int>()
@ -428,14 +415,14 @@ public class CacheImporter @Inject constructor(
} }
} }
public suspend fun importGroups(scopeId: Int, sourceId: Int, groups: List<Group>) { public suspend fun importGroups(sourceId: Int, groups: List<Group>) {
if (groups.isEmpty()) { if (groups.isEmpty()) {
return return
} }
database.execute { connection -> database.execute { connection ->
prepare(connection) prepare(connection)
addGroups(connection, scopeId, sourceId, groups) addGroups(connection, sourceId, groups)
} }
} }
@ -462,7 +449,7 @@ public class CacheImporter @Inject constructor(
SELECT id SELECT id
FROM master_indexes FROM master_indexes
WHERE container_id = ? AND format = ?::master_index_format WHERE container_id = ? AND format = ?::master_index_format
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.setLong(1, containerId) stmt.setLong(1, containerId)
stmt.setString(2, masterIndex.index.format.name.lowercase()) stmt.setString(2, masterIndex.index.format.name.lowercase())
@ -481,7 +468,7 @@ public class CacheImporter @Inject constructor(
INSERT INTO caches (id) INSERT INTO caches (id)
VALUES (DEFAULT) VALUES (DEFAULT)
RETURNING id RETURNING id
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.executeQuery().use { rows -> stmt.executeQuery().use { rows ->
check(rows.next()) check(rows.next())
@ -493,7 +480,7 @@ public class CacheImporter @Inject constructor(
""" """
INSERT INTO master_indexes (id, container_id, format) INSERT INTO master_indexes (id, container_id, format)
VALUES (?, ?, ?::master_index_format) VALUES (?, ?, ?::master_index_format)
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.setInt(1, masterIndexId) stmt.setInt(1, masterIndexId)
stmt.setLong(2, containerId) stmt.setLong(2, containerId)
@ -508,7 +495,7 @@ public class CacheImporter @Inject constructor(
master_index_id, archive_id, crc32, version, whirlpool, groups, total_uncompressed_length master_index_id, archive_id, crc32, version, whirlpool, groups, total_uncompressed_length
) )
VALUES (?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?)
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
for ((i, entry) in masterIndex.index.entries.withIndex()) { for ((i, entry) in masterIndex.index.entries.withIndex()) {
stmt.setInt(1, masterIndexId) stmt.setInt(1, masterIndexId)
@ -544,11 +531,11 @@ public class CacheImporter @Inject constructor(
return masterIndexId return masterIndexId
} }
internal fun addSource( private fun addSource(
connection: Connection, connection: Connection,
type: SourceType, type: SourceType,
cacheId: Int?, cacheId: Int,
gameId: Int?, gameId: Int,
buildMajor: Int?, buildMajor: Int?,
buildMinor: Int?, buildMinor: Int?,
timestamp: Instant?, timestamp: Instant?,
@ -556,29 +543,13 @@ public class CacheImporter @Inject constructor(
description: String?, description: String?,
url: String? url: String?
): Int { ): Int {
if (type == SourceType.CROSS_POLLINATION) { if (type == SourceType.JS5REMOTE && buildMajor != null) {
connection.prepareStatement(
"""
SELECT id
FROM sources
WHERE type = 'cross_pollination'
""".trimIndent()
).use { stmt ->
stmt.executeQuery().use { rows ->
if (rows.next()) {
return rows.getInt(1)
}
}
}
}
if (type == SourceType.JS5REMOTE && cacheId != null && gameId != null && buildMajor != null) {
connection.prepareStatement( connection.prepareStatement(
""" """
SELECT id SELECT id
FROM sources FROM sources
WHERE type = 'js5remote' AND cache_id = ? AND game_id = ? AND build_major = ? AND build_minor IS NOT DISTINCT FROM ? WHERE type = 'js5remote' AND cache_id = ? AND game_id = ? AND build_major = ? AND build_minor IS NOT DISTINCT FROM ?
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.setInt(1, cacheId) stmt.setInt(1, cacheId)
stmt.setInt(2, gameId) stmt.setInt(2, gameId)
@ -598,11 +569,11 @@ public class CacheImporter @Inject constructor(
INSERT INTO sources (type, cache_id, game_id, build_major, build_minor, timestamp, name, description, url) INSERT INTO sources (type, cache_id, game_id, build_major, build_minor, timestamp, name, description, url)
VALUES (?::source_type, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?::source_type, ?, ?, ?, ?, ?, ?, ?, ?)
RETURNING id RETURNING id
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.setString(1, type.toString().lowercase()) stmt.setString(1, type.toString().lowercase())
stmt.setObject(2, cacheId, Types.INTEGER) stmt.setInt(2, cacheId)
stmt.setObject(3, gameId, Types.INTEGER) stmt.setInt(3, gameId)
stmt.setObject(4, buildMajor, Types.INTEGER) stmt.setObject(4, buildMajor, Types.INTEGER)
stmt.setObject(5, buildMinor, Types.INTEGER) stmt.setObject(5, buildMinor, Types.INTEGER)
@ -656,23 +627,22 @@ public class CacheImporter @Inject constructor(
} }
} }
internal fun addGroups(connection: Connection, scopeId: Int, sourceId: Int, groups: List<Group>): List<Long> { private fun addGroups(connection: Connection, sourceId: Int, groups: List<Group>): List<Long> {
val containerIds = addContainers(connection, groups) val containerIds = addContainers(connection, groups)
connection.prepareStatement( connection.prepareStatement(
""" """
INSERT INTO groups (scope_id, archive_id, group_id, version, version_truncated, container_id) INSERT INTO groups (archive_id, group_id, version, version_truncated, container_id)
VALUES (?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?)
ON CONFLICT DO NOTHING ON CONFLICT DO NOTHING
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
for ((i, group) in groups.withIndex()) { for ((i, group) in groups.withIndex()) {
stmt.setInt(1, scopeId) stmt.setInt(1, group.archive)
stmt.setInt(2, group.archive) stmt.setInt(2, group.group)
stmt.setInt(3, group.group) stmt.setInt(3, group.version)
stmt.setInt(4, group.version) stmt.setBoolean(4, group.versionTruncated)
stmt.setBoolean(5, group.versionTruncated) stmt.setLong(5, containerIds[i])
stmt.setLong(6, containerIds[i])
stmt.addBatch() stmt.addBatch()
} }
@ -684,7 +654,7 @@ public class CacheImporter @Inject constructor(
INSERT INTO source_groups (source_id, archive_id, group_id, version, version_truncated, container_id) INSERT INTO source_groups (source_id, archive_id, group_id, version, version_truncated, container_id)
VALUES (?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT DO NOTHING ON CONFLICT DO NOTHING
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
for ((i, group) in groups.withIndex()) { for ((i, group) in groups.withIndex()) {
stmt.setInt(1, sourceId) stmt.setInt(1, sourceId)
@ -702,8 +672,8 @@ public class CacheImporter @Inject constructor(
return containerIds return containerIds
} }
private fun addGroup(connection: Connection, scopeId: Int, sourceId: Int, group: Group): Long { private fun addGroup(connection: Connection, sourceId: Int, group: Group): Long {
return addGroups(connection, scopeId, sourceId, listOf(group)).single() return addGroups(connection, sourceId, listOf(group)).single()
} }
private fun readIndex(store: Store, archive: Int): Index { private fun readIndex(store: Store, archive: Int): Index {
@ -714,8 +684,8 @@ public class CacheImporter @Inject constructor(
} }
} }
private fun addIndex(connection: Connection, scopeId: Int, sourceId: Int, index: Index): Long { private fun addIndex(connection: Connection, sourceId: Int, index: Index): Long {
val containerId = addGroup(connection, scopeId, sourceId, index) val containerId = addGroup(connection, sourceId, index)
val savepoint = connection.setSavepoint() val savepoint = connection.setSavepoint()
connection.prepareStatement( connection.prepareStatement(
@ -724,7 +694,7 @@ public class CacheImporter @Inject constructor(
container_id, protocol, version, has_names, has_digests, has_lengths, has_uncompressed_checksums container_id, protocol, version, has_names, has_digests, has_lengths, has_uncompressed_checksums
) )
VALUES (?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?)
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.setLong(1, containerId) stmt.setLong(1, containerId)
stmt.setInt(2, index.index.protocol.id) stmt.setInt(2, index.index.protocol.id)
@ -752,7 +722,7 @@ public class CacheImporter @Inject constructor(
uncompressed_crc32 uncompressed_crc32
) )
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
for (group in index.index) { for (group in index.index) {
stmt.setLong(1, containerId) stmt.setLong(1, containerId)
@ -791,7 +761,7 @@ public class CacheImporter @Inject constructor(
""" """
INSERT INTO index_files (container_id, group_id, file_id, name_hash) INSERT INTO index_files (container_id, group_id, file_id, name_hash)
VALUES (?, ?, ?, ?) VALUES (?, ?, ?, ?)
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
for (group in index.index) { for (group in index.index) {
for (file in group) { for (file in group) {
@ -815,21 +785,10 @@ public class CacheImporter @Inject constructor(
return containerId return containerId
} }
internal fun prepare(connection: Connection) { private fun prepare(connection: Connection) {
connection.prepareStatement( connection.prepareStatement(
""" """
LOCK TABLE containers IN EXCLUSIVE MODE LOCK TABLE containers IN EXCLUSIVE MODE
""".trimIndent()
).use { stmt ->
stmt.execute()
}
connection.prepareStatement(
"""
CREATE TEMPORARY TABLE tmp_container_hashes (
index INTEGER NOT NULL,
whirlpool BYTEA NOT NULL
) ON COMMIT DROP
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.execute() stmt.execute()
@ -847,7 +806,7 @@ public class CacheImporter @Inject constructor(
encrypted BOOLEAN NOT NULL, encrypted BOOLEAN NOT NULL,
empty_loc BOOLEAN NULL empty_loc BOOLEAN NULL
) ON COMMIT DROP ) ON COMMIT DROP
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.execute() stmt.execute()
} }
@ -857,11 +816,10 @@ public class CacheImporter @Inject constructor(
CREATE TEMPORARY TABLE tmp_blobs ( CREATE TEMPORARY TABLE tmp_blobs (
index INTEGER NOT NULL, index INTEGER NOT NULL,
crc32 INTEGER NOT NULL, crc32 INTEGER NOT NULL,
sha1 BYTEA NOT NULL,
whirlpool BYTEA NOT NULL, whirlpool BYTEA NOT NULL,
data BYTEA NOT NULL data BYTEA NOT NULL
) ON COMMIT DROP ) ON COMMIT DROP
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.execute() stmt.execute()
} }
@ -874,70 +832,19 @@ public class CacheImporter @Inject constructor(
private fun addContainers(connection: Connection, containers: List<Container>): List<Long> { private fun addContainers(connection: Connection, containers: List<Container>): List<Long> {
connection.prepareStatement( connection.prepareStatement(
""" """
TRUNCATE TABLE tmp_containers, tmp_container_hashes TRUNCATE TABLE tmp_containers
""".trimIndent()
).use { stmt ->
stmt.execute()
}
connection.prepareStatement(
"""
INSERT INTO tmp_container_hashes (index, whirlpool)
VALUES (?, ?)
""".trimIndent()
).use { stmt ->
for ((i, container) in containers.withIndex()) {
stmt.setInt(1, i)
stmt.setBytes(2, container.whirlpool)
stmt.addBatch()
}
stmt.executeBatch()
}
val ids = mutableListOf<Long?>()
var count = 0
connection.prepareStatement(
"""
SELECT c.id
FROM tmp_container_hashes t
LEFT JOIN containers c ON c.whirlpool = t.whirlpool
ORDER BY t.index ASC
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.executeQuery().use { rows -> stmt.execute()
while (rows.next()) {
val id = rows.getLong(1)
if (rows.wasNull()) {
ids += null
} else {
ids += id
count++
}
}
}
}
check(ids.size == containers.size)
if (count == containers.size) {
@Suppress("UNCHECKED_CAST")
return ids as List<Long>
} }
connection.prepareStatement( connection.prepareStatement(
""" """
INSERT INTO tmp_containers (index, crc32, whirlpool, data, uncompressed_length, uncompressed_crc32, encrypted, empty_loc) INSERT INTO tmp_containers (index, crc32, whirlpool, data, uncompressed_length, uncompressed_crc32, encrypted, empty_loc)
VALUES (?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
for ((i, container) in containers.withIndex()) { for ((i, container) in containers.withIndex()) {
if (ids[i] != null) {
continue
}
stmt.setInt(1, i) stmt.setInt(1, i)
stmt.setInt(2, container.crc32) stmt.setInt(2, container.crc32)
stmt.setBytes(3, container.whirlpool) stmt.setBytes(3, container.whirlpool)
@ -966,61 +873,56 @@ public class CacheImporter @Inject constructor(
LEFT JOIN containers c ON c.whirlpool = t.whirlpool LEFT JOIN containers c ON c.whirlpool = t.whirlpool
WHERE c.whirlpool IS NULL WHERE c.whirlpool IS NULL
ON CONFLICT DO NOTHING ON CONFLICT DO NOTHING
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.execute() stmt.execute()
} }
val ids = mutableListOf<Long>()
connection.prepareStatement( connection.prepareStatement(
""" """
SELECT t.index, c.id SELECT c.id
FROM tmp_containers t FROM tmp_containers t
JOIN containers c ON c.whirlpool = t.whirlpool JOIN containers c ON c.whirlpool = t.whirlpool
ORDER BY t.index ASC ORDER BY t.index ASC
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.executeQuery().use { rows -> stmt.executeQuery().use { rows ->
while (rows.next()) { while (rows.next()) {
val index = rows.getInt(1) ids += rows.getLong(1)
val id = rows.getLong(2)
ids[index] = id
count++
} }
} }
} }
check(count == containers.size) check(ids.size == containers.size)
return ids
@Suppress("UNCHECKED_CAST")
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() 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( connection.prepareStatement(
""" """
TRUNCATE TABLE tmp_blobs TRUNCATE TABLE tmp_blobs
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.execute() stmt.execute()
} }
connection.prepareStatement( connection.prepareStatement(
""" """
INSERT INTO tmp_blobs (index, crc32, sha1, whirlpool, data) INSERT INTO tmp_blobs (index, crc32, whirlpool, data)
VALUES (?, ?, ?, ?, ?) VALUES (?, ?, ?, ?)
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
for ((i, blob) in blobs.withIndex()) { for ((i, blob) in blobs.withIndex()) {
stmt.setInt(1, i) stmt.setInt(1, i)
stmt.setInt(2, blob.crc32) stmt.setInt(2, blob.crc32)
stmt.setBytes(3, blob.sha1) stmt.setBytes(3, blob.whirlpool)
stmt.setBytes(4, blob.whirlpool) stmt.setBytes(4, blob.bytes)
stmt.setBytes(5, blob.bytes)
stmt.addBatch() stmt.addBatch()
} }
@ -1030,13 +932,13 @@ public class CacheImporter @Inject constructor(
connection.prepareStatement( connection.prepareStatement(
""" """
INSERT INTO blobs (crc32, sha1, whirlpool, data) INSERT INTO blobs (crc32, whirlpool, data)
SELECT t.crc32, t.sha1, t.whirlpool, t.data SELECT t.crc32, t.whirlpool, t.data
FROM tmp_blobs t FROM tmp_blobs t
LEFT JOIN blobs b ON b.whirlpool = t.whirlpool LEFT JOIN blobs b ON b.whirlpool = t.whirlpool
WHERE b.whirlpool IS NULL WHERE b.whirlpool IS NULL
ON CONFLICT DO NOTHING ON CONFLICT DO NOTHING
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.execute() stmt.execute()
} }
@ -1049,7 +951,7 @@ public class CacheImporter @Inject constructor(
FROM tmp_blobs t FROM tmp_blobs t
JOIN blobs b ON b.whirlpool = t.whirlpool JOIN blobs b ON b.whirlpool = t.whirlpool
ORDER BY t.index ASC ORDER BY t.index ASC
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.executeQuery().use { rows -> stmt.executeQuery().use { rows ->
while (rows.next()) { while (rows.next()) {
@ -1062,10 +964,10 @@ public class CacheImporter @Inject constructor(
return ids return ids
} }
private fun getGame(connection: Connection, name: String, environment: String, language: String): Game { private fun getGameId(connection: Connection, name: String, environment: String, language: String): Int {
connection.prepareStatement( connection.prepareStatement(
""" """
SELECT v.id, g.scope_id SELECT v.id
FROM game_variants v FROM game_variants v
JOIN games g ON g.id = v.game_id JOIN games g ON g.id = v.game_id
JOIN environments e ON e.id = v.environment_id JOIN environments e ON e.id = v.environment_id
@ -1082,10 +984,7 @@ public class CacheImporter @Inject constructor(
throw Exception("Game not found") throw Exception("Game not found")
} }
val id = rows.getInt(1) return rows.getInt(1)
val scopeId = rows.getInt(2)
return Game(id, scopeId)
} }
} }
} }
@ -1095,7 +994,7 @@ public class CacheImporter @Inject constructor(
connection.prepareStatement( connection.prepareStatement(
""" """
UPDATE game_variants SET last_master_index_id = ? WHERE id = ? UPDATE game_variants SET last_master_index_id = ? WHERE id = ?
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.setInt(1, masterIndexId) stmt.setInt(1, masterIndexId)
stmt.setInt(2, gameId) stmt.setInt(2, gameId)
@ -1144,13 +1043,8 @@ public class CacheImporter @Inject constructor(
// import archives and version list // import archives and version list
for (id in store.list(0)) { for (id in store.list(0)) {
try { readArchive(store, id).use { archive ->
readArchive(store, id).use { archive -> addArchive(connection, sourceId, archive)
addArchive(connection, sourceId, archive)
}
} catch (ex: StoreCorruptException) {
// see the comment in ChecksumTable::create
logger.warn(ex) { "Skipping corrupt archive ($id)" }
} }
} }
@ -1202,7 +1096,7 @@ public class CacheImporter @Inject constructor(
SELECT id SELECT id
FROM crc_tables FROM crc_tables
WHERE blob_id = ? WHERE blob_id = ?
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.setLong(1, blobId) stmt.setLong(1, blobId)
@ -1220,7 +1114,7 @@ public class CacheImporter @Inject constructor(
INSERT INTO caches (id) INSERT INTO caches (id)
VALUES (DEFAULT) VALUES (DEFAULT)
RETURNING id RETURNING id
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.executeQuery().use { rows -> stmt.executeQuery().use { rows ->
check(rows.next()) check(rows.next())
@ -1232,7 +1126,7 @@ public class CacheImporter @Inject constructor(
""" """
INSERT INTO crc_tables (id, blob_id) INSERT INTO crc_tables (id, blob_id)
VALUES (?, ?) VALUES (?, ?)
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.setInt(1, checksumTableId) stmt.setInt(1, checksumTableId)
stmt.setLong(2, blobId) stmt.setLong(2, blobId)
@ -1244,7 +1138,7 @@ public class CacheImporter @Inject constructor(
""" """
INSERT INTO crc_table_archives (crc_table_id, archive_id, crc32) INSERT INTO crc_table_archives (crc_table_id, archive_id, crc32)
VALUES (?, ?, ?) VALUES (?, ?, ?)
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
for ((i, entry) in checksumTable.table.entries.withIndex()) { for ((i, entry) in checksumTable.table.entries.withIndex()) {
stmt.setInt(1, checksumTableId) stmt.setInt(1, checksumTableId)
@ -1282,7 +1176,7 @@ public class CacheImporter @Inject constructor(
INSERT INTO archives (archive_id, blob_id) INSERT INTO archives (archive_id, blob_id)
VALUES (?, ?) VALUES (?, ?)
ON CONFLICT DO NOTHING ON CONFLICT DO NOTHING
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.setInt(1, archive.id) stmt.setInt(1, archive.id)
stmt.setLong(2, blobId) stmt.setLong(2, blobId)
@ -1295,7 +1189,7 @@ public class CacheImporter @Inject constructor(
INSERT INTO source_archives (source_id, archive_id, blob_id) INSERT INTO source_archives (source_id, archive_id, blob_id)
VALUES (?, ?, ?) VALUES (?, ?, ?)
ON CONFLICT DO NOTHING ON CONFLICT DO NOTHING
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.setInt(1, sourceId) stmt.setInt(1, sourceId)
stmt.setInt(2, archive.id) stmt.setInt(2, archive.id)
@ -1311,7 +1205,7 @@ public class CacheImporter @Inject constructor(
""" """
INSERT INTO version_lists (blob_id) INSERT INTO version_lists (blob_id)
VALUES (?) VALUES (?)
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
try { try {
stmt.setLong(1, blobId) stmt.setLong(1, blobId)
@ -1330,7 +1224,7 @@ public class CacheImporter @Inject constructor(
""" """
INSERT INTO version_list_files (blob_id, index_id, file_id, version, crc32) INSERT INTO version_list_files (blob_id, index_id, file_id, version, crc32)
VALUES (?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?)
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
for ((indexId, files) in versionList.files.withIndex()) { for ((indexId, files) in versionList.files.withIndex()) {
for ((fileId, file) in files.withIndex()) { for ((fileId, file) in files.withIndex()) {
@ -1355,7 +1249,7 @@ public class CacheImporter @Inject constructor(
""" """
INSERT INTO version_list_maps (blob_id, map_square, map_file_id, loc_file_id, free_to_play) INSERT INTO version_list_maps (blob_id, map_square, map_file_id, loc_file_id, free_to_play)
VALUES (?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?)
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
for ((mapSquare, map) in versionList.maps) { for ((mapSquare, map) in versionList.maps) {
stmt.setLong(1, blobId) stmt.setLong(1, blobId)
@ -1386,7 +1280,7 @@ public class CacheImporter @Inject constructor(
} }
} }
internal fun addFiles(connection: Connection, sourceId: Int, files: List<File>) { private fun addFiles(connection: Connection, sourceId: Int, files: List<File>) {
val blobIds = addBlobs(connection, files) val blobIds = addBlobs(connection, files)
connection.prepareStatement( connection.prepareStatement(
@ -1394,7 +1288,7 @@ public class CacheImporter @Inject constructor(
INSERT INTO files (index_id, file_id, version, blob_id) INSERT INTO files (index_id, file_id, version, blob_id)
VALUES (?, ?, ?, ?) VALUES (?, ?, ?, ?)
ON CONFLICT DO NOTHING ON CONFLICT DO NOTHING
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
for ((i, file) in files.withIndex()) { for ((i, file) in files.withIndex()) {
stmt.setInt(1, file.index) stmt.setInt(1, file.index)
@ -1413,7 +1307,7 @@ public class CacheImporter @Inject constructor(
INSERT INTO source_files (source_id, index_id, file_id, version, blob_id) INSERT INTO source_files (source_id, index_id, file_id, version, blob_id)
VALUES (?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?)
ON CONFLICT DO NOTHING ON CONFLICT DO NOTHING
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
for ((i, file) in files.withIndex()) { for ((i, file) in files.withIndex()) {
stmt.setInt(1, sourceId) stmt.setInt(1, sourceId)
@ -1431,27 +1325,10 @@ public class CacheImporter @Inject constructor(
public suspend fun refreshViews() { public suspend fun refreshViews() {
database.execute { connection -> database.execute { connection ->
connection.prepareStatement(
"""
SELECT pg_try_advisory_lock(0)
""".trimIndent()
).use { stmt ->
stmt.executeQuery().use { rows ->
if (!rows.next()) {
throw IllegalStateException()
}
val locked = rows.getBoolean(1)
if (!locked) {
return@execute
}
}
}
connection.prepareStatement( connection.prepareStatement(
""" """
REFRESH MATERIALIZED VIEW CONCURRENTLY index_stats REFRESH MATERIALIZED VIEW CONCURRENTLY index_stats
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.execute() stmt.execute()
} }
@ -1459,7 +1336,7 @@ public class CacheImporter @Inject constructor(
connection.prepareStatement( connection.prepareStatement(
""" """
REFRESH MATERIALIZED VIEW CONCURRENTLY master_index_stats REFRESH MATERIALIZED VIEW CONCURRENTLY master_index_stats
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.execute() stmt.execute()
} }
@ -1467,7 +1344,7 @@ public class CacheImporter @Inject constructor(
connection.prepareStatement( connection.prepareStatement(
""" """
REFRESH MATERIALIZED VIEW CONCURRENTLY version_list_stats REFRESH MATERIALIZED VIEW CONCURRENTLY version_list_stats
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.execute() stmt.execute()
} }
@ -1475,7 +1352,7 @@ public class CacheImporter @Inject constructor(
connection.prepareStatement( connection.prepareStatement(
""" """
REFRESH MATERIALIZED VIEW CONCURRENTLY crc_table_stats REFRESH MATERIALIZED VIEW CONCURRENTLY crc_table_stats
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.execute() stmt.execute()
} }

@ -1,16 +0,0 @@
package org.openrs2.archive.cache
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 CrossPollinateCommand : CliktCommand(name = "cross-pollinate") {
override fun run(): Unit = runBlocking {
CloseableInjector(Guice.createInjector(ArchiveModule)).use { injector ->
val crossPollinator = injector.getInstance(CrossPollinator::class.java)
crossPollinator.crossPollinate()
}
}
}

@ -1,223 +0,0 @@
package org.openrs2.archive.cache
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
import org.openrs2.cache.Js5CompressionType
import org.openrs2.db.Database
import java.sql.Connection
import java.util.zip.GZIPInputStream
@Singleton
public class CrossPollinator @Inject constructor(
private val database: Database,
private val alloc: ByteBufAllocator,
private val importer: CacheImporter
) {
public suspend fun crossPollinate() {
database.execute { connection ->
for ((index, archive) in OLD_TO_NEW_ENGINE) {
crossPollinate(connection, index, archive)
}
}
}
private fun crossPollinate(connection: Connection, index: Int, archive: Int) {
val scopeId: Int
connection.prepareStatement(
"""
SELECT id
FROM scopes
WHERE name = 'runescape'
""".trimIndent()
).use { stmt ->
stmt.executeQuery().use { rows ->
check(rows.next())
scopeId = rows.getInt(1)
}
}
val groups = mutableListOf<CacheImporter.Group>()
val files = mutableListOf<CacheImporter.File>()
try {
connection.prepareStatement(
"""
SELECT
new.group_id AS id,
old.version AS old_version,
old.crc32 AS old_crc32,
b.data AS old_data,
new.version AS new_version,
new.crc32 AS new_crc32,
c.data AS new_data
FROM (
SELECT DISTINCT vf.index_id, vf.file_id, vf.version, vf.crc32
FROM version_list_files vf
WHERE vf.blob_id IN (
SELECT v.blob_id
FROM version_lists v
JOIN resolved_archives a ON a.blob_id = v.blob_id AND a.archive_id = 5
) AND vf.index_id = ?
) old
JOIN (
SELECT DISTINCT ig.group_id, ig.version, ig.crc32
FROM index_groups ig
WHERE ig.container_id IN (
SELECT i.container_id
FROM resolved_indexes i
WHERE i.scope_id = ? AND i.archive_id = ?
)
) new ON old.file_id = new.group_id AND old.version = new.version + 1
LEFT JOIN resolve_file(old.index_id, old.file_id, old.version, old.crc32) b ON TRUE
LEFT JOIN resolve_group(?, ?::uint1, new.group_id, new.crc32, new.version) c ON TRUE
WHERE (b.data IS NULL AND c.data IS NOT NULL) OR (b.data IS NOT NULL AND c.data IS NULL)
""".trimIndent()
).use { stmt ->
stmt.setInt(1, index)
stmt.setInt(2, scopeId)
stmt.setInt(3, archive)
stmt.setInt(4, scopeId)
stmt.setInt(5, archive)
stmt.executeQuery().use { rows ->
while (rows.next()) {
val id = rows.getInt(1)
val oldVersion = rows.getInt(2)
val oldChecksum = rows.getInt(3)
val newVersion = rows.getInt(5)
val newChecksum = rows.getInt(6)
val oldData = rows.getBytes(4)
if (oldData != null) {
Unpooled.wrappedBuffer(oldData).use { oldBuf ->
fileToGroup(oldBuf, newChecksum).use { newBuf ->
if (newBuf != null) {
val uncompressed = Js5Compression.uncompressUnlessEncrypted(newBuf.slice())
groups += CacheImporter.Group(
archive,
id,
newBuf.retain(),
uncompressed,
newVersion,
false
)
}
}
}
}
val newData = rows.getBytes(7)
if (newData != null) {
Unpooled.wrappedBuffer(newData).use { newBuf ->
val oldBuf = groupToFile(newBuf, oldChecksum)
if (oldBuf != null) {
files += CacheImporter.File(index, id, oldBuf, oldVersion)
}
}
}
}
}
}
if (groups.isEmpty() && files.isEmpty()) {
return
}
importer.prepare(connection)
val sourceId = importer.addSource(
connection,
type = CacheImporter.SourceType.CROSS_POLLINATION,
cacheId = null,
gameId = null,
buildMajor = null,
buildMinor = null,
timestamp = null,
name = null,
description = null,
url = null,
)
if (groups.isNotEmpty()) {
importer.addGroups(connection, scopeId, sourceId, groups)
}
if (files.isNotEmpty()) {
importer.addFiles(connection, sourceId, files)
}
} finally {
groups.forEach(CacheImporter.Group::release)
files.forEach(CacheImporter.File::release)
}
}
private fun getUncompressedLength(buf: ByteBuf): Int {
GZIPInputStream(ByteBufInputStream(buf)).use { input ->
var len = 0
val temp = ByteArray(4096)
while (true) {
val n = input.read(temp)
if (n == -1) {
break
}
len += n
}
return len
}
}
private fun fileToGroup(input: ByteBuf, expectedChecksum: Int): ByteBuf? {
val len = input.readableBytes()
val lenWithHeader = len + JS5_COMPRESSION_HEADER_LEN
val uncompressedLen = getUncompressedLength(input.slice())
alloc.buffer(lenWithHeader, lenWithHeader).use { output ->
output.writeByte(Js5CompressionType.GZIP.ordinal)
output.writeInt(len)
output.writeInt(uncompressedLen)
output.writeBytes(input)
return if (output.crc32() == expectedChecksum) {
output.retain()
} else {
null
}
}
}
private fun groupToFile(input: ByteBuf, expectedChecksum: Int): ByteBuf? {
val type = Js5CompressionType.fromOrdinal(input.readUnsignedByte().toInt())
if (type != Js5CompressionType.GZIP) {
return null
}
input.skipBytes(JS5_COMPRESSION_HEADER_LEN - 1)
return if (input.crc32() == expectedChecksum) {
input.retainedSlice()
} else {
null
}
}
private companion object {
private val OLD_TO_NEW_ENGINE = mapOf(
1 to 7, // MODELS
3 to 6, // MIDI_SONGS
4 to 5, // MAPS
)
private const val JS5_COMPRESSION_HEADER_LEN = 9
}
}

@ -2,8 +2,6 @@ package org.openrs2.archive.cache
import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.parameters.arguments.argument import com.github.ajalt.clikt.parameters.arguments.argument
import com.github.ajalt.clikt.parameters.options.default
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.types.int import com.github.ajalt.clikt.parameters.types.int
import com.github.ajalt.clikt.parameters.types.path import com.github.ajalt.clikt.parameters.types.path
import com.google.inject.Guice import com.google.inject.Guice
@ -13,7 +11,6 @@ import org.openrs2.cache.DiskStore
import org.openrs2.inject.CloseableInjector import org.openrs2.inject.CloseableInjector
public class ExportCommand : CliktCommand(name = "export") { public class ExportCommand : CliktCommand(name = "export") {
private val scope by option().default("runescape")
private val id by argument().int() private val id by argument().int()
private val output by argument().path( private val output by argument().path(
mustExist = true, mustExist = true,
@ -26,7 +23,7 @@ public class ExportCommand : CliktCommand(name = "export") {
CloseableInjector(Guice.createInjector(ArchiveModule)).use { injector -> CloseableInjector(Guice.createInjector(ArchiveModule)).use { injector ->
val exporter = injector.getInstance(CacheExporter::class.java) val exporter = injector.getInstance(CacheExporter::class.java)
exporter.export(scope, id) { legacy -> exporter.export(id) { legacy ->
DiskStore.create(output, legacy = legacy) DiskStore.create(output, legacy = legacy)
} }
} }

@ -3,11 +3,11 @@ package org.openrs2.archive.cache
import com.github.michaelbull.logging.InlineLogger import com.github.michaelbull.logging.InlineLogger
import io.netty.bootstrap.Bootstrap import io.netty.bootstrap.Bootstrap
import io.netty.buffer.ByteBuf import io.netty.buffer.ByteBuf
import io.netty.channel.ChannelException
import io.netty.channel.ChannelHandler import io.netty.channel.ChannelHandler
import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelHandlerContext
import io.netty.channel.ChannelPipeline import io.netty.channel.ChannelPipeline
import io.netty.channel.SimpleChannelInboundHandler import io.netty.channel.SimpleChannelInboundHandler
import io.netty.handler.timeout.ReadTimeoutException
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.openrs2.buffer.crc32 import org.openrs2.buffer.crc32
import org.openrs2.buffer.use import org.openrs2.buffer.use
@ -16,7 +16,6 @@ import org.openrs2.cache.Js5Compression
import org.openrs2.cache.Js5Index import org.openrs2.cache.Js5Index
import org.openrs2.cache.Js5MasterIndex import org.openrs2.cache.Js5MasterIndex
import org.openrs2.cache.MasterIndexFormat import org.openrs2.cache.MasterIndexFormat
import java.io.IOException
import java.nio.channels.ClosedChannelException import java.nio.channels.ClosedChannelException
import java.time.Instant import java.time.Instant
import kotlin.coroutines.Continuation import kotlin.coroutines.Continuation
@ -26,7 +25,6 @@ import kotlin.coroutines.resumeWithException
@ChannelHandler.Sharable @ChannelHandler.Sharable
public abstract class Js5ChannelHandler( public abstract class Js5ChannelHandler(
private val bootstrap: Bootstrap, private val bootstrap: Bootstrap,
private val scopeId: Int,
private val gameId: Int, private val gameId: Int,
private val hostname: String, private val hostname: String,
private val port: Int, private val port: Int,
@ -153,17 +151,16 @@ public abstract class Js5ChannelHandler(
} }
} }
@Suppress("OVERRIDE_DEPRECATION")
override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) {
releaseGroups() releaseGroups()
if (state == State.RESUMING_CONTINUATION) { if (state == State.RESUMING_CONTINUATION) {
logger.warn(cause) { "Swallowing exception as continuation has already resumed" } 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 * We skip continuation resumption if there's a read timeout - this
* timeout - this allows channelInactive() to attempt to reconnect * allows channelInactive() to attempt to reconnect if we haven't
* if we haven't used too many reconnection attempts. * used too many reconnection attempts.
*/ */
state = State.RESUMING_CONTINUATION state = State.RESUMING_CONTINUATION
continuation.resumeWithException(cause) continuation.resumeWithException(cause)
@ -239,7 +236,7 @@ public abstract class Js5ChannelHandler(
if (groups.size >= CacheImporter.BATCH_SIZE || complete) { if (groups.size >= CacheImporter.BATCH_SIZE || complete) {
runBlocking { runBlocking {
importer.importGroups(scopeId, sourceId, groups) importer.importGroups(sourceId, groups)
} }
releaseGroups() releaseGroups()
@ -254,12 +251,6 @@ public abstract class Js5ChannelHandler(
continuation.resume(Unit) continuation.resume(Unit)
ctx.close() ctx.close()
} else {
/*
* Reset the number of reconnection attempts as we are making
* progress.
*/
reconnectionAttempts = 0
} }
} }
@ -277,7 +268,6 @@ public abstract class Js5ChannelHandler(
buf, buf,
uncompressed, uncompressed,
gameId, gameId,
scopeId,
buildMajor, buildMajor,
buildMinor, buildMinor,
lastMasterIndexId, lastMasterIndexId,
@ -325,15 +315,7 @@ public abstract class Js5ChannelHandler(
} }
val groups = runBlocking { val groups = runBlocking {
importer.importIndexAndGetMissingGroups( importer.importIndexAndGetMissingGroups(sourceId, archive, index, buf, uncompressed, lastMasterIndexId)
scopeId,
sourceId,
archive,
index,
buf,
uncompressed,
lastMasterIndexId
)
} }
for (group in groups) { for (group in groups) {
val groupEntry = index[group]!! val groupEntry = index[group]!!

@ -19,12 +19,11 @@ import org.openrs2.buffer.use
import org.openrs2.cache.MasterIndexFormat import org.openrs2.cache.MasterIndexFormat
import org.openrs2.protocol.Rs2Decoder import org.openrs2.protocol.Rs2Decoder
import org.openrs2.protocol.Rs2Encoder import org.openrs2.protocol.Rs2Encoder
import org.openrs2.protocol.js5.downstream.XorDecoder import org.openrs2.protocol.js5.XorDecoder
import kotlin.coroutines.Continuation import kotlin.coroutines.Continuation
public class NxtJs5ChannelHandler( public class NxtJs5ChannelHandler(
bootstrap: Bootstrap, bootstrap: Bootstrap,
scopeId: Int,
gameId: Int, gameId: Int,
hostname: String, hostname: String,
port: Int, port: Int,
@ -39,7 +38,6 @@ public class NxtJs5ChannelHandler(
private val maxMinorBuildAttempts: Int = 5 private val maxMinorBuildAttempts: Int = 5
) : Js5ChannelHandler( ) : Js5ChannelHandler(
bootstrap, bootstrap,
scopeId,
gameId, gameId,
hostname, hostname,
port, port,

@ -6,28 +6,26 @@ import io.netty.channel.ChannelPipeline
import org.openrs2.cache.MasterIndexFormat import org.openrs2.cache.MasterIndexFormat
import org.openrs2.protocol.Rs2Decoder import org.openrs2.protocol.Rs2Decoder
import org.openrs2.protocol.Rs2Encoder import org.openrs2.protocol.Rs2Encoder
import org.openrs2.protocol.js5.downstream.Js5LoginResponse import org.openrs2.protocol.js5.Js5Request
import org.openrs2.protocol.js5.downstream.Js5Response import org.openrs2.protocol.js5.Js5RequestEncoder
import org.openrs2.protocol.js5.downstream.Js5ResponseDecoder import org.openrs2.protocol.js5.Js5Response
import org.openrs2.protocol.js5.downstream.XorDecoder import org.openrs2.protocol.js5.Js5ResponseDecoder
import org.openrs2.protocol.js5.upstream.Js5Request import org.openrs2.protocol.js5.XorDecoder
import org.openrs2.protocol.js5.upstream.Js5RequestEncoder import org.openrs2.protocol.login.LoginRequest
import org.openrs2.protocol.login.upstream.LoginRequest import org.openrs2.protocol.login.LoginResponse
import kotlin.coroutines.Continuation import kotlin.coroutines.Continuation
public class OsrsJs5ChannelHandler( public class OsrsJs5ChannelHandler(
bootstrap: Bootstrap, bootstrap: Bootstrap,
scopeId: Int,
gameId: Int, gameId: Int,
hostname: String, hostname: String,
port: Int, port: Int,
build: Int, build: Int,
lastMasterIndexId: Int?, lastMasterIndexId: Int?,
continuation: Continuation<Unit>, continuation: Continuation<Unit>,
importer: CacheImporter importer: CacheImporter,
) : Js5ChannelHandler( ) : Js5ChannelHandler(
bootstrap, bootstrap,
scopeId,
gameId, gameId,
hostname, hostname,
port, port,
@ -66,9 +64,9 @@ public class OsrsJs5ChannelHandler(
override fun channelRead0(ctx: ChannelHandlerContext, msg: Any) { override fun channelRead0(ctx: ChannelHandlerContext, msg: Any) {
when (msg) { when (msg) {
is Js5LoginResponse.Ok -> handleOk(ctx) is LoginResponse.Js5Ok -> handleOk(ctx)
is Js5LoginResponse.ClientOutOfDate -> handleClientOutOfDate(ctx) is LoginResponse.ClientOutOfDate -> handleClientOutOfDate(ctx)
is Js5LoginResponse -> throw Exception("Invalid response: $msg") is LoginResponse -> throw Exception("Invalid response: $msg")
is Js5Response -> handleResponse(ctx, msg.prefetch, msg.archive, msg.group, msg.data) is Js5Response -> handleResponse(ctx, msg.prefetch, msg.archive, msg.group, msg.data)
else -> throw Exception("Unknown message type: ${msg.javaClass.name}") else -> throw Exception("Unknown message type: ${msg.javaClass.name}")
} }

@ -6,16 +6,13 @@ import io.netty.handler.timeout.ReadTimeoutHandler
import org.openrs2.protocol.Protocol import org.openrs2.protocol.Protocol
import org.openrs2.protocol.Rs2Decoder import org.openrs2.protocol.Rs2Decoder
import org.openrs2.protocol.Rs2Encoder import org.openrs2.protocol.Rs2Encoder
import org.openrs2.protocol.js5.downstream.Js5ClientOutOfDateCodec
import org.openrs2.protocol.js5.downstream.Js5OkCodec
import org.openrs2.protocol.login.upstream.InitJs5RemoteConnectionCodec
public class OsrsJs5ChannelInitializer(private val handler: OsrsJs5ChannelHandler) : ChannelInitializer<Channel>() { public class OsrsJs5ChannelInitializer(private val handler: OsrsJs5ChannelHandler) : ChannelInitializer<Channel>() {
override fun initChannel(ch: Channel) { override fun initChannel(ch: Channel) {
ch.pipeline().addLast( ch.pipeline().addLast(
ReadTimeoutHandler(30), ReadTimeoutHandler(30),
Rs2Encoder(Protocol(InitJs5RemoteConnectionCodec())), Rs2Encoder(Protocol.LOGIN_UPSTREAM),
Rs2Decoder(Protocol(Js5OkCodec(), Js5ClientOutOfDateCodec())) Rs2Decoder(Protocol.LOGIN_DOWNSTREAM)
) )
ch.pipeline().addLast("handler", handler) ch.pipeline().addLast("handler", handler)
} }

@ -1,149 +0,0 @@
package org.openrs2.archive.cache.finder
import com.github.michaelbull.logging.InlineLogger
import com.google.common.io.ByteStreams
import com.google.common.io.LittleEndianDataInputStream
import org.openrs2.util.charset.Cp1252Charset
import java.io.Closeable
import java.io.EOFException
import java.io.IOException
import java.io.InputStream
import java.io.PushbackInputStream
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.attribute.BasicFileAttributeView
import java.nio.file.attribute.FileTime
import java.time.Instant
public class CacheFinderExtractor(
input: InputStream
) : Closeable {
private val pushbackInput = PushbackInputStream(input)
private val input = LittleEndianDataInputStream(pushbackInput)
private fun readTimestamp(): FileTime {
val lo = input.readInt().toLong() and 0xFFFFFFFF
val hi = input.readInt().toLong() and 0xFFFFFFFF
val seconds = (((hi shl 32) or lo) / 10_000_000) - FILETIME_TO_UNIX_EPOCH
return FileTime.from(Instant.ofEpochSecond(seconds, lo))
}
private fun readName(): String {
val bytes = ByteArray(MAX_PATH)
input.readFully(bytes)
var len = bytes.size
for ((i, b) in bytes.withIndex()) {
if (b.toInt() == 0) {
len = i
break
}
}
return String(bytes, 0, len, Cp1252Charset)
}
private fun peekUnsignedByte(): Int {
val n = pushbackInput.read()
pushbackInput.unread(n)
return n
}
public fun extract(destination: Path) {
val newVersion = peekUnsignedByte() == 0xFE
if (newVersion) {
val signature = input.readInt()
if (signature != 0x435352FE) {
throw IOException("Invalid signature")
}
}
var readDirectoryPath = true
var number = 0
var directorySuffix: String? = null
while (true) {
if (newVersion && readDirectoryPath) {
val len = try {
input.readInt()
} catch (ex: EOFException) {
break
}
val bytes = ByteArray(len)
input.readFully(bytes)
val path = String(bytes, Cp1252Charset)
logger.info { "Extracting $path" }
readDirectoryPath = false
directorySuffix = path.substring(path.lastIndexOf('\\') + 1)
.replace(INVALID_CHARS, "_")
continue
}
if (peekUnsignedByte() == 0xFF) {
input.skipBytes(1)
readDirectoryPath = true
number++
continue
}
val attributes = try {
input.readInt()
} catch (ex: EOFException) {
break
}
val btime = readTimestamp()
val atime = readTimestamp()
val mtime = readTimestamp()
val sizeHi = input.readInt().toLong() and 0xFFFFFFFF
val sizeLo = input.readInt().toLong() and 0xFFFFFFFF
val size = (sizeHi shl 32) or sizeLo
input.skipBytes(8) // reserved
val name = readName()
input.skipBytes(14) // alternate name
input.skipBytes(2) // padding
val dir = if (directorySuffix != null) {
destination.resolve("cache${number}_$directorySuffix")
} else {
destination.resolve("cache$number")
}
Files.createDirectories(dir)
if ((attributes and FILE_ATTRIBUTE_DIRECTORY) == 0) {
val file = dir.resolve(name)
Files.newOutputStream(file).use { output ->
ByteStreams.copy(ByteStreams.limit(input, size), output)
}
val view = Files.getFileAttributeView(file, BasicFileAttributeView::class.java)
view.setTimes(mtime, atime, btime)
}
}
}
override fun close() {
input.close()
}
private companion object {
private const val FILETIME_TO_UNIX_EPOCH: Long = 11644473600
private const val MAX_PATH = 260
private const val FILE_ATTRIBUTE_DIRECTORY = 0x10
private val INVALID_CHARS = Regex("[^A-Za-z0-9-]")
private val logger = InlineLogger()
}
}

@ -1,25 +0,0 @@
package org.openrs2.archive.cache.finder
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.parameters.arguments.argument
import com.github.ajalt.clikt.parameters.arguments.default
import com.github.ajalt.clikt.parameters.types.inputStream
import com.github.ajalt.clikt.parameters.types.path
import java.nio.file.Path
public class ExtractCommand : CliktCommand(name = "extract") {
private val input by argument().inputStream()
private val output by argument().path(
mustExist = false,
canBeFile = false,
canBeDir = true,
mustBeReadable = true,
mustBeWritable = true
).default(Path.of("."))
override fun run() {
CacheFinderExtractor(input).use { extractor ->
extractor.extract(output)
}
}
}

@ -4,11 +4,13 @@ import io.netty.buffer.ByteBuf
import org.openrs2.buffer.readString import org.openrs2.buffer.readString
import org.openrs2.buffer.writeString import org.openrs2.buffer.writeString
import org.openrs2.crypto.StreamCipher import org.openrs2.crypto.StreamCipher
import org.openrs2.protocol.VariableBytePacketCodec import org.openrs2.protocol.PacketCodec
import org.openrs2.protocol.PacketLength
public object InitJs5RemoteConnectionCodec : VariableBytePacketCodec<InitJs5RemoteConnection>( public object InitJs5RemoteConnectionCodec : PacketCodec<InitJs5RemoteConnection>(
type = InitJs5RemoteConnection::class.java, length = PacketLength.VARIABLE_BYTE,
opcode = 15 opcode = 15,
type = InitJs5RemoteConnection::class.java
) { ) {
override fun decode(input: ByteBuf, cipher: StreamCipher): InitJs5RemoteConnection { override fun decode(input: ByteBuf, cipher: StreamCipher): InitJs5RemoteConnection {
val buildMajor = input.readInt() val buildMajor = input.readInt()

@ -1,8 +1,25 @@
package org.openrs2.archive.cache.nxt package org.openrs2.archive.cache.nxt
import org.openrs2.protocol.EmptyPacketCodec import io.netty.buffer.ByteBuf
import org.openrs2.crypto.StreamCipher
import org.openrs2.protocol.PacketCodec
public object Js5OkCodec : EmptyPacketCodec<LoginResponse.Js5Ok>( public object Js5OkCodec : PacketCodec<LoginResponse.Js5Ok>(
opcode = 0, opcode = 0,
packet = LoginResponse.Js5Ok length = LoginResponse.Js5Ok.LOADING_REQUIREMENTS * 4,
) type = LoginResponse.Js5Ok::class.java
) {
override fun decode(input: ByteBuf, cipher: StreamCipher): LoginResponse.Js5Ok {
val loadingRequirements = mutableListOf<Int>()
for (i in 0 until LoginResponse.Js5Ok.LOADING_REQUIREMENTS) {
loadingRequirements += input.readInt()
}
return LoginResponse.Js5Ok(loadingRequirements)
}
override fun encode(input: LoginResponse.Js5Ok, output: ByteBuf, cipher: StreamCipher) {
for (requirement in input.loadingRequirements) {
output.writeInt(requirement)
}
}
}

@ -16,7 +16,6 @@ public object Js5RequestEncoder : MessageToByteEncoder<Js5Request>(Js5Request::c
out.writeShort(msg.build) out.writeShort(msg.build)
out.writeShort(0) out.writeShort(0)
} }
is Js5Request.Connected -> { is Js5Request.Connected -> {
out.writeByte(6) out.writeByte(6)
out.writeMedium(5) out.writeMedium(5)

@ -38,10 +38,10 @@ public class Js5ResponseDecoder : ByteToMessageDecoder() {
request = Request(prefetch, archive, group) request = Request(prefetch, archive, group)
state = if (buffers.containsKey(request)) { if (buffers.containsKey(request)) {
State.READ_DATA state = State.READ_DATA
} else { } else {
State.READ_LEN state = State.READ_LEN
} }
} }

@ -3,6 +3,11 @@ package org.openrs2.archive.cache.nxt
import org.openrs2.protocol.Packet import org.openrs2.protocol.Packet
public sealed class LoginResponse : Packet { public sealed class LoginResponse : Packet {
public object Js5Ok : LoginResponse() public data class Js5Ok(val loadingRequirements: List<Int>) : LoginResponse() {
public companion object {
public const val LOADING_REQUIREMENTS: Int = 31
}
}
public object ClientOutOfDate : LoginResponse() public object ClientOutOfDate : LoginResponse()
} }

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

@ -6,6 +6,5 @@ public data class Game(
public val buildMajor: Int?, public val buildMajor: Int?,
public val buildMinor: Int?, public val buildMinor: Int?,
public val lastMasterIndexId: Int?, public val lastMasterIndexId: Int?,
public val languageId: Int, public val languageId: Int
public val scopeId: Int
) )

@ -1,8 +1,8 @@
package org.openrs2.archive.game package org.openrs2.archive.game
import jakarta.inject.Inject
import jakarta.inject.Singleton
import org.openrs2.db.Database import org.openrs2.db.Database
import javax.inject.Inject
import javax.inject.Singleton
@Singleton @Singleton
public class GameDatabase @Inject constructor( public class GameDatabase @Inject constructor(
@ -12,13 +12,13 @@ public class GameDatabase @Inject constructor(
return database.execute { connection -> return database.execute { connection ->
connection.prepareStatement( connection.prepareStatement(
""" """
SELECT v.id, v.url, v.build_major, v.build_minor, v.last_master_index_id, v.language_id, g.scope_id SELECT v.id, v.url, v.build_major, v.build_minor, v.last_master_index_id, v.language_id
FROM game_variants v FROM game_variants v
JOIN games g ON g.id = v.game_id JOIN games g ON g.id = v.game_id
JOIN environments e ON e.id = v.environment_id JOIN environments e ON e.id = v.environment_id
JOIN languages l ON l.id = v.language_id JOIN languages l ON l.id = v.language_id
WHERE g.name = ? AND e.name = ? AND l.iso_code = ? WHERE g.name = ? AND e.name = ? AND l.iso_code = ?
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.setString(1, name) stmt.setString(1, name)
stmt.setString(2, environment) stmt.setString(2, environment)
@ -48,9 +48,8 @@ public class GameDatabase @Inject constructor(
} }
val languageId = rows.getInt(6) val languageId = rows.getInt(6)
val scopeId = rows.getInt(7)
return@execute Game(id, url, buildMajor, buildMinor, lastMasterIndexId, languageId, scopeId) return@execute Game(id, url, buildMajor, buildMinor, lastMasterIndexId, languageId)
} }
} }
} }

@ -47,14 +47,12 @@ public data class JavConfig(
messages[parts[0]] = parts[1] messages[parts[0]] = parts[1]
} }
} }
line.startsWith("param=") -> { line.startsWith("param=") -> {
val parts = line.substring("param=".length).split("=", limit = 2) val parts = line.substring("param=".length).split("=", limit = 2)
if (parts.size == 2) { if (parts.size == 2) {
params[parts[0]] = parts[1] params[parts[0]] = parts[1]
} }
} }
else -> { else -> {
val parts = line.split("=", limit = 2) val parts = line.split("=", limit = 2)
if (parts.size == 2) { if (parts.size == 2) {

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

@ -1,57 +0,0 @@
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.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
@Singleton
public class HdosKeyDownloader @Inject constructor(
private val client: HttpClient
) : KeyDownloader(KeySource.HDOS) {
override suspend fun getMissingUrls(seenUrls: Set<String>): Set<String> {
return setOf(ENDPOINT)
}
override suspend fun download(url: String): Sequence<SymmetricKey> {
val request = HttpRequest.newBuilder(URI(url))
.GET()
.timeout(Duration.ofSeconds(30))
.build()
val response = client.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream()).await()
response.checkStatusCode()
return withContext(Dispatchers.IO) {
response.body().use { input ->
input.bufferedReader().use { reader ->
val keys = mutableSetOf<SymmetricKey>()
for (line in reader.lineSequence()) {
val parts = line.split(',')
if (parts.size < 3) {
continue
}
val key = SymmetricKey.fromHexOrNull(parts[2]) ?: continue
keys += key
}
keys.asSequence()
}
}
}
}
private companion object {
private const val ENDPOINT = "https://api.hdos.dev/keys/get"
}
}

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

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

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

@ -1,15 +1,15 @@
package org.openrs2.archive.key package org.openrs2.archive.key
import io.netty.buffer.Unpooled import io.netty.buffer.Unpooled
import jakarta.inject.Inject
import jakarta.inject.Singleton
import org.openrs2.buffer.crc32 import org.openrs2.buffer.crc32
import org.openrs2.buffer.use import org.openrs2.buffer.use
import org.openrs2.cache.Js5Compression import org.openrs2.cache.Js5Compression
import org.openrs2.crypto.SymmetricKey import org.openrs2.crypto.XteaKey
import org.openrs2.db.Database import org.openrs2.db.Database
import java.sql.Connection import java.sql.Connection
import java.sql.Types import java.sql.Types
import javax.inject.Inject
import javax.inject.Singleton
@Singleton @Singleton
public class KeyBruteForcer @Inject constructor( public class KeyBruteForcer @Inject constructor(
@ -36,7 +36,7 @@ public class KeyBruteForcer @Inject constructor(
connection.prepareStatement( connection.prepareStatement(
""" """
LOCK TABLE keys IN EXCLUSIVE MODE LOCK TABLE keys IN EXCLUSIVE MODE
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.execute() stmt.execute()
} }
@ -49,7 +49,7 @@ public class KeyBruteForcer @Inject constructor(
first_seen TIMESTAMPTZ NOT NULL, first_seen TIMESTAMPTZ NOT NULL,
last_seen TIMESTAMPTZ NOT NULL last_seen TIMESTAMPTZ NOT NULL
) ON COMMIT DROP ) ON COMMIT DROP
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.execute() stmt.execute()
} }
@ -60,7 +60,7 @@ public class KeyBruteForcer @Inject constructor(
SELECT key, source, first_seen, last_seen SELECT key, source, first_seen, last_seen
FROM key_queue FROM key_queue
FOR UPDATE SKIP LOCKED FOR UPDATE SKIP LOCKED
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.execute() stmt.execute()
} }
@ -73,7 +73,7 @@ public class KeyBruteForcer @Inject constructor(
LEFT JOIN keys k ON k.key = t.key LEFT JOIN keys k ON k.key = t.key
WHERE k.key IS NULL WHERE k.key IS NULL
ON CONFLICT DO NOTHING ON CONFLICT DO NOTHING
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.execute() stmt.execute()
} }
@ -87,7 +87,7 @@ public class KeyBruteForcer @Inject constructor(
ON CONFLICT (key_id, source) DO UPDATE SET ON CONFLICT (key_id, source) DO UPDATE SET
first_seen = LEAST(s.first_seen, EXCLUDED.first_seen), first_seen = LEAST(s.first_seen, EXCLUDED.first_seen),
last_seen = GREATEST(s.last_seen, EXCLUDED.last_seen) last_seen = GREATEST(s.last_seen, EXCLUDED.last_seen)
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.execute() stmt.execute()
} }
@ -97,7 +97,7 @@ public class KeyBruteForcer @Inject constructor(
DELETE FROM key_queue k DELETE FROM key_queue k
USING tmp_keys t USING tmp_keys t
WHERE k.key = t.key AND k.source = t.source WHERE k.key = t.key AND k.source = t.source
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.execute() stmt.execute()
} }
@ -152,7 +152,7 @@ public class KeyBruteForcer @Inject constructor(
connection.prepareStatement( connection.prepareStatement(
""" """
LOCK TABLE containers, keys IN SHARE MODE LOCK TABLE containers, keys IN SHARE MODE
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.execute() stmt.execute()
} }
@ -170,7 +170,7 @@ public class KeyBruteForcer @Inject constructor(
SELECT last_container_id SELECT last_container_id
FROM brute_force_iterator FROM brute_force_iterator
FOR UPDATE FOR UPDATE
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.executeQuery().use { rows -> stmt.executeQuery().use { rows ->
check(rows.next()) check(rows.next())
@ -191,7 +191,7 @@ public class KeyBruteForcer @Inject constructor(
""" """
SELECT id, (key).k0, (key).k1, (key).k2, (key).k3 SELECT id, (key).k0, (key).k1, (key).k2, (key).k3
FROM keys FROM keys
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.fetchSize = BATCH_SIZE stmt.fetchSize = BATCH_SIZE
@ -203,7 +203,7 @@ public class KeyBruteForcer @Inject constructor(
val k1 = rows.getInt(3) val k1 = rows.getInt(3)
val k2 = rows.getInt(4) val k2 = rows.getInt(4)
val k3 = rows.getInt(5) 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) validatedKey = validateKey(data, key, keyId, containerId)
if (validatedKey != null) { if (validatedKey != null) {
@ -224,7 +224,7 @@ public class KeyBruteForcer @Inject constructor(
""" """
UPDATE brute_force_iterator UPDATE brute_force_iterator
SET last_container_id = ? SET last_container_id = ?
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.setObject(1, lastContainerId, Types.BIGINT) stmt.setObject(1, lastContainerId, Types.BIGINT)
stmt.execute() stmt.execute()
@ -239,7 +239,7 @@ public class KeyBruteForcer @Inject constructor(
WHERE (? IS NULL OR id > ?) AND encrypted AND key_id IS NULL WHERE (? IS NULL OR id > ?) AND encrypted AND key_id IS NULL
ORDER BY id ASC ORDER BY id ASC
LIMIT 1 LIMIT 1
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.setObject(1, lastContainerId, Types.BIGINT) stmt.setObject(1, lastContainerId, Types.BIGINT)
stmt.setObject(2, lastContainerId, Types.BIGINT) stmt.setObject(2, lastContainerId, Types.BIGINT)
@ -265,7 +265,7 @@ public class KeyBruteForcer @Inject constructor(
SELECT last_key_id, last_container_id SELECT last_key_id, last_container_id
FROM brute_force_iterator FROM brute_force_iterator
FOR UPDATE FOR UPDATE
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.executeQuery().use { rows -> stmt.executeQuery().use { rows ->
check(rows.next()) check(rows.next())
@ -292,7 +292,7 @@ public class KeyBruteForcer @Inject constructor(
SELECT id, data SELECT id, data
FROM containers FROM containers
WHERE encrypted AND key_id IS NULL AND id <= ? WHERE encrypted AND key_id IS NULL AND id <= ?
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.fetchSize = BATCH_SIZE stmt.fetchSize = BATCH_SIZE
stmt.setLong(1, lastContainerId) stmt.setLong(1, lastContainerId)
@ -319,14 +319,14 @@ public class KeyBruteForcer @Inject constructor(
""" """
UPDATE brute_force_iterator UPDATE brute_force_iterator
SET last_key_id = ? SET last_key_id = ?
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.setObject(1, lastKeyId, Types.BIGINT) stmt.setObject(1, lastKeyId, Types.BIGINT)
stmt.execute() stmt.execute()
} }
} }
private fun nextKey(connection: Connection, lastKeyId: Long?): Pair<Long, SymmetricKey>? { private fun nextKey(connection: Connection, lastKeyId: Long?): Pair<Long, XteaKey>? {
connection.prepareStatement( connection.prepareStatement(
""" """
SELECT id, (key).k0, (key).k1, (key).k2, (key).k3 SELECT id, (key).k0, (key).k1, (key).k2, (key).k3
@ -334,7 +334,7 @@ public class KeyBruteForcer @Inject constructor(
WHERE ? IS NULL OR id > ? WHERE ? IS NULL OR id > ?
ORDER BY id ASC ORDER BY id ASC
LIMIT 1 LIMIT 1
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.setObject(1, lastKeyId, Types.BIGINT) stmt.setObject(1, lastKeyId, Types.BIGINT)
stmt.setObject(2, lastKeyId, Types.BIGINT) stmt.setObject(2, lastKeyId, Types.BIGINT)
@ -350,7 +350,7 @@ public class KeyBruteForcer @Inject constructor(
val k1 = rows.getInt(3) val k1 = rows.getInt(3)
val k2 = rows.getInt(4) val k2 = rows.getInt(4)
val k3 = rows.getInt(5) val k3 = rows.getInt(5)
val key = SymmetricKey(k0, k1, k2, k3) val key = XteaKey(k0, k1, k2, k3)
return Pair(keyId, key) return Pair(keyId, key)
} }
@ -359,7 +359,7 @@ public class KeyBruteForcer @Inject constructor(
private fun validateKey( private fun validateKey(
data: ByteArray, data: ByteArray,
key: SymmetricKey, key: XteaKey,
keyId: Long, keyId: Long,
containerId: Long containerId: Long
): ValidatedKey? { ): ValidatedKey? {

@ -1,10 +1,10 @@
package org.openrs2.archive.key package org.openrs2.archive.key
import org.openrs2.crypto.SymmetricKey import org.openrs2.crypto.XteaKey
public abstract class KeyDownloader( public abstract class KeyDownloader(
public val source: KeySource public val source: KeySource
) { ) {
public abstract suspend fun getMissingUrls(seenUrls: Set<String>): Set<String> 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 package org.openrs2.archive.key
import jakarta.inject.Inject import org.openrs2.crypto.XteaKey
import jakarta.inject.Singleton
import org.openrs2.crypto.SymmetricKey
import org.openrs2.db.Database import org.openrs2.db.Database
import java.io.BufferedOutputStream import java.io.BufferedOutputStream
import java.io.DataOutputStream import java.io.DataOutputStream
import javax.inject.Inject
import javax.inject.Singleton
@Singleton @Singleton
public class KeyExporter @Inject constructor( public class KeyExporter @Inject constructor(
@ -51,7 +51,7 @@ public class KeyExporter @Inject constructor(
COUNT(*) FILTER (WHERE c.key_id IS NULL AND c.empty_loc) COUNT(*) FILTER (WHERE c.key_id IS NULL AND c.empty_loc)
FROM containers c FROM containers c
WHERE c.encrypted WHERE c.encrypted
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.executeQuery().use { rows -> stmt.executeQuery().use { rows ->
check(rows.next()) check(rows.next())
@ -69,7 +69,7 @@ public class KeyExporter @Inject constructor(
COUNT(DISTINCT k.id) FILTER (WHERE c.key_id IS NOT NULL) COUNT(DISTINCT k.id) FILTER (WHERE c.key_id IS NOT NULL)
FROM keys k FROM keys k
LEFT JOIN containers c ON c.key_id = k.id LEFT JOIN containers c ON c.key_id = k.id
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.executeQuery().use { rows -> stmt.executeQuery().use { rows ->
check(rows.next()) check(rows.next())
@ -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) return export(validOnly = false)
} }
public suspend fun exportValid(): List<SymmetricKey> { public suspend fun exportValid(): List<XteaKey> {
return export(validOnly = true) return export(validOnly = true)
} }
@ -116,7 +116,7 @@ public class KeyExporter @Inject constructor(
return analysis return analysis
} }
private suspend fun export(validOnly: Boolean): List<SymmetricKey> { private suspend fun export(validOnly: Boolean): List<XteaKey> {
return database.execute { connection -> return database.execute { connection ->
val query = if (validOnly) { val query = if (validOnly) {
EXPORT_VALID_QUERY EXPORT_VALID_QUERY
@ -126,14 +126,14 @@ public class KeyExporter @Inject constructor(
connection.prepareStatement(query).use { stmt -> connection.prepareStatement(query).use { stmt ->
stmt.executeQuery().use { rows -> stmt.executeQuery().use { rows ->
val keys = mutableListOf<SymmetricKey>() val keys = mutableListOf<XteaKey>()
while (rows.next()) { while (rows.next()) {
val k0 = rows.getInt(1) val k0 = rows.getInt(1)
val k1 = rows.getInt(2) val k1 = rows.getInt(2)
val k2 = rows.getInt(3) val k2 = rows.getInt(3)
val k3 = rows.getInt(4) val k3 = rows.getInt(4)
keys += SymmetricKey(k0, k1, k2, k3) keys += XteaKey(k0, k1, k2, k3)
} }
keys keys

@ -1,17 +1,16 @@
package org.openrs2.archive.key package org.openrs2.archive.key
import com.github.michaelbull.logging.InlineLogger import com.github.michaelbull.logging.InlineLogger
import jakarta.inject.Inject import org.openrs2.crypto.XteaKey
import jakarta.inject.Singleton
import org.openrs2.crypto.SymmetricKey
import org.openrs2.db.Database import org.openrs2.db.Database
import java.io.IOException
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import java.sql.Connection import java.sql.Connection
import java.sql.Types import java.sql.Types
import java.time.Instant import java.time.Instant
import java.time.ZoneOffset import java.time.ZoneOffset
import javax.inject.Inject
import javax.inject.Singleton
@Singleton @Singleton
public class KeyImporter @Inject constructor( public class KeyImporter @Inject constructor(
@ -19,10 +18,10 @@ public class KeyImporter @Inject constructor(
private val jsonKeyReader: JsonKeyReader, private val jsonKeyReader: JsonKeyReader,
private val downloaders: Set<KeyDownloader> 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) { public suspend fun import(path: Path) {
val keys = mutableSetOf<SymmetricKey>() val keys = mutableSetOf<XteaKey>()
for (file in Files.walk(path)) { for (file in Files.walk(path)) {
if (!Files.isRegularFile(file)) { if (!Files.isRegularFile(file)) {
@ -45,7 +44,7 @@ public class KeyImporter @Inject constructor(
} }
} }
keys -= SymmetricKey.ZERO keys -= XteaKey.ZERO
logger.info { "Importing ${keys.size} keys" } logger.info { "Importing ${keys.size} keys" }
@ -59,7 +58,7 @@ public class KeyImporter @Inject constructor(
connection.prepareStatement( connection.prepareStatement(
""" """
SELECT url FROM keysets SELECT url FROM keysets
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.executeQuery().use { rows -> stmt.executeQuery().use { rows ->
val urls = mutableSetOf<String>() val urls = mutableSetOf<String>()
@ -75,16 +74,11 @@ public class KeyImporter @Inject constructor(
val urls = mutableSetOf<String>() val urls = mutableSetOf<String>()
for (downloader in downloaders) { for (downloader in downloaders) {
try { for (url in downloader.getMissingUrls(seenUrls)) {
for (url in downloader.getMissingUrls(seenUrls)) { keys += downloader.download(url).map { key ->
keys += downloader.download(url).map { key -> Key(key, downloader.source)
Key(key, downloader.source)
}
urls += url
} }
} catch (ex: IOException) { urls += url
logger.warn(ex) { "Failed to download keys from ${downloader.source.name}" }
continue
} }
} }
@ -94,7 +88,7 @@ public class KeyImporter @Inject constructor(
INSERT INTO keysets (url) INSERT INTO keysets (url)
VALUES (?) VALUES (?)
ON CONFLICT DO NOTHING ON CONFLICT DO NOTHING
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
for (url in urls) { for (url in urls) {
stmt.setString(1, url) stmt.setString(1, url)
@ -108,7 +102,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() val now = Instant.now()
database.execute { connection -> database.execute { connection ->
@ -128,7 +122,7 @@ public class KeyImporter @Inject constructor(
ON CONFLICT (key, source) DO UPDATE SET ON CONFLICT (key, source) DO UPDATE SET
first_seen = LEAST(k.first_seen, EXCLUDED.first_seen), first_seen = LEAST(k.first_seen, EXCLUDED.first_seen),
last_seen = GREATEST(k.last_seen, EXCLUDED.last_seen) last_seen = GREATEST(k.last_seen, EXCLUDED.last_seen)
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
for (key in keys) { for (key in keys) {
if (key.key.isZero) { if (key.key.isZero) {

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

@ -5,6 +5,5 @@ public enum class KeySource {
DISK, DISK,
OPENOSRS, OPENOSRS,
POLAR, POLAR,
RUNELITE, RUNELITE
HDOS
} }

@ -0,0 +1,19 @@
package org.openrs2.archive.key
import java.net.http.HttpClient
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
public class OpenOsrsKeyDownloader @Inject constructor(
client: HttpClient,
jsonKeyReader: JsonKeyReader
) : JsonKeyDownloader(KeySource.OPENOSRS, client, jsonKeyReader) {
override suspend fun getMissingUrls(seenUrls: Set<String>): Set<String> {
return setOf(ENDPOINT)
}
private companion object {
private const val ENDPOINT = "https://xtea.openosrs.dev/get"
}
}

@ -0,0 +1,50 @@
package org.openrs2.archive.key
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.future.await
import kotlinx.coroutines.withContext
import org.jsoup.Jsoup
import org.openrs2.http.charset
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 PolarKeyDownloader @Inject constructor(
private val client: HttpClient,
jsonKeyReader: JsonKeyReader
) : JsonKeyDownloader(KeySource.POLAR, client, jsonKeyReader) {
override suspend fun getMissingUrls(seenUrls: Set<String>): Set<String> {
val request = HttpRequest.newBuilder(ENDPOINT)
.GET()
.timeout(Duration.ofSeconds(30))
.build()
val response = client.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream()).await()
response.checkStatusCode()
val document = withContext(Dispatchers.IO) {
Jsoup.parse(response.body(), response.charset?.name(), ENDPOINT.toString())
}
val urls = mutableSetOf<String>()
for (element in document.select("a")) {
val url = element.absUrl("href")
if (url.endsWith(".json") && url !in seenUrls) {
urls += url
}
}
return urls
}
private companion object {
private val ENDPOINT = URI("https://archive.runestats.com/osrs/xtea/")
}
}

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

@ -1,10 +1,10 @@
package org.openrs2.archive.key package org.openrs2.archive.key
import org.openrs2.crypto.SymmetricKey import org.openrs2.crypto.XteaKey
import java.io.InputStream import java.io.InputStream
public object TextKeyReader : KeyReader { public object TextKeyReader : KeyReader {
override fun read(input: InputStream): Sequence<SymmetricKey> { override fun read(input: InputStream): Sequence<XteaKey> {
val reader = input.bufferedReader() val reader = input.bufferedReader()
val k0 = reader.readLine()?.toIntOrNull() ?: return emptySequence() val k0 = reader.readLine()?.toIntOrNull() ?: return emptySequence()
@ -12,6 +12,6 @@ public object TextKeyReader : KeyReader {
val k2 = reader.readLine()?.toIntOrNull() ?: return emptySequence() val k2 = reader.readLine()?.toIntOrNull() ?: return emptySequence()
val k3 = 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.ByteBuf
import io.netty.buffer.Unpooled import io.netty.buffer.Unpooled
import it.unimi.dsi.fastutil.ints.Int2ObjectSortedMap import it.unimi.dsi.fastutil.ints.Int2ObjectSortedMap
import jakarta.inject.Inject
import org.openrs2.buffer.use import org.openrs2.buffer.use
import org.openrs2.cache.Group import org.openrs2.cache.Group
import org.openrs2.cache.Js5Archive import org.openrs2.cache.Js5Archive
@ -15,6 +14,7 @@ import java.awt.Color
import java.awt.Graphics2D import java.awt.Graphics2D
import java.awt.image.BufferedImage import java.awt.image.BufferedImage
import java.sql.Connection import java.sql.Connection
import javax.inject.Inject
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
@ -31,28 +31,10 @@ public class MapRenderer @Inject constructor(
val fillColor = Color(outlineColor.red, outlineColor.green, outlineColor.blue, 128) val fillColor = Color(outlineColor.red, outlineColor.green, outlineColor.blue, 128)
} }
public suspend fun render(scope: String, masterIndexId: Int): BufferedImage { public suspend fun render(masterIndexId: Int): BufferedImage {
return database.execute { connection -> return database.execute { connection ->
val scopeId = connection.prepareStatement(
"""
SELECT id
FROM scopes
WHERE name = ?
""".trimIndent()
).use { stmt ->
stmt.setString(1, scope)
stmt.executeQuery().use { rows ->
if (!rows.next()) {
throw IllegalArgumentException("Invalid scope")
}
rows.getInt(1)
}
}
// read config index // read config index
val configIndex = readIndex(connection, scopeId, masterIndexId, Js5Archive.CONFIG) val configIndex = readIndex(connection, masterIndexId, Js5Archive.CONFIG)
?: throw IllegalArgumentException("Config index missing") ?: throw IllegalArgumentException("Config index missing")
// read FluType group // read FluType group
@ -61,7 +43,7 @@ public class MapRenderer @Inject constructor(
val underlayGroup = configIndex[Js5ConfigGroup.FLUTYPE] val underlayGroup = configIndex[Js5ConfigGroup.FLUTYPE]
?: throw IllegalArgumentException("FluType group missing in index") ?: throw IllegalArgumentException("FluType group missing in index")
val underlayFiles = readGroup(connection, scopeId, masterIndexId, Js5Archive.CONFIG, underlayGroup) val underlayFiles = readGroup(connection, masterIndexId, Js5Archive.CONFIG, underlayGroup)
?: throw IllegalArgumentException("FluType group missing") ?: throw IllegalArgumentException("FluType group missing")
try { try {
for ((id, file) in underlayFiles) { for ((id, file) in underlayFiles) {
@ -77,7 +59,7 @@ public class MapRenderer @Inject constructor(
val overlayGroup = configIndex[Js5ConfigGroup.FLOTYPE] val overlayGroup = configIndex[Js5ConfigGroup.FLOTYPE]
?: throw IllegalArgumentException("FloType group missing in index") ?: throw IllegalArgumentException("FloType group missing in index")
val overlayFiles = readGroup(connection, scopeId, masterIndexId, Js5Archive.CONFIG, overlayGroup) val overlayFiles = readGroup(connection, masterIndexId, Js5Archive.CONFIG, overlayGroup)
?: throw IllegalArgumentException("FloType group missing") ?: throw IllegalArgumentException("FloType group missing")
try { try {
for ((id, file) in overlayFiles) { for ((id, file) in overlayFiles) {
@ -89,13 +71,13 @@ public class MapRenderer @Inject constructor(
// read textures // read textures
val textures = mutableMapOf<Int, Int>() val textures = mutableMapOf<Int, Int>()
val materialsIndex = readIndex(connection, scopeId, masterIndexId, Js5Archive.MATERIALS) val materialsIndex = readIndex(connection, masterIndexId, Js5Archive.MATERIALS)
if (materialsIndex != null) { if (materialsIndex != null) {
val materialsGroup = materialsIndex[0] val materialsGroup = materialsIndex[0]
?: throw IllegalArgumentException("Materials group missing in index") ?: throw IllegalArgumentException("Materials group missing in index")
val materialsFiles = readGroup(connection, scopeId, masterIndexId, Js5Archive.MATERIALS, materialsGroup) val materialsFiles = readGroup(connection, masterIndexId, Js5Archive.MATERIALS, materialsGroup)
?: throw IllegalArgumentException("Materials group missing") ?: throw IllegalArgumentException("Materials group missing")
try { try {
val metadata = materialsFiles[0] val metadata = materialsFiles[0]
@ -141,13 +123,13 @@ public class MapRenderer @Inject constructor(
materialsFiles.values.forEach(ByteBuf::release) materialsFiles.values.forEach(ByteBuf::release)
} }
} else { } else {
val textureIndex = readIndex(connection, scopeId, masterIndexId, Js5Archive.TEXTURES) val textureIndex = readIndex(connection, masterIndexId, Js5Archive.TEXTURES)
?: throw IllegalArgumentException("Textures index missing") ?: throw IllegalArgumentException("Textures index missing")
val textureGroup = textureIndex[0] val textureGroup = textureIndex[0]
?: throw IllegalArgumentException("Textures group missing from index") ?: throw IllegalArgumentException("Textures group missing from index")
val textureFiles = readGroup(connection, scopeId, masterIndexId, Js5Archive.TEXTURES, textureGroup) val textureFiles = readGroup(connection, masterIndexId, Js5Archive.TEXTURES, textureGroup)
?: throw IllegalArgumentException("Textures group missing") ?: throw IllegalArgumentException("Textures group missing")
try { try {
for ((id, file) in textureFiles) { for ((id, file) in textureFiles) {
@ -173,12 +155,11 @@ public class MapRenderer @Inject constructor(
SELECT n.name, g.encrypted, g.empty_loc, g.key_id SELECT n.name, g.encrypted, g.empty_loc, g.key_id
FROM resolved_groups g FROM resolved_groups g
JOIN names n ON n.hash = g.name_hash JOIN names n ON n.hash = g.name_hash
WHERE g.scope_id = ? AND g.master_index_id = ? AND g.archive_id = ${Js5Archive.MAPS} AND WHERE g.master_index_id = ? AND g.archive_id = ${Js5Archive.MAPS} AND
n.name ~ '^[lm](?:[0-9]|[1-9][0-9])_(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$' n.name ~ '^[lm](?:[0-9]|[1-9][0-9])_(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$'
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.setInt(1, scopeId) stmt.setInt(1, masterIndexId)
stmt.setInt(2, masterIndexId)
stmt.executeQuery().use { rows -> stmt.executeQuery().use { rows ->
while (rows.next()) { while (rows.next()) {
@ -226,12 +207,11 @@ public class MapRenderer @Inject constructor(
SELECT n.name, g.data SELECT n.name, g.data
FROM resolved_groups g FROM resolved_groups g
JOIN names n ON n.hash = g.name_hash JOIN names n ON n.hash = g.name_hash
WHERE g.scope_id = ? AND g.master_index_id = ? AND g.archive_id = ${Js5Archive.MAPS} AND WHERE g.master_index_id = ? AND g.archive_id = ${Js5Archive.MAPS} AND
n.name ~ '^m(?:[0-9]|[1-9][0-9])_(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$' n.name ~ '^m(?:[0-9]|[1-9][0-9])_(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$'
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.setInt(1, scopeId) stmt.setInt(1, masterIndexId)
stmt.setInt(2, masterIndexId)
stmt.executeQuery().use { rows -> stmt.executeQuery().use { rows ->
while (rows.next()) { while (rows.next()) {
@ -266,17 +246,16 @@ public class MapRenderer @Inject constructor(
} }
} }
private fun readIndex(connection: Connection, scopeId: Int, masterIndexId: Int, archiveId: Int): Js5Index? { private fun readIndex(connection: Connection, masterIndexId: Int, archiveId: Int): Js5Index? {
connection.prepareStatement( connection.prepareStatement(
""" """
SELECT data SELECT data
FROM resolved_indexes FROM resolved_indexes
WHERE scope_id = ? AND master_index_id = ? AND archive_id = ? WHERE master_index_id = ? AND archive_id = ?
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.setInt(1, scopeId) stmt.setInt(1, masterIndexId)
stmt.setInt(2, masterIndexId) stmt.setInt(2, archiveId)
stmt.setInt(3, archiveId)
stmt.executeQuery().use { rows -> stmt.executeQuery().use { rows ->
if (!rows.next()) { if (!rows.next()) {
@ -296,7 +275,6 @@ public class MapRenderer @Inject constructor(
private fun readGroup( private fun readGroup(
connection: Connection, connection: Connection,
scopeId: Int,
masterIndexId: Int, masterIndexId: Int,
archiveId: Int, archiveId: Int,
group: Js5Index.Group<*> group: Js5Index.Group<*>
@ -305,13 +283,12 @@ public class MapRenderer @Inject constructor(
""" """
SELECT data SELECT data
FROM resolved_groups FROM resolved_groups
WHERE scope_id = ? AND master_index_id = ? AND archive_id = ? AND group_id = ? WHERE master_index_id = ? AND archive_id = ? AND group_id = ?
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
stmt.setInt(1, scopeId) stmt.setInt(1, masterIndexId)
stmt.setInt(2, masterIndexId) stmt.setInt(2, archiveId)
stmt.setInt(3, archiveId) stmt.setInt(3, group.id)
stmt.setInt(4, group.id)
stmt.executeQuery().use { rows -> stmt.executeQuery().use { rows ->
if (!rows.next()) { if (!rows.next()) {
@ -344,40 +321,6 @@ public class MapRenderer @Inject constructor(
} }
} }
private fun isShortCode(buf: ByteBuf): Boolean {
for (plane in 0 until LEVELS) {
for (dx in 0 until MAP_SIZE) {
for (dz in 0 until MAP_SIZE) {
while (true) {
if (buf.readableBytes() < 2) {
return false
}
val code = buf.readUnsignedShort()
if (code == 0) {
break
} else if (code == 1) {
if (!buf.isReadable) {
return false
}
buf.skipBytes(1)
break
} else if (code <= 49) {
if (buf.readableBytes() < 2) {
return false
}
buf.skipBytes(2)
}
}
}
}
}
return !buf.isReadable
}
private fun renderMap( private fun renderMap(
image: BufferedImage, image: BufferedImage,
x: Int, x: Int,
@ -386,12 +329,6 @@ public class MapRenderer @Inject constructor(
underlayColors: Map<Int, Int>, underlayColors: Map<Int, Int>,
overlayColors: Map<Int, Int> overlayColors: Map<Int, Int>
) { ) {
val readCode = if (isShortCode(buf.slice())) {
buf::readUnsignedShort
} else {
{ buf.readUnsignedByte().toInt() }
}
for (plane in 0 until LEVELS) { for (plane in 0 until LEVELS) {
for (dx in 0 until MAP_SIZE) { for (dx in 0 until MAP_SIZE) {
for (dz in 0 until MAP_SIZE) { for (dz in 0 until MAP_SIZE) {
@ -400,14 +337,14 @@ public class MapRenderer @Inject constructor(
var underlay = 0 var underlay = 0
while (true) { while (true) {
val code = readCode() val code = buf.readUnsignedByte().toInt()
if (code == 0) { if (code == 0) {
break break
} else if (code == 1) { } else if (code == 1) {
buf.skipBytes(1) buf.skipBytes(1)
break break
} else if (code <= 49) { } else if (code <= 49) {
overlay = readCode() overlay = buf.readUnsignedByte().toInt()
shape = (code - 2) shr 2 shape = (code - 2) shr 2
} else if (code <= 81) { } else if (code <= 81) {
// empty // empty

@ -1,9 +1,9 @@
package org.openrs2.archive.name package org.openrs2.archive.name
import jakarta.inject.Inject
import jakarta.inject.Singleton
import org.openrs2.db.Database import org.openrs2.db.Database
import org.openrs2.util.krHashCode import org.openrs2.util.krHashCode
import javax.inject.Inject
import javax.inject.Singleton
@Singleton @Singleton
public class NameImporter @Inject constructor( public class NameImporter @Inject constructor(
@ -25,7 +25,7 @@ public class NameImporter @Inject constructor(
INSERT INTO names (hash, name) INSERT INTO names (hash, name)
VALUES (?, ?) VALUES (?, ?)
ON CONFLICT DO NOTHING ON CONFLICT DO NOTHING
""".trimIndent() """.trimIndent()
).use { stmt -> ).use { stmt ->
for (name in names) { for (name in names) {
stmt.setInt(1, name.krHashCode()) stmt.setInt(1, name.krHashCode())

@ -1,7 +1,5 @@
package org.openrs2.archive.name package org.openrs2.archive.name
import jakarta.inject.Inject
import jakarta.inject.Singleton
import kotlinx.coroutines.future.await import kotlinx.coroutines.future.await
import org.openrs2.http.checkStatusCode import org.openrs2.http.checkStatusCode
import java.io.IOException import java.io.IOException
@ -10,6 +8,8 @@ import java.net.http.HttpClient
import java.net.http.HttpRequest import java.net.http.HttpRequest
import java.net.http.HttpResponse import java.net.http.HttpResponse
import java.time.Duration import java.time.Duration
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.streams.asSequence import kotlin.streams.asSequence
@Singleton @Singleton
@ -27,8 +27,6 @@ public class RuneStarNameDownloader @Inject constructor(
names += readTsv(endpoint, 0) names += readTsv(endpoint, 0)
} }
names += readTsv(LEANBOW_NAMES_ENDPOINT, 1)
return names.asSequence() return names.asSequence()
} }
@ -52,18 +50,12 @@ public class RuneStarNameDownloader @Inject constructor(
private companion object { private companion object {
private val NAMES_ENDPOINTS = listOf( private val NAMES_ENDPOINTS = listOf(
URI("https://raw.githubusercontent.com/Joshua-F/cache-names/master/names.tsv"),
URI("https://raw.githubusercontent.com/Pazaz/RT4-Data/main/names.tsv"),
URI("https://raw.githubusercontent.com/Pazaz/RT4-Data/main/osrs.tsv"),
URI("https://raw.githubusercontent.com/Pazaz/RT4-Data/main/walied.tsv"),
URI("https://raw.githubusercontent.com/RuneStar/cache-names/master/names.tsv"), URI("https://raw.githubusercontent.com/RuneStar/cache-names/master/names.tsv"),
URI("https://raw.githubusercontent.com/Joshua-F/cache-names/master/names.tsv"),
) )
private val INDIVIDUAL_NAMES_ENDPOINTS = listOf( private val INDIVIDUAL_NAMES_ENDPOINTS = listOf(
URI("https://raw.githubusercontent.com/Joshua-F/cache-names/master/individual-names.tsv"),
URI("https://raw.githubusercontent.com/Pazaz/RT4-Data/main/walied.individual.components.tsv"),
URI("https://raw.githubusercontent.com/Pazaz/RT4-Data/main/walied.individual.tsv"),
URI("https://raw.githubusercontent.com/RuneStar/cache-names/master/individual-names.tsv"), URI("https://raw.githubusercontent.com/RuneStar/cache-names/master/individual-names.tsv"),
URI("https://raw.githubusercontent.com/Joshua-F/cache-names/master/individual-names.tsv"),
) )
private val LEANBOW_NAMES_ENDPOINT = URI("https://raw.githubusercontent.com/Pazaz/RT4-Data/main/leanbow.tsv")
} }
} }

@ -1,44 +1,31 @@
package org.openrs2.archive.web package org.openrs2.archive.web
import io.ktor.http.CacheControl import io.ktor.application.ApplicationCall
import io.ktor.http.ContentDisposition import io.ktor.http.ContentDisposition
import io.ktor.http.ContentType import io.ktor.http.ContentType
import io.ktor.http.HttpHeaders import io.ktor.http.HttpHeaders
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.http.content.EntityTagVersion import io.ktor.response.header
import io.ktor.http.content.caching import io.ktor.response.respond
import io.ktor.http.content.versions import io.ktor.response.respondOutputStream
import io.ktor.server.application.ApplicationCall import io.ktor.thymeleaf.ThymeleafContent
import io.ktor.server.http.content.CachingOptions
import io.ktor.server.plugins.cachingheaders.caching
import io.ktor.server.response.header
import io.ktor.server.response.respond
import io.ktor.server.response.respondBytes
import io.ktor.server.response.respondOutputStream
import io.ktor.server.thymeleaf.ThymeleafContent
import io.netty.buffer.ByteBufAllocator 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.Semaphore
import kotlinx.coroutines.sync.withPermit import kotlinx.coroutines.sync.withPermit
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream
import org.openrs2.archive.cache.CacheExporter import org.openrs2.archive.cache.CacheExporter
import org.openrs2.archive.map.MapRenderer import org.openrs2.archive.map.MapRenderer
import org.openrs2.buffer.use
import org.openrs2.cache.DiskStoreZipWriter import org.openrs2.cache.DiskStoreZipWriter
import org.openrs2.cache.FlatFileStoreTarWriter import org.openrs2.cache.FlatFileStoreTarWriter
import org.openrs2.compress.gzip.GzipLevelOutputStream import org.openrs2.compress.gzip.GzipLevelOutputStream
import org.openrs2.crypto.whirlpool
import java.nio.file.attribute.FileTime import java.nio.file.attribute.FileTime
import java.time.Instant import java.time.Instant
import java.time.ZoneOffset
import java.time.ZonedDateTime
import java.util.Base64
import java.util.zip.Deflater import java.util.zip.Deflater
import java.util.zip.ZipEntry import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream import java.util.zip.ZipOutputStream
import javax.imageio.ImageIO import javax.imageio.ImageIO
import javax.inject.Inject
import javax.inject.Singleton
@Singleton @Singleton
public class CachesController @Inject constructor( public class CachesController @Inject constructor(
@ -50,186 +37,93 @@ public class CachesController @Inject constructor(
public suspend fun index(call: ApplicationCall) { public suspend fun index(call: ApplicationCall) {
val caches = exporter.list() val caches = exporter.list()
val totalSize = exporter.totalSize() call.respond(ThymeleafContent("caches/index.html", mapOf("caches" to caches)))
call.respond(
ThymeleafContent(
"caches/index.html", mapOf(
"caches" to caches,
"totalSize" to totalSize
)
)
)
} }
public suspend fun indexJson(call: ApplicationCall) { public suspend fun indexJson(call: ApplicationCall) {
val caches = exporter.list() val caches = exporter.list()
call.caching = CachingOptions(
cacheControl = CacheControl.MaxAge(
maxAgeSeconds = 900,
visibility = CacheControl.Visibility.Public
),
expires = ZonedDateTime.now(ZoneOffset.UTC).plusSeconds(900)
)
call.respond(caches) call.respond(caches)
} }
public suspend fun show(call: ApplicationCall) { public suspend fun show(call: ApplicationCall) {
val scope = call.parameters["scope"]!!
val id = call.parameters["id"]?.toIntOrNull() val id = call.parameters["id"]?.toIntOrNull()
if (id == null) { if (id == null) {
call.respond(HttpStatusCode.NotFound) call.respond(HttpStatusCode.NotFound)
return return
} }
val cache = exporter.get(scope, id) val cache = exporter.get(id)
if (cache == null) { if (cache == null) {
call.respond(HttpStatusCode.NotFound) call.respond(HttpStatusCode.NotFound)
return return
} }
call.respond( call.respond(ThymeleafContent("caches/show.html", mapOf("cache" to cache)))
ThymeleafContent(
"caches/show.html", mapOf(
"cache" to cache,
"scope" to scope
)
)
)
}
public suspend fun exportGroup(call: ApplicationCall) {
val scope = call.parameters["scope"]!!
val id = call.parameters["id"]?.toIntOrNull()
val archiveId = call.parameters["archive"]?.toIntOrNull()
val groupId = call.parameters["group"]?.toIntOrNull()
if (id == null || archiveId == null || groupId == null) {
call.respond(HttpStatusCode.NotFound)
return
}
exporter.exportGroup(scope, id, archiveId, groupId).use { buf ->
if (buf == null) {
call.respond(HttpStatusCode.NotFound)
return
}
val etag = Base64.getEncoder().encodeToString(buf.whirlpool().sliceArray(0 until 16))
val bytes = ByteBufUtil.getBytes(buf, 0, buf.readableBytes(), false)
call.respondBytes(bytes, contentType = ContentType.Application.OctetStream) {
caching = CachingOptions(
cacheControl = CacheControl.MaxAge(
maxAgeSeconds = 86400,
visibility = CacheControl.Visibility.Public
),
expires = ZonedDateTime.now(ZoneOffset.UTC).plusSeconds(86400)
)
versions = listOf(
EntityTagVersion(etag, weak = false)
)
}
}
} }
public suspend fun exportDisk(call: ApplicationCall) { public suspend fun exportDisk(call: ApplicationCall) {
val scope = call.parameters["scope"]!!
val id = call.parameters["id"]?.toIntOrNull() val id = call.parameters["id"]?.toIntOrNull()
if (id == null) { if (id == null) {
call.respond(HttpStatusCode.NotFound) call.respond(HttpStatusCode.NotFound)
return return
} }
val name = exporter.getFileName(scope, id)
if (name == null) {
call.respond(HttpStatusCode.NotFound)
return
}
call.response.header( call.response.header(
HttpHeaders.ContentDisposition, HttpHeaders.ContentDisposition,
ContentDisposition.Attachment ContentDisposition.Attachment
.withParameter(ContentDisposition.Parameters.FileName, "cache-$name.zip") .withParameter(ContentDisposition.Parameters.FileName, "cache.zip")
.toString() .toString()
) )
call.respondOutputStream(contentType = ContentType.Application.Zip) { call.respondOutputStream(contentType = ContentType.Application.Zip) {
exporter.export(scope, id) { legacy -> exporter.export(id) { legacy ->
DiskStoreZipWriter(ZipOutputStream(this), alloc = alloc, legacy = legacy) DiskStoreZipWriter(ZipOutputStream(this), alloc = alloc, legacy = legacy)
} }
} }
} }
public suspend fun exportFlatFile(call: ApplicationCall) { public suspend fun exportFlatFile(call: ApplicationCall) {
val scope = call.parameters["scope"]!!
val id = call.parameters["id"]?.toIntOrNull() val id = call.parameters["id"]?.toIntOrNull()
if (id == null) { if (id == null) {
call.respond(HttpStatusCode.NotFound) call.respond(HttpStatusCode.NotFound)
return return
} }
val name = exporter.getFileName(scope, id)
if (name == null) {
call.respond(HttpStatusCode.NotFound)
return
}
call.response.header( call.response.header(
HttpHeaders.ContentDisposition, HttpHeaders.ContentDisposition,
ContentDisposition.Attachment ContentDisposition.Attachment
.withParameter(ContentDisposition.Parameters.FileName, "cache-$name.tar.gz") .withParameter(ContentDisposition.Parameters.FileName, "cache.tar.gz")
.toString() .toString()
) )
call.respondOutputStream(contentType = ContentType.Application.GZip) { call.respondOutputStream(contentType = ContentType.Application.GZip) {
exporter.export(scope, id) { exporter.export(id) {
FlatFileStoreTarWriter(TarArchiveOutputStream(GzipLevelOutputStream(this, Deflater.BEST_COMPRESSION))) FlatFileStoreTarWriter(TarArchiveOutputStream(GzipLevelOutputStream(this, Deflater.BEST_COMPRESSION)))
} }
} }
} }
public suspend fun exportKeysJson(call: ApplicationCall) { public suspend fun exportKeysJson(call: ApplicationCall) {
val scope = call.parameters["scope"]!!
val id = call.parameters["id"]?.toIntOrNull() val id = call.parameters["id"]?.toIntOrNull()
if (id == null) { if (id == null) {
call.respond(HttpStatusCode.NotFound) call.respond(HttpStatusCode.NotFound)
return return
} }
val name = exporter.getFileName(scope, id) call.respond(exporter.exportKeys(id))
if (name == null) {
call.respond(HttpStatusCode.NotFound)
return
}
call.response.header(
HttpHeaders.ContentDisposition,
ContentDisposition.Inline
.withParameter(ContentDisposition.Parameters.FileName, "keys-$name.json")
.toString()
)
call.respond(exporter.exportKeys(scope, id))
} }
public suspend fun exportKeysZip(call: ApplicationCall) { public suspend fun exportKeysZip(call: ApplicationCall) {
val scope = call.parameters["scope"]!!
val id = call.parameters["id"]?.toIntOrNull() val id = call.parameters["id"]?.toIntOrNull()
if (id == null) { if (id == null) {
call.respond(HttpStatusCode.NotFound) call.respond(HttpStatusCode.NotFound)
return return
} }
val name = exporter.getFileName(scope, id)
if (name == null) {
call.respond(HttpStatusCode.NotFound)
return
}
call.response.header( call.response.header(
HttpHeaders.ContentDisposition, HttpHeaders.ContentDisposition,
ContentDisposition.Attachment ContentDisposition.Attachment
.withParameter(ContentDisposition.Parameters.FileName, "keys-$name.zip") .withParameter(ContentDisposition.Parameters.FileName, "keys.zip")
.toString() .toString()
) )
@ -240,7 +134,7 @@ public class CachesController @Inject constructor(
val timestamp = FileTime.from(Instant.EPOCH) val timestamp = FileTime.from(Instant.EPOCH)
for (key in exporter.exportKeys(scope, id)) { for (key in exporter.exportKeys(id)) {
if (key.mapSquare == null) { if (key.mapSquare == null) {
continue continue
} }
@ -272,33 +166,19 @@ public class CachesController @Inject constructor(
} }
public suspend fun renderMap(call: ApplicationCall) { public suspend fun renderMap(call: ApplicationCall) {
val scope = call.parameters["scope"]!!
val id = call.parameters["id"]?.toIntOrNull() val id = call.parameters["id"]?.toIntOrNull()
if (id == null) { if (id == null) {
call.respond(HttpStatusCode.NotFound) call.respond(HttpStatusCode.NotFound)
return return
} }
val name = exporter.getFileName(scope, id)
if (name == null) {
call.respond(HttpStatusCode.NotFound)
return
}
call.response.header(
HttpHeaders.ContentDisposition,
ContentDisposition.Inline
.withParameter(ContentDisposition.Parameters.FileName, "map-$name.png")
.toString()
)
/* /*
* The temporary BufferedImages used by the MapRenderer use a large * The temporary BufferedImages used by the MapRenderer use a large
* amount of heap space. We limit the number of renders that can be * amount of heap space. We limit the number of renders that can be
* performed in parallel to prevent OOMs. * performed in parallel to prevent OOMs.
*/ */
renderSemaphore.withPermit { renderSemaphore.withPermit {
val image = renderer.render(scope, id) val image = renderer.render(id)
call.respondOutputStream(contentType = ContentType.Image.PNG) { call.respondOutputStream(contentType = ContentType.Image.PNG) {
ImageIO.write(image, "PNG", this) ImageIO.write(image, "PNG", this)

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

@ -1,16 +1,16 @@
package org.openrs2.archive.web package org.openrs2.archive.web
import io.ktor.application.ApplicationCall
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.server.application.ApplicationCall import io.ktor.request.receive
import io.ktor.server.request.receive import io.ktor.response.respond
import io.ktor.server.response.respond import io.ktor.thymeleaf.ThymeleafContent
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.KeyExporter
import org.openrs2.archive.key.KeyImporter import org.openrs2.archive.key.KeyImporter
import org.openrs2.archive.key.KeySource 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 @Singleton
public class KeysController @Inject constructor( public class KeysController @Inject constructor(
@ -24,7 +24,7 @@ public class KeysController @Inject constructor(
} }
public suspend fun import(call: ApplicationCall) { 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()) { if (keys.isNotEmpty()) {
importer.import(keys, KeySource.API) importer.import(keys, KeySource.API)

@ -1,65 +1,44 @@
package org.openrs2.archive.web package org.openrs2.archive.web
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import io.ktor.application.call
import io.ktor.application.install
import io.ktor.features.ContentNegotiation
import io.ktor.features.XForwardedHeaderSupport
import io.ktor.http.ContentType import io.ktor.http.ContentType
import io.ktor.http.HttpHeaders import io.ktor.http.HttpStatusCode
import io.ktor.serialization.jackson.JacksonConverter import io.ktor.http.content.resources
import io.ktor.server.application.ApplicationCall import io.ktor.http.content.static
import io.ktor.server.application.call import io.ktor.jackson.JacksonConverter
import io.ktor.server.application.createApplicationPlugin import io.ktor.response.respond
import io.ktor.server.application.install import io.ktor.response.respondRedirect
import io.ktor.routing.get
import io.ktor.routing.post
import io.ktor.routing.routing
import io.ktor.server.cio.CIO
import io.ktor.server.engine.embeddedServer import io.ktor.server.engine.embeddedServer
import io.ktor.server.http.content.staticResources import io.ktor.thymeleaf.Thymeleaf
import io.ktor.server.jetty.Jetty import io.ktor.thymeleaf.ThymeleafContent
import io.ktor.server.plugins.autohead.AutoHeadResponse import io.ktor.webjars.Webjars
import io.ktor.server.plugins.cachingheaders.CachingHeaders
import io.ktor.server.plugins.conditionalheaders.ConditionalHeaders
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
import io.ktor.server.plugins.defaultheaders.DefaultHeaders
import io.ktor.server.plugins.forwardedheaders.XForwardedHeaders
import io.ktor.server.response.header
import io.ktor.server.response.respond
import io.ktor.server.response.respondRedirect
import io.ktor.server.routing.get
import io.ktor.server.routing.post
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.openrs2.json.Json
import org.thymeleaf.extras.java8time.dialect.Java8TimeDialect import org.thymeleaf.extras.java8time.dialect.Java8TimeDialect
import org.thymeleaf.templatemode.TemplateMode import org.thymeleaf.templatemode.TemplateMode
import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver
import javax.inject.Inject
import javax.inject.Singleton
@Singleton @Singleton
public class WebServer @Inject constructor( public class WebServer @Inject constructor(
private val cachesController: CachesController, private val cachesController: CachesController,
private val clientsController: ClientsController,
private val keysController: KeysController, private val keysController: KeysController,
@Json private val mapper: ObjectMapper @Json private val mapper: ObjectMapper
) { ) {
public fun start(address: String, port: Int) { public fun start(address: String, port: Int) {
embeddedServer(Jetty, host = address, port = port) { embeddedServer(CIO, host = address, port = port) {
install(AutoHeadResponse)
install(CachingHeaders)
install(ConditionalHeaders)
install(createApplicationPlugin(name = "CORS") {
onCall { call ->
call.response.header(HttpHeaders.AccessControlAllowOrigin, "*")
}
})
install(ContentNegotiation) { install(ContentNegotiation) {
ignoreType<ThymeleafContent>()
register(ContentType.Application.Json, JacksonConverter(mapper)) register(ContentType.Application.Json, JacksonConverter(mapper))
} }
install(DefaultHeaders)
install(Thymeleaf) { install(Thymeleaf) {
addDialect(ByteUnitsDialect) addDialect(ByteUnitsDialect)
addDialect(Java8TimeDialect()) addDialect(Java8TimeDialect())
@ -70,55 +49,47 @@ public class WebServer @Inject constructor(
}) })
} }
install(XForwardedHeaders) install(XForwardedHeaderSupport)
install(Webjars) install(Webjars)
routing { routing {
get("/") { call.respond(ThymeleafContent("index.html", emptyMap())) } get("/") { call.respond(ThymeleafContent("index.html", emptyMap())) }
get("/api") { call.respond(ThymeleafContent("api/index.html", mapOf("active" to "api"))) }
get("/caches") { cachesController.index(call) } get("/caches") { cachesController.index(call) }
get("/caches.json") { cachesController.indexJson(call) } get("/caches.json") { cachesController.indexJson(call) }
get("/caches/{scope}/{id}") { cachesController.show(call) } get("/caches/{id}") { cachesController.show(call) }
get("/caches/{scope}/{id}/archives/{archive}/groups/{group}.dat") { cachesController.exportGroup(call) } get("/caches/{id}.zip") {
get("/caches/{scope}/{id}/disk.zip") { cachesController.exportDisk(call) } val id = call.parameters["id"]
get("/caches/{scope}/{id}/flat-file.tar.gz") { cachesController.exportFlatFile(call) } if (id == null) {
get("/caches/{scope}/{id}/keys.json") { cachesController.exportKeysJson(call) } call.respond(HttpStatusCode.NotFound)
get("/caches/{scope}/{id}/keys.zip") { cachesController.exportKeysZip(call) } return@get
get("/caches/{scope}/{id}/map.png") { cachesController.renderMap(call) } }
get("/clients") { clientsController.index(call) }
get("/clients/{id}.dat") { clientsController.export(call) } call.respondRedirect(permanent = true) {
get("/clients/{id}") { clientsController.show(call) } path("caches", id, "disk.zip")
}
}
get("/caches/{id}.json") {
val id = call.parameters["id"]
if (id == null) {
call.respond(HttpStatusCode.NotFound)
return@get
}
call.respondRedirect(permanent = true) {
path("caches", id, "keys.json")
}
}
get("/caches/{id}/disk.zip") { cachesController.exportDisk(call) }
get("/caches/{id}/flat-file.tar.gz") { cachesController.exportFlatFile(call) }
get("/caches/{id}/keys.json") { cachesController.exportKeysJson(call) }
get("/caches/{id}/keys.zip") { cachesController.exportKeysZip(call) }
get("/caches/{id}/map.png") { cachesController.renderMap(call) }
get("/keys") { keysController.index(call) } get("/keys") { keysController.index(call) }
post("/keys") { keysController.import(call) } post("/keys") { keysController.import(call) }
get("/keys/all.json") { keysController.exportAll(call) } get("/keys/all.json") { keysController.exportAll(call) }
get("/keys/valid.json") { keysController.exportValid(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}") }
get("/caches/{id}.json") { redirect(call, permanent = true, "/caches/runescape/{id}/keys.json") }
get("/caches/{id}.zip") { redirect(call, permanent = true, "/caches/runescape/{id}/disk.zip") }
get("/caches/{id}/disk.zip") { redirect(call, permanent = true, "/caches/runescape/{id}/disk.zip") }
get("/caches/{id}/flat-file.tar.gz") {
redirect(call, permanent = true, "/caches/runescape/{id}/flat-file.tar.gz")
}
get("/caches/{id}/keys.json") { redirect(call, permanent = true, "/caches/runescape/{id}/keys.json") }
get("/caches/{id}/keys.zip") { redirect(call, permanent = true, "/caches/runescape/{id}/keys.zip") }
get("/caches/{id}/map.png") { redirect(call, permanent = true, "/caches/runescape/{id}/map.png") }
} }
}.start(wait = true) }.start(wait = true)
} }
private suspend fun redirect(call: ApplicationCall, permanent: Boolean, path: String) {
val destination = path.replace(PARAMETER) { match ->
val (name) = match.destructured
call.parameters[name] ?: throw IllegalArgumentException()
}
call.respondRedirect(destination, permanent)
}
private companion object {
private val PARAMETER = Regex("\\{([^}]*)}")
}
} }

@ -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 SELECT id, name
FROM game_variants; 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 ALTER TABLE game_variants
ADD COLUMN game_id INT NULL REFERENCES games (id), ADD COLUMN game_id INT NULL REFERENCES games (id),

@ -1,176 +0,0 @@
-- @formatter:off
CREATE TABLE scopes (
id SERIAL PRIMARY KEY NOT NULL,
name TEXT UNIQUE NOT NULL
);
INSERT INTO scopes (name) VALUES ('runescape');
ALTER TABLE games
ADD COLUMN scope_id INTEGER DEFAULT 1 NOT NULL REFERENCES scopes (id);
ALTER TABLE games
ALTER COLUMN scope_id DROP DEFAULT;
-- XXX(gpe): I don't think we can easily replace this as the source_groups
-- table doesn't contain a scope_id directly - only indirectly via the sources
-- and games tables.
ALTER TABLE source_groups
DROP CONSTRAINT source_groups_archive_id_group_id_version_version_truncate_fkey;
ALTER TABLE groups
ADD COLUMN scope_id INTEGER DEFAULT 1 NOT NULL REFERENCES scopes (id),
DROP CONSTRAINT groups_pkey,
ADD PRIMARY KEY (scope_id, archive_id, group_id, version, version_truncated, container_id);
ALTER TABLE groups
ALTER COLUMN scope_id DROP DEFAULT;
CREATE FUNCTION resolve_index(_scope_id INTEGER, _archive_id uint1, _crc32 INTEGER, _version INTEGER) RETURNS SETOF containers AS $$
SELECT c.*
FROM groups g
JOIN containers c ON c.id = g.container_id
JOIN indexes i ON i.container_id = c.id
WHERE g.scope_id = _scope_id AND g.archive_id = 255 AND g.group_id = _archive_id::INTEGER AND c.crc32 = _crc32 AND
g.version = _version AND NOT g.version_truncated AND i.version = _version
ORDER BY c.id ASC
LIMIT 1;
$$ LANGUAGE SQL STABLE PARALLEL SAFE ROWS 1;
CREATE FUNCTION resolve_group(_scope_id INTEGER, _archive_id uint1, _group_id INTEGER, _crc32 INTEGER, _version INTEGER) RETURNS SETOF containers AS $$
SELECT c.*
FROM groups g
JOIN containers c ON c.id = g.container_id
WHERE g.scope_id = _scope_id AND g.archive_id = _archive_id AND g.group_id = _group_id AND c.crc32 = _crc32 AND (
(g.version = _version AND NOT g.version_truncated) OR
(g.version = _version & 65535 AND g.version_truncated)
)
ORDER BY g.version_truncated ASC, c.id ASC
LIMIT 1;
$$ LANGUAGE SQL STABLE PARALLEL SAFE ROWS 1;
DROP VIEW resolved_groups;
DROP VIEW resolved_indexes;
CREATE VIEW resolved_indexes AS
SELECT s.id AS scope_id, m.id AS master_index_id, a.archive_id, c.data, c.id AS container_id
FROM scopes s
CROSS JOIN master_indexes m
JOIN master_index_archives a ON a.master_index_id = m.id
JOIN resolve_index(s.id, a.archive_id, a.crc32, a.version) c ON TRUE;
CREATE VIEW resolved_groups (scope_id, master_index_id, archive_id, group_id, name_hash, version, data, encrypted, empty_loc, key_id) AS
WITH i AS NOT MATERIALIZED (
SELECT scope_id, master_index_id, archive_id, data, container_id
FROM resolved_indexes
)
SELECT i.scope_id, i.master_index_id, 255::uint1, i.archive_id::INTEGER, NULL, NULL, i.data, FALSE, FALSE, NULL
FROM i
UNION ALL
SELECT i.scope_id, i.master_index_id, i.archive_id, ig.group_id, ig.name_hash, ig.version, c.data, c.encrypted, c.empty_loc, c.key_id
FROM i
JOIN index_groups ig ON ig.container_id = i.container_id
JOIN resolve_group(i.scope_id, i.archive_id, ig.group_id, ig.crc32, ig.version) c ON TRUE;
DROP VIEW colliding_groups;
CREATE VIEW colliding_groups (scope_id, archive_id, group_id, crc32, truncated_version, versions, containers) AS
SELECT
g.scope_id,
g.archive_id,
g.group_id,
c.crc32,
g.version & 65535 AS truncated_version,
array_agg(DISTINCT g.version ORDER BY g.version ASC),
array_agg(DISTINCT c.id ORDER BY c.id ASC)
FROM groups g
JOIN containers c ON c.id = g.container_id
GROUP BY g.scope_id, g.archive_id, g.group_id, c.crc32, truncated_version
HAVING COUNT(DISTINCT c.id) > 1;
DROP VIEW cache_stats;
DROP MATERIALIZED VIEW master_index_stats;
DROP MATERIALIZED VIEW index_stats;
CREATE MATERIALIZED VIEW index_stats (
scope_id,
archive_id,
container_id,
valid_groups,
groups,
valid_keys,
keys,
size,
blocks
) AS
SELECT
s.id AS scope_id,
g.group_id AS archive_id,
i.container_id,
COUNT(*) FILTER (WHERE c.id IS NOT NULL) AS valid_groups,
COUNT(*) AS groups,
COUNT(*) FILTER (WHERE c.encrypted AND (c.key_id IS NOT NULL OR c.empty_loc)) AS valid_keys,
COUNT(*) FILTER (WHERE c.encrypted) AS keys,
SUM(length(c.data) + 2) FILTER (WHERE c.id IS NOT NULL) AS size,
SUM(group_blocks(ig.group_id, length(c.data) + 2)) FILTER (WHERE c.id IS NOT NULL) AS blocks
FROM scopes s
CROSS JOIN indexes i
JOIN groups g ON g.container_id = i.container_id AND g.archive_id = 255 AND NOT g.version_truncated AND
g.version = i.version
JOIN index_groups ig ON ig.container_id = i.container_id
LEFT JOIN resolve_group(s.id, g.group_id::uint1, ig.group_id, ig.crc32, ig.version) c ON TRUE
GROUP BY s.id, g.group_id, i.container_id;
CREATE UNIQUE INDEX ON index_stats (scope_id, archive_id, container_id);
CREATE MATERIALIZED VIEW master_index_stats (
scope_id,
master_index_id,
valid_indexes,
indexes,
valid_groups,
groups,
valid_keys,
keys,
size,
blocks
) AS
SELECT
sc.id,
m.id,
COUNT(*) FILTER (WHERE c.id IS NOT NULL OR (a.version = 0 AND a.crc32 = 0)) AS valid_indexes,
COUNT(*) FILTER (WHERE a.master_index_id IS NOT NULL) AS indexes,
SUM(COALESCE(s.valid_groups, 0)) AS valid_groups,
SUM(COALESCE(s.groups, 0)) AS groups,
SUM(COALESCE(s.valid_keys, 0)) AS valid_keys,
SUM(COALESCE(s.keys, 0)) AS keys,
SUM(COALESCE(s.size, 0)) + SUM(COALESCE(length(c.data), 0)) AS size,
SUM(COALESCE(s.blocks, 0)) + SUM(COALESCE(group_blocks(a.archive_id, length(c.data)), 0)) AS blocks
FROM scopes sc
CROSS JOIN master_indexes m
LEFT JOIN master_index_archives a ON a.master_index_id = m.id
LEFT JOIN resolve_index(sc.id, a.archive_id, a.crc32, a.version) c ON TRUE
LEFT JOIN index_stats s ON s.scope_id = sc.id AND s.archive_id = a.archive_id AND s.container_id = c.id
GROUP BY sc.id, m.id;
CREATE UNIQUE INDEX ON master_index_stats (scope_id, master_index_id);
CREATE VIEW cache_stats AS
SELECT
s.id AS scope_id,
c.id AS cache_id,
COALESCE(ms.valid_indexes, cs.valid_archives) AS valid_indexes,
COALESCE(ms.indexes, cs.archives) AS indexes,
COALESCE(ms.valid_groups, cs.valid_files) AS valid_groups,
COALESCE(ms.groups, cs.files) AS groups,
COALESCE(ms.valid_keys, 0) AS valid_keys,
COALESCE(ms.keys, 0) AS keys,
COALESCE(ms.size, cs.size) AS size,
COALESCE(ms.blocks, cs.blocks) AS blocks
FROM scopes s
CROSS JOIN caches c
LEFT JOIN master_index_stats ms ON ms.scope_id = s.id AND ms.master_index_id = c.id
LEFT JOIN crc_table_stats cs ON s.name = 'runescape' AND cs.crc_table_id = c.id;
DROP FUNCTION resolve_group(_archive_id uint1, _group_id INTEGER, _crc32 INTEGER, _version INTEGER);
DROP FUNCTION resolve_index(_archive_id uint1, _crc32 INTEGER, _version INTEGER);

@ -1,2 +0,0 @@
-- @formatter:off
ALTER TABLE caches ADD COLUMN hidden BOOLEAN NOT NULL DEFAULT FALSE;

@ -1,95 +0,0 @@
-- @formatter:off
CREATE MATERIALIZED VIEW index_stats_new (
scope_id,
archive_id,
container_id,
valid_groups,
groups,
valid_keys,
keys,
size,
blocks
) AS
SELECT
s.id AS scope_id,
g.group_id AS archive_id,
i.container_id,
COUNT(*) FILTER (WHERE c.id IS NOT NULL) AS valid_groups,
COUNT(*) AS groups,
COUNT(*) FILTER (WHERE c.encrypted AND (c.key_id IS NOT NULL OR c.empty_loc)) AS valid_keys,
COUNT(*) FILTER (WHERE c.encrypted) AS keys,
SUM(length(c.data) + 2) FILTER (WHERE c.id IS NOT NULL) AS size,
SUM(group_blocks(ig.group_id, length(c.data) + 2)) FILTER (WHERE c.id IS NOT NULL) AS blocks
FROM scopes s
CROSS JOIN indexes i
JOIN groups g ON g.scope_id = s.id AND g.container_id = i.container_id AND g.archive_id = 255 AND
NOT g.version_truncated AND g.version = i.version
JOIN index_groups ig ON ig.container_id = i.container_id
LEFT JOIN resolve_group(s.id, g.group_id::uint1, ig.group_id, ig.crc32, ig.version) c ON TRUE
GROUP BY s.id, g.group_id, i.container_id;
CREATE UNIQUE INDEX ON index_stats_new (scope_id, archive_id, container_id);
ALTER MATERIALIZED VIEW index_stats RENAME TO index_stats_old;
ALTER INDEX index_stats_scope_id_archive_id_container_id_idx RENAME TO index_stats_old_scope_id_archive_id_container_id_idx;
ALTER MATERIALIZED VIEW index_stats_new RENAME TO index_stats;
ALTER INDEX index_stats_new_scope_id_archive_id_container_id_idx RENAME TO index_stats_scope_id_archive_id_container_id_idx;
CREATE MATERIALIZED VIEW master_index_stats_new (
scope_id,
master_index_id,
valid_indexes,
indexes,
valid_groups,
groups,
valid_keys,
keys,
size,
blocks
) AS
SELECT
sc.id,
m.id,
COUNT(*) FILTER (WHERE c.id IS NOT NULL OR (a.version = 0 AND a.crc32 = 0)) AS valid_indexes,
COUNT(*) FILTER (WHERE a.master_index_id IS NOT NULL) AS indexes,
SUM(COALESCE(s.valid_groups, 0)) AS valid_groups,
SUM(COALESCE(s.groups, 0)) AS groups,
SUM(COALESCE(s.valid_keys, 0)) AS valid_keys,
SUM(COALESCE(s.keys, 0)) AS keys,
SUM(COALESCE(s.size, 0)) + SUM(COALESCE(length(c.data), 0)) AS size,
SUM(COALESCE(s.blocks, 0)) + SUM(COALESCE(group_blocks(a.archive_id, length(c.data)), 0)) AS blocks
FROM scopes sc
CROSS JOIN master_indexes m
LEFT JOIN master_index_archives a ON a.master_index_id = m.id
LEFT JOIN resolve_index(sc.id, a.archive_id, a.crc32, a.version) c ON TRUE
LEFT JOIN index_stats s ON s.scope_id = sc.id AND s.archive_id = a.archive_id AND s.container_id = c.id
GROUP BY sc.id, m.id;
CREATE UNIQUE INDEX ON master_index_stats_new (scope_id, master_index_id);
ALTER MATERIALIZED VIEW master_index_stats RENAME TO master_index_stats_old;
ALTER INDEX master_index_stats_scope_id_master_index_id_idx RENAME TO master_index_stats_old_scope_id_master_index_id_idx;
ALTER MATERIALIZED VIEW master_index_stats_new RENAME TO master_index_stats;
ALTER INDEX master_index_stats_new_scope_id_master_index_id_idx RENAME TO master_index_stats_scope_id_master_index_id_idx;
CREATE OR REPLACE VIEW cache_stats AS
SELECT
s.id AS scope_id,
c.id AS cache_id,
COALESCE(ms.valid_indexes, cs.valid_archives) AS valid_indexes,
COALESCE(ms.indexes, cs.archives) AS indexes,
COALESCE(ms.valid_groups, cs.valid_files) AS valid_groups,
COALESCE(ms.groups, cs.files) AS groups,
COALESCE(ms.valid_keys, 0) AS valid_keys,
COALESCE(ms.keys, 0) AS keys,
COALESCE(ms.size, cs.size) AS size,
COALESCE(ms.blocks, cs.blocks) AS blocks
FROM scopes s
CROSS JOIN caches c
LEFT JOIN master_index_stats ms ON ms.scope_id = s.id AND ms.master_index_id = c.id
LEFT JOIN crc_table_stats cs ON s.name = 'runescape' AND cs.crc_table_id = c.id;
DROP MATERIALIZED VIEW master_index_stats_old;
DROP MATERIALIZED VIEW index_stats_old;

@ -1,95 +0,0 @@
-- @formatter:off
CREATE MATERIALIZED VIEW index_stats_new (
scope_id,
archive_id,
container_id,
valid_groups,
groups,
valid_keys,
keys,
size,
blocks
) AS
SELECT
s.id AS scope_id,
g.group_id AS archive_id,
i.container_id,
COUNT(*) FILTER (WHERE c.id IS NOT NULL) AS valid_groups,
COUNT(*) FILTER (WHERE ig.container_id IS NOT NULL) AS groups,
COUNT(*) FILTER (WHERE c.encrypted AND (c.key_id IS NOT NULL OR c.empty_loc)) AS valid_keys,
COUNT(*) FILTER (WHERE c.encrypted) AS keys,
SUM(length(c.data) + 2) FILTER (WHERE c.id IS NOT NULL) AS size,
SUM(group_blocks(ig.group_id, length(c.data) + 2)) FILTER (WHERE c.id IS NOT NULL) AS blocks
FROM scopes s
CROSS JOIN indexes i
JOIN groups g ON g.scope_id = s.id AND g.container_id = i.container_id AND g.archive_id = 255 AND
NOT g.version_truncated AND g.version = i.version
LEFT JOIN index_groups ig ON ig.container_id = i.container_id
LEFT JOIN resolve_group(s.id, g.group_id::uint1, ig.group_id, ig.crc32, ig.version) c ON TRUE
GROUP BY s.id, g.group_id, i.container_id;
CREATE UNIQUE INDEX ON index_stats_new (scope_id, archive_id, container_id);
ALTER MATERIALIZED VIEW index_stats RENAME TO index_stats_old;
ALTER INDEX index_stats_scope_id_archive_id_container_id_idx RENAME TO index_stats_old_scope_id_archive_id_container_id_idx;
ALTER MATERIALIZED VIEW index_stats_new RENAME TO index_stats;
ALTER INDEX index_stats_new_scope_id_archive_id_container_id_idx RENAME TO index_stats_scope_id_archive_id_container_id_idx;
CREATE MATERIALIZED VIEW master_index_stats_new (
scope_id,
master_index_id,
valid_indexes,
indexes,
valid_groups,
groups,
valid_keys,
keys,
size,
blocks
) AS
SELECT
sc.id,
m.id,
COUNT(*) FILTER (WHERE c.id IS NOT NULL OR (a.version = 0 AND a.crc32 = 0)) AS valid_indexes,
COUNT(*) FILTER (WHERE a.master_index_id IS NOT NULL) AS indexes,
SUM(COALESCE(s.valid_groups, 0)) AS valid_groups,
SUM(COALESCE(s.groups, 0)) AS groups,
SUM(COALESCE(s.valid_keys, 0)) AS valid_keys,
SUM(COALESCE(s.keys, 0)) AS keys,
SUM(COALESCE(s.size, 0)) + SUM(COALESCE(length(c.data), 0)) AS size,
SUM(COALESCE(s.blocks, 0)) + SUM(COALESCE(group_blocks(a.archive_id, length(c.data)), 0)) AS blocks
FROM scopes sc
CROSS JOIN master_indexes m
LEFT JOIN master_index_archives a ON a.master_index_id = m.id
LEFT JOIN resolve_index(sc.id, a.archive_id, a.crc32, a.version) c ON TRUE
LEFT JOIN index_stats s ON s.scope_id = sc.id AND s.archive_id = a.archive_id AND s.container_id = c.id
GROUP BY sc.id, m.id;
CREATE UNIQUE INDEX ON master_index_stats_new (scope_id, master_index_id);
ALTER MATERIALIZED VIEW master_index_stats RENAME TO master_index_stats_old;
ALTER INDEX master_index_stats_scope_id_master_index_id_idx RENAME TO master_index_stats_old_scope_id_master_index_id_idx;
ALTER MATERIALIZED VIEW master_index_stats_new RENAME TO master_index_stats;
ALTER INDEX master_index_stats_new_scope_id_master_index_id_idx RENAME TO master_index_stats_scope_id_master_index_id_idx;
CREATE OR REPLACE VIEW cache_stats AS
SELECT
s.id AS scope_id,
c.id AS cache_id,
COALESCE(ms.valid_indexes, cs.valid_archives) AS valid_indexes,
COALESCE(ms.indexes, cs.archives) AS indexes,
COALESCE(ms.valid_groups, cs.valid_files) AS valid_groups,
COALESCE(ms.groups, cs.files) AS groups,
COALESCE(ms.valid_keys, 0) AS valid_keys,
COALESCE(ms.keys, 0) AS keys,
COALESCE(ms.size, cs.size) AS size,
COALESCE(ms.blocks, cs.blocks) AS blocks
FROM scopes s
CROSS JOIN caches c
LEFT JOIN master_index_stats ms ON ms.scope_id = s.id AND ms.master_index_id = c.id
LEFT JOIN crc_table_stats cs ON s.name = 'runescape' AND cs.crc_table_id = c.id;
DROP MATERIALIZED VIEW master_index_stats_old;
DROP MATERIALIZED VIEW index_stats_old;

@ -1,95 +0,0 @@
-- @formatter:off
CREATE MATERIALIZED VIEW index_stats_new (
scope_id,
archive_id,
container_id,
valid_groups,
groups,
valid_keys,
keys,
size,
blocks
) AS
SELECT
s.id AS scope_id,
g.group_id AS archive_id,
i.container_id,
COUNT(*) FILTER (WHERE c.id IS NOT NULL) AS valid_groups,
COUNT(*) FILTER (WHERE ig.container_id IS NOT NULL) AS groups,
COUNT(*) FILTER (WHERE c.encrypted AND (c.key_id IS NOT NULL OR c.empty_loc)) AS valid_keys,
COUNT(*) FILTER (WHERE c.encrypted) AS keys,
COALESCE(SUM(length(c.data) + 2) FILTER (WHERE c.id IS NOT NULL), 0) AS size,
COALESCE(SUM(group_blocks(ig.group_id, length(c.data) + 2)) FILTER (WHERE c.id IS NOT NULL), 0) AS blocks
FROM scopes s
CROSS JOIN indexes i
JOIN groups g ON g.scope_id = s.id AND g.container_id = i.container_id AND g.archive_id = 255 AND
NOT g.version_truncated AND g.version = i.version
LEFT JOIN index_groups ig ON ig.container_id = i.container_id
LEFT JOIN resolve_group(s.id, g.group_id::uint1, ig.group_id, ig.crc32, ig.version) c ON TRUE
GROUP BY s.id, g.group_id, i.container_id;
CREATE UNIQUE INDEX ON index_stats_new (scope_id, archive_id, container_id);
ALTER MATERIALIZED VIEW index_stats RENAME TO index_stats_old;
ALTER INDEX index_stats_scope_id_archive_id_container_id_idx RENAME TO index_stats_old_scope_id_archive_id_container_id_idx;
ALTER MATERIALIZED VIEW index_stats_new RENAME TO index_stats;
ALTER INDEX index_stats_new_scope_id_archive_id_container_id_idx RENAME TO index_stats_scope_id_archive_id_container_id_idx;
CREATE MATERIALIZED VIEW master_index_stats_new (
scope_id,
master_index_id,
valid_indexes,
indexes,
valid_groups,
groups,
valid_keys,
keys,
size,
blocks
) AS
SELECT
sc.id,
m.id,
COUNT(*) FILTER (WHERE c.id IS NOT NULL OR (a.version = 0 AND a.crc32 = 0)) AS valid_indexes,
COUNT(*) FILTER (WHERE a.master_index_id IS NOT NULL) AS indexes,
SUM(COALESCE(s.valid_groups, 0)) AS valid_groups,
SUM(COALESCE(s.groups, 0)) AS groups,
SUM(COALESCE(s.valid_keys, 0)) AS valid_keys,
SUM(COALESCE(s.keys, 0)) AS keys,
SUM(COALESCE(s.size, 0)) + SUM(COALESCE(length(c.data), 0)) AS size,
SUM(COALESCE(s.blocks, 0)) + SUM(COALESCE(group_blocks(a.archive_id, length(c.data)), 0)) AS blocks
FROM scopes sc
CROSS JOIN master_indexes m
LEFT JOIN master_index_archives a ON a.master_index_id = m.id
LEFT JOIN resolve_index(sc.id, a.archive_id, a.crc32, a.version) c ON TRUE
LEFT JOIN index_stats s ON s.scope_id = sc.id AND s.archive_id = a.archive_id AND s.container_id = c.id
GROUP BY sc.id, m.id;
CREATE UNIQUE INDEX ON master_index_stats_new (scope_id, master_index_id);
ALTER MATERIALIZED VIEW master_index_stats RENAME TO master_index_stats_old;
ALTER INDEX master_index_stats_scope_id_master_index_id_idx RENAME TO master_index_stats_old_scope_id_master_index_id_idx;
ALTER MATERIALIZED VIEW master_index_stats_new RENAME TO master_index_stats;
ALTER INDEX master_index_stats_new_scope_id_master_index_id_idx RENAME TO master_index_stats_scope_id_master_index_id_idx;
CREATE OR REPLACE VIEW cache_stats AS
SELECT
s.id AS scope_id,
c.id AS cache_id,
COALESCE(ms.valid_indexes, cs.valid_archives) AS valid_indexes,
COALESCE(ms.indexes, cs.archives) AS indexes,
COALESCE(ms.valid_groups, cs.valid_files) AS valid_groups,
COALESCE(ms.groups, cs.files) AS groups,
COALESCE(ms.valid_keys, 0) AS valid_keys,
COALESCE(ms.keys, 0) AS keys,
COALESCE(ms.size, cs.size) AS size,
COALESCE(ms.blocks, cs.blocks) AS blocks
FROM scopes s
CROSS JOIN caches c
LEFT JOIN master_index_stats ms ON ms.scope_id = s.id AND ms.master_index_id = c.id
LEFT JOIN crc_table_stats cs ON s.name = 'runescape' AND cs.crc_table_id = c.id;
DROP MATERIALIZED VIEW master_index_stats_old;
DROP MATERIALIZED VIEW index_stats_old;

@ -1,53 +0,0 @@
-- @formatter:off
DROP VIEW cache_stats;
DROP MATERIALIZED VIEW crc_table_stats;
DROP MATERIALIZED VIEW version_list_stats;
CREATE MATERIALIZED VIEW version_list_stats AS
SELECT
v.blob_id,
vf.index_id,
COUNT(*) FILTER (WHERE b.id IS NOT NULL) AS valid_files,
COUNT(*) AS files,
SUM(length(b.data) + 2) FILTER (WHERE b.id IS NOT NULL) AS size,
SUM(group_blocks(vf.file_id, length(b.data) + 2)) AS blocks
FROM version_lists v
JOIN version_list_files vf ON vf.blob_id = v.blob_id
LEFT JOIN resolve_file(vf.index_id, vf.file_id, vf.version, vf.crc32) b ON TRUE
GROUP BY v.blob_id, vf.index_id;
CREATE UNIQUE INDEX ON version_list_stats (blob_id, index_id);
CREATE MATERIALIZED VIEW crc_table_stats AS
SELECT
c.id AS crc_table_id,
COUNT(*) FILTER (WHERE b.id IS NOT NULL AND a.crc32 <> 0) AS valid_archives,
COUNT(*) FILTER (WHERE a.crc32 <> 0) AS archives,
SUM(COALESCE(s.valid_files, 0)) AS valid_files,
SUM(COALESCE(s.files, 0)) AS files,
SUM(COALESCE(s.size, 0)) + SUM(COALESCE(length(b.data), 0)) AS size,
SUM(COALESCE(s.blocks, 0)) + SUM(COALESCE(group_blocks(a.archive_id, length(b.data)), 0)) AS blocks
FROM crc_tables c
LEFT JOIN crc_table_archives a ON a.crc_table_id = c.id
LEFT JOIN resolve_archive(a.archive_id, a.crc32) b ON TRUE
LEFT JOIN version_list_stats s ON s.blob_id = b.id
GROUP BY c.id;
CREATE UNIQUE INDEX ON crc_table_stats (crc_table_id);
CREATE VIEW cache_stats AS
SELECT
s.id AS scope_id,
c.id AS cache_id,
COALESCE(ms.valid_indexes, cs.valid_archives) AS valid_indexes,
COALESCE(ms.indexes, cs.archives) AS indexes,
COALESCE(ms.valid_groups, cs.valid_files) AS valid_groups,
COALESCE(ms.groups, cs.files) AS groups,
COALESCE(ms.valid_keys, 0) AS valid_keys,
COALESCE(ms.keys, 0) AS keys,
COALESCE(ms.size, cs.size) AS size,
COALESCE(ms.blocks, cs.blocks) AS blocks
FROM scopes s
CROSS JOIN caches c
LEFT JOIN master_index_stats ms ON ms.scope_id = s.id AND ms.master_index_id = c.id
LEFT JOIN crc_table_stats cs ON s.name = 'runescape' AND cs.crc_table_id = c.id;

@ -1,2 +0,0 @@
-- @formatter:off
ALTER TYPE key_source ADD VALUE 'hdos';

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

@ -1,7 +0,0 @@
-- @formatter:off
ALTER TABLE sources
ALTER COLUMN cache_id DROP NOT NULL,
ALTER COLUMN game_id DROP NOT NULL;
CREATE UNIQUE INDEX ON sources (type) WHERE type = 'cross_pollination';

@ -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,4 +1,3 @@
-- @formatter:off
ALTER TABLE games ALTER TABLE games
DROP COLUMN hostname, DROP COLUMN hostname,
DROP COLUMN port, DROP COLUMN port,

@ -1,3 +1,2 @@
-- @formatter:off
ALTER TABLE games ALTER TABLE games
DROP COLUMN key; DROP COLUMN key;

@ -1,43 +0,0 @@
var buildRegex = new RegExp('>([0-9]+)(?:[.]([0-9]+))?<');
function customSort(name, order, data) {
order = order === 'asc' ? 1 : -1;
data.sort(function (a, b) {
a = a[name];
b = b[name];
if (!a) {
return 1;
} else if (!b) {
return -1;
}
if (name === 'builds') {
return buildSort(a, b) * order;
} else {
if (a < b) {
return -order;
} else if (a === b) {
return 0;
} else {
return order;
}
}
});
}
function buildSort(a, b) {
a = buildRegex.exec(a);
b = buildRegex.exec(b);
var aMajor = parseInt(a[1]);
var bMajor = parseInt(b[1]);
if (aMajor !== bMajor) {
return aMajor - bMajor;
}
var aMinor = a[2] ? parseInt(a[2]) : 0;
var bMinor = b[2] ? parseInt(b[2]) : 0;
return aMinor - bMinor;
}

@ -1,285 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head th:replace="layout.html :: head">
<title>API - 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>API</h1>
<p>All endpoints accept requests from any origin. Range requests are not supported by any endpoint.</p>
<h2><code>GET /caches.json</code></h2>
<p>
Returns a list of all caches, including all data available on the main <a href="/caches">caches</a>
page, in JSON format:
</p>
<pre><code>[
{
// The cache's internal ID.
"id": 1,
// A scope is a group of related games. Missing groups are only located
// from caches for games in the same scope.
//
// Currently the "runescape" scope is used for the "runescape" and
// "oldschool" games. Each FunOrb game has its own scope.
//
// Your code must be prepared for new scopes to be added in the future.
"scope": "runescape",
// The game's name. Your code must be prepared for new games to be
// added in the future.
"game": "runescape",
// Currently either "live" or "beta", but your code must be prepared
// for new environments to be added in the future.
"environment": "live",
// The language's ISO-639-1 code. Currently either "en", "de", "fr" or
// "pt", but your code must be prepared for new languages to be added
// in the future.
"language": "en",
// A list of build numbers the cache is associated with, which may be
// empty if the build number(s) are not known.
"builds": [
{
// The major number is always set.
"major": 549,
// The minor number may be null.
"minor": null
},
{
"major": 550,
"minor": null
}
],
// The earliest timestamp the cache was available to users, in ISO 8601
// format. May be null if not known.
"timestamp": "2009-06-12T14:55:58Z",
// A list of users who provided a copy of this cache.
//
// May be empty if the users wished to remain anonymous.
//
// The value "Jagex" indicates the cache was directly downloaded from
// Jagex's servers by the OpenRS2 project, so we are completely certain
// it is genuine. This value will never be used for a cache obtained
// from a third party.
"sources": [
"Erand",
"Hlwys",
"Jagex",
"K4rn4ge",
"Nathan",
"Rune-Wars"
],
// In old engine caches, the number of valid .jag archives that are not
// missing.
//
// In new engine caches, the number of valid JS5 indexes that are not
// missing.
//
// May be null if the cache is still being processed.
"valid_indexes": 29,
// In old engine caches, the total number of .jag archives that should
// exist, based on the cache's CRC table.
//
// In new engine caches, the total number of JS5 indexes that should
// exist, based on the JS5 master index.
//
// May be null if the cache is still being processed.
"indexes": 29,
// The number of valid files (old engine) or valid groups (new engine)
// that are not missing. May be null if the cache is still being processed.
"valid_groups": 71002,
// In old engine caches, the total number of files that should exist,
// based on the cache's versionlist.jag archive.
//
// In new engine caches, the total number of groups that should exist,
// based on the JS5 indexes that are available.
//
// May be null if the cache is still being processed.
"groups": 71146,
// The number of encrypted groups for which a valid key is available.
// May be null if the cache is still being processed.
"valid_keys": 1203,
// The total number of encrypted groups in the cache. May be null if
// the cache is still being processed.
"keys": 1240,
// The total size of all groups in the cache in bytes. May be null if
// the cache is still being processed.
"size": 74970573,
// The number of 520-byte blocks required to store the cache's data in
// a .dat2 file. May be null if the cache is still being processed.
"blocks": 185273,
// A boolean flag indicating if the cache is small enough to be
// downloaded in .dat2/.idx format. May be null if the cache is still
// being processed.
"disk_store_valid": true
},
...
]</code></pre>
<h2><code>GET /caches/&lt;scope&gt;/&lt;id&gt;/disk.zip</code></h2>
<p>
Returns a cache as a ZIP archive of <code>.dat/.idx</code>
(old engine) or <code>.dat2/.idx</code> (new engine) files. All
files are stored underneath a <code>cache</code> subdirectory
in the zip archive.
</p>
<h2><code>GET /caches/&lt;scope&gt;/&lt;id&gt;/flat-file.tar.gz</code></h2>
<p>
Returns a cache as a gzipped tarball of files, where each
file in the tarball holds a single file from the cache (old
engine) or single group (new engine).
</p>
<p>
The paths within the archive all have a format of
<code>cache/&lt;index&gt;/&lt;file&gt;.dat</code> (old engine)
or <code>cache/&lt;archive&gt;/&lt;group&gt;.dat</code> (new
engine).
</p>
<p>The two byte version trailers are included.</p>
<h2><code>GET /caches/&lt;scope&gt;/&lt;id&gt;/keys.json</code></h2>
<p>Returns a list of valid XTEA keys for the cache in JSON format:</p>
<pre><code>[
{
// The ID of the archive containing the group the key is used for.
// Typically this is 5 (maps), but do note that RuneScape 3 does
// support encrypting interfaces, though the functionality has not yet
// been used, and some FunOrb games also have encrypted groups.
"archive": 5,
// The ID of the group the key is used for.
"group": 1,
// The group's name hash, or null if the group has no name.
"name_hash": -1153472937,
// The name of the group, if available, or null if the group has no
// name or if the name is not known.
"name": "l40_55",
// The ID of the map square, if the group is an encrypted loc group
// (has a name of lX_Z). The map square ID is ((X &lt;&lt; 8) | Z).
// null if the group is not an encrypted loc group.
"mapsquare": 10295,
// The XTEA key, represented as four 32-bit integers.
"key": [
-1920480496,
-1423914110,
951774544,
-1419269290
]
},
...
]</code></pre>
<h2><code>GET /caches/&lt;scope&gt;/&lt;id&gt;/keys.zip</code></h2>
<p>
Returns a zip archive file of valid XTEA keys for loc groups.
Each key is stored in a text file containing four lines, with
each line containing a 32-bit component of the key as a decimal
string. The paths within the archive all have a format of
<code>keys/&lt;mapsquare&gt;.txt</code>.
</p>
<h2><code>GET /caches/&lt;scope&gt;/&lt;id&gt;/map.png</code></h2>
<p>
Renders the map squares in the cache, with a coloured outline
representing whether we have a valid key for each map square or
not:
</p>
<ul>
<li><strong>Valid key:</strong> green outline.</li>
<li><strong>Loc group is not encrypted:</strong> green outline.</li>
<li><strong>Empty loc group:</strong> grey outline.</li>
<li><strong>Key unknown:</strong> red outline.</li>
</ul>
<p>
Empty loc groups may be replaced with an unencrypted equivalent
with a cache editor.
</p>
<h2><code>GET /caches/&lt;scope&gt;/&lt;id&gt;/archives/&lt;archive&gt;/groups/&lt;group&gt;.dat</code></h2>
<p>
Returns a single file (old engine) or group (new engine) in
binary format. The response contains a <code>.jag</code>
archive (index 0 of an old engine cache), a GZIP-compressed
file (the remaining indexes of an old engine cache) or
JS5-compressed data (new engine cache, also known as a
container). The two byte version trailer is not included.
</p>
<h2><code>GET /keys/all.json</code></h2>
<p>
Returns a list of all XTEA keys in the database, including
candidate keys that have not been validated against any cache.
</p>
<pre><code>[
// The XTEA key, represented as four 32-bit integers.
[
-2147135705,
1113423446,
1294100345,
946019601
],
...
]</code></pre>
<h2><code>GET /keys/valid.json</code></h2>
<p>
Returns a list of XTEA keys in the database, only including
keys validated against at least one cache.
</p>
<pre><code>[
// The XTEA key, represented as four 32-bit integers.
[
-2147135705,
1113423446,
1294100345,
946019601
],
...
]</code></pre>
</main>
</body>
</html>

@ -12,7 +12,7 @@
<main class="container"> <main class="container">
<h1>Caches</h1> <h1>Caches</h1>
<div class="table-responsive"> <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"> <table class="table table-striped table-bordered table-hover" data-toggle="table" data-filter-control="true" data-sticky-header="true">
<thead class="table-dark"> <thead class="table-dark">
<tr> <tr>
<th data-field="game" data-filter-control="select">Game</th> <th data-field="game" data-filter-control="select">Game</th>
@ -34,22 +34,20 @@
<td th:text="${cache.game}">runescape</td> <td th:text="${cache.game}">runescape</td>
<td th:text="${cache.environment}">live</td> <td th:text="${cache.environment}">live</td>
<td th:text="${cache.language}">en</td> <td th:text="${cache.language}">en</td>
<td class="text-end"> <td class="text-right">
<span th:each="build, it : ${cache.builds}" th:remove="tag"> <span th:each="build, it : ${cache.builds}" th:remove="tag">
<span th:text="${build}">550</span> <span th:text="${build}">550</span>
<br th:remove="${it.last}? 'all' : 'none'" /> <br th:remove="${it.last}? 'all' : 'none'" />
</span> </span>
</td> </td>
<td> <td>
<span th:if="${cache.timestamp}" th:remove="tag"> <span th:text="${#temporals.format(cache.timestamp, 'yyyy-MM-dd')}"></span>
<span th:text="${#temporals.format(cache.timestamp, 'yyyy-MM-dd')}"></span> <br />
<br /> <span th:text="${#temporals.format(cache.timestamp, 'HH:mm:ss')}"></span>
<span th:text="${#temporals.format(cache.timestamp, 'HH:mm:ss')}"></span>
</span>
</td> </td>
<td th:text="${#strings.setJoin(cache.sources, ', ')}"></td> <td th:text="${#strings.setJoin(cache.sources, ', ')}"></td>
<td th:classappend="${cache.stats}? (${cache.stats.allIndexesValid}? 'table-success' : 'table-danger')" <td th:classappend="${cache.stats}? (${cache.stats.allIndexesValid}? 'table-success' : 'table-danger')"
class="text-end"> class="text-right">
<span <span
th:text="${cache.stats}? ${cache.stats.validIndexes} + '&nbsp;/&nbsp;' + ${cache.stats.indexes} : 'Calculating...'"></span> th:text="${cache.stats}? ${cache.stats.validIndexes} + '&nbsp;/&nbsp;' + ${cache.stats.indexes} : 'Calculating...'"></span>
<br /> <br />
@ -57,7 +55,7 @@
th:text="${cache.stats}? '(' + ${#numbers.formatPercent(cache.stats.validIndexesFraction, 1, 2)} + ')'"></span> th:text="${cache.stats}? '(' + ${#numbers.formatPercent(cache.stats.validIndexesFraction, 1, 2)} + ')'"></span>
</td> </td>
<td th:classappend="${cache.stats}? (${cache.stats.allGroupsValid}? 'table-success' : 'table-warning')" <td th:classappend="${cache.stats}? (${cache.stats.allGroupsValid}? 'table-success' : 'table-warning')"
class="text-end"> class="text-right">
<span <span
th:text="${cache.stats}? ${#numbers.formatInteger(cache.stats.validGroups, 1, 'COMMA')} + '&nbsp;/&nbsp;' + ${#numbers.formatInteger(cache.stats.groups, 1, 'COMMA')} : 'Calculating...'"></span> th:text="${cache.stats}? ${#numbers.formatInteger(cache.stats.validGroups, 1, 'COMMA')} + '&nbsp;/&nbsp;' + ${#numbers.formatInteger(cache.stats.groups, 1, 'COMMA')} : 'Calculating...'"></span>
<br /> <br />
@ -65,7 +63,7 @@
th:text="${cache.stats}? '(' + ${#numbers.formatPercent(cache.stats.validGroupsFraction, 1, 2)} + ')'"></span> th:text="${cache.stats}? '(' + ${#numbers.formatPercent(cache.stats.validGroupsFraction, 1, 2)} + ')'"></span>
</td> </td>
<td th:classappend="${cache.stats}? (${cache.stats.allKeysValid}? 'table-success' : 'table-warning')" <td th:classappend="${cache.stats}? (${cache.stats.allKeysValid}? 'table-success' : 'table-warning')"
class="text-end"> class="text-right">
<span <span
th:text="${cache.stats}? ${#numbers.formatInteger(cache.stats.validKeys, 1, 'COMMA')} + '&nbsp;/&nbsp;' + ${#numbers.formatInteger(cache.stats.keys, 1, 'COMMA')} : 'Calculating...'"></span> th:text="${cache.stats}? ${#numbers.formatInteger(cache.stats.validKeys, 1, 'COMMA')} + '&nbsp;/&nbsp;' + ${#numbers.formatInteger(cache.stats.keys, 1, 'COMMA')} : 'Calculating...'"></span>
<br /> <br />
@ -74,7 +72,7 @@
</td> </td>
<!--/*@thymesVar id="#byteunits" type="org.openrs2.archive.web.ByteUnits"*/--> <!--/*@thymesVar id="#byteunits" type="org.openrs2.archive.web.ByteUnits"*/-->
<td th:text="${cache.stats}? ${#byteunits.format(cache.stats.size)} : 'Calculating...'" <td th:text="${cache.stats}? ${#byteunits.format(cache.stats.size)} : 'Calculating...'"
class="text-end">Calculating... class="text-right">Calculating...
</td> </td>
<td> <td>
<div class="btn-group"> <div class="btn-group">
@ -86,25 +84,25 @@
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li th:if="${cache.stats != null and cache.stats.diskStoreValid}"><a <li th:if="${cache.stats != null and cache.stats.diskStoreValid}"><a
th:href="${'/caches/' + cache.scope + '/' + cache.id + '/disk.zip'}" th:href="${'/caches/' + cache.id + '/disk.zip'}"
class="dropdown-item">Cache (.dat2/.idx)</a></li> class="dropdown-item">Cache (.dat2/.idx)</a></li>
<li><a th:href="${'/caches/' + cache.scope + '/' + cache.id + '/flat-file.tar.gz'}" <li><a th:href="${'/caches/' + cache.id + '/flat-file.tar.gz'}"
class="dropdown-item">Cache (Flat file)</a></li> class="dropdown-item">Cache (Flat file)</a></li>
<li> <li>
<hr class="dropdown-divider" /> <hr class="dropdown-divider" />
</li> </li>
<li><a th:href="${'/caches/' + cache.scope + '/' + cache.id + '/keys.json'}" <li><a th:href="${'/caches/' + cache.id + '/keys.json'}"
class="dropdown-item">Keys (JSON)</a></li> class="dropdown-item">Keys (JSON)</a></li>
<li><a th:href="${'/caches/' + cache.scope + '/' + cache.id + '/keys.zip'}" <li><a th:href="${'/caches/' + cache.id + '/keys.zip'}"
class="dropdown-item">Keys (Text)</a></li> class="dropdown-item">Keys (Text)</a></li>
<li> <li>
<hr class="dropdown-divider" /> <hr class="dropdown-divider" />
</li> </li>
<li><a th:href="${'/caches/' + cache.scope + '/' + cache.id + '/map.png'}" <li><a th:href="${'/caches/' + cache.id + '/map.png'}"
class="dropdown-item">Map</a></li> class="dropdown-item">Map</a></li>
</ul> </ul>
</div> </div>
<a th:href="${'/caches/' + cache.scope + '/' + cache.id}" <a th:href="${'/caches/' + cache.id}"
class="btn btn-secondary btn-sm">More</a> class="btn btn-secondary btn-sm">More</a>
</div> </div>
</td> </td>
@ -112,10 +110,6 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<p>
The total size of all caches in the archive is
<strong th:text="${#byteunits.format(totalSize)}">0 B</strong>.
</p>
<p> <p>
<sup id="empty-locs">1</sup> Map squares in the middle of the <sup id="empty-locs">1</sup> Map squares in the middle of the
sea are unreachable by normal players, making it impossible to sea are unreachable by normal players, making it impossible to

@ -52,19 +52,19 @@
<div class="btn-toolbar"> <div class="btn-toolbar">
<div class="btn-group me-2"> <div class="btn-group me-2">
<a th:if="${cache.stats != null and cache.stats.diskStoreValid}" <a th:if="${cache.stats != null and cache.stats.diskStoreValid}"
th:href="${'/caches/' + scope + '/' + cache.id + '/disk.zip'}" th:href="${'/caches/' + cache.id + '/disk.zip'}"
class="btn btn-primary btn-sm">Cache (.dat2/.idx)</a> class="btn btn-primary btn-sm">Cache (.dat2/.idx)</a>
<a th:href="${'/caches/' + scope + '/' + cache.id + '/flat-file.tar.gz'}" <a th:href="${'/caches/' + cache.id + '/flat-file.tar.gz'}"
class="btn btn-primary btn-sm">Cache (Flat file)</a> class="btn btn-primary btn-sm">Cache (Flat file)</a>
</div> </div>
<div class="btn-group me-2"> <div class="btn-group me-2">
<a th:href="${'/caches/' + scope + '/' + cache.id + '/keys.json'}" <a th:href="${'/caches/' + cache.id + '/keys.json'}"
class="btn btn-primary btn-sm">Keys (JSON)</a> class="btn btn-primary btn-sm">Keys (JSON)</a>
<a th:href="${'/caches/' + scope + '/' + cache.id + '/keys.zip'}" <a th:href="${'/caches/' + cache.id + '/keys.zip'}"
class="btn btn-primary btn-sm">Keys (Text)</a> class="btn btn-primary btn-sm">Keys (Text)</a>
</div> </div>
<div class="btn-group"> <div class="btn-group">
<a th:href="${'/caches/' + scope + '/' + cache.id + '/map.png'}" <a th:href="${'/caches/' + cache.id + '/map.png'}"
class="btn btn-primary btn-sm">Map</a> class="btn btn-primary btn-sm">Map</a>
</div> </div>
</div> </div>
@ -102,7 +102,7 @@
<td th:text="${source.game}">runescape</td> <td th:text="${source.game}">runescape</td>
<td th:text="${source.environment}">live</td> <td th:text="${source.environment}">live</td>
<td th:text="${source.language}">en</td> <td th:text="${source.language}">en</td>
<td th:text="${source.build}" class="text-end">550</td> <td th:text="${source.build}" class="text-right">550</td>
<td th:text="${#temporals.format(source.timestamp, 'yyyy-MM-dd HH:mm:ss')}"></td> <td th:text="${#temporals.format(source.timestamp, 'yyyy-MM-dd HH:mm:ss')}"></td>
<td th:text="${source.name}"></td> <td th:text="${source.name}"></td>
<td th:text="${source.description}"></td> <td th:text="${source.description}"></td>
@ -124,43 +124,37 @@
<th>Archive</th> <th>Archive</th>
<th>Version</th> <th>Version</th>
<th>Checksum</th> <th>Checksum</th>
<th>Digest</th>
<th>Groups</th> <th>Groups</th>
<th>Keys<sup><a href="/caches#empty-locs">1</a></sup></th> <th>Total uncompressed length</th>
<th>Size<sup><a href="/caches#size">2</a></sup></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr th:each="entry, it : ${cache.masterIndex.entries}" th:with="archive=${cache.archives[it.index]}"> <tr th:each="entry, it : ${cache.masterIndex.entries}">
<td th:text="${it.index}" class="text-end">0</td> <td th:text="${it.index}" class="text-right">0</td>
<td th:text="${#numbers.formatInteger(entry.version, 1, 'COMMA')}" class="text-end">0</td> <td th:text="${#numbers.formatInteger(entry.version, 1, 'COMMA')}" class="text-right">0</td>
<td class="text-end"> <td class="text-right">
<code th:text="${entry.checksum}">0</code> <code th:text="${entry.checksum}">0</code>
</td> </td>
<div th:switch="true" th:remove="tag"> <td>
<div th:case="${archive.stats != null}" th:remove="tag"> <code
<td th:classappend="${archive.stats.allGroupsValid}? 'table-success' : 'table-warning'" class="text-end"> th:if="${cache.masterIndex.format >= @org.openrs2.cache.MasterIndexFormat@DIGESTS}"><span
<span th:text="${#numbers.formatInteger(archive.stats.validGroups, 1, 'COMMA')} + '&nbsp;/&nbsp;' + ${#numbers.formatInteger(archive.stats.groups, 1, 'COMMA')}"></span> th:remove="tag"
<br /> th:text="${@io.netty.buffer.ByteBufUtil@hexDump(entry.digest).substring(0, 64)}"></span>&ZeroWidthSpace;<span
<span th:text="'(' + ${#numbers.formatPercent(archive.stats.validGroupsFraction, 1, 2)} + ')'"></span> th:remove="tag"
</td> th:text="${@io.netty.buffer.ByteBufUtil@hexDump(entry.digest).substring(64)}"></span></code>
<td th:classappend="${archive.stats.allKeysValid}? 'table-success' : 'table-warning'" class="text-end"> </td>
<span th:text="${#numbers.formatInteger(archive.stats.validKeys, 1, 'COMMA')} + '&nbsp;/&nbsp;' + ${#numbers.formatInteger(archive.stats.keys, 1, 'COMMA')}"></span> <td class="text-right">
<br /> <span
<span th:text="'(' + ${#numbers.formatPercent(archive.stats.validKeysFraction, 1, 2)} + ')'"></span> th:if="${cache.masterIndex.format >= @org.openrs2.cache.MasterIndexFormat@LENGTHS}"
</td> th:text="${#numbers.formatInteger(entry.groups, 1, 'COMMA')}"></span>
<!--/*@thymesVar id="#byteunits" type="org.openrs2.archive.web.ByteUnits"*/--> </td>
<td th:text="${#byteunits.format(archive.stats.size)}" class="text-end">0 B</td> <td class="text-right">
</div> <!--/*@thymesVar id="#byteunits" type="org.openrs2.archive.web.ByteUnits"*/-->
<div th:case="${archive.resolved}" th:remove="tag"> <span
<td class="text-center" colspan="3">Calculating...</td> th:if="${cache.masterIndex.format >= @org.openrs2.cache.MasterIndexFormat@LENGTHS}"
</div> th:text="${#byteunits.format(@java.lang.Integer@toUnsignedLong(entry.totalUncompressedLength))}"></span>
<div th:case="${entry.checksum != 0 || entry.version != 0}" th:remove="tag"> </td>
<td class="text-center table-danger" colspan="3">Index missing</td>
</div>
<div th:case="true" th:remove="tag">
<td class="text-center text-muted" colspan="3">N/A</td>
</div>
</div>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@ -176,54 +170,18 @@
<tr> <tr>
<th>Archive</th> <th>Archive</th>
<th>Checksum</th> <th>Checksum</th>
<th>Size<sup><a href="/caches#size">2</a></sup></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr th:each="entry, it : ${cache.checksumTable.entries}" th:with="archive=${cache.archives[it.index]}"> <tr th:each="entry, it : ${cache.checksumTable.entries}">
<td th:text="${it.index}" class="text-end">0</td> <td th:text="${it.index}" class="text-right">0</td>
<td class="text-end"> <td class="text-right">
<code th:text="${entry}">0</code> <code th:text="${entry}">0</code>
</td> </td>
<div th:switch="true" th:remove="tag">
<!--/*@thymesVar id="#byteunits" type="org.openrs2.archive.web.ByteUnits"*/-->
<td th:case="${archive.stats != null}" th:text="${#byteunits.format(archive.stats.size)}" class="text-end">0 B</td>
<td th:case="${archive.resolved}" class="text-center">Calculating...</td>
<td th:case="${entry != 0}" class="text-center table-danger">Missing</td>
<td th:case="true" class="text-center text-muted">N/A</td>
</div>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
<div th:if="${cache.indexes}" th:remove="tag">
<h2>Version list</h2>
<div class="table-responsive">
<table class="table table-striped table-bordered table-hover">
<thead class="table-dark">
<tr>
<th>Index</th>
<th>Files</th>
<th>Size<sup><a href="/caches#size">2</a></sup></th>
</tr>
</thead>
<tbody>
<tr th:each="index, it : ${cache.indexes}">
<td th:text="${it.index + 1}" class="text-end">0</td>
<td th:classappend="${index.allFilesValid}? 'table-success' : 'table-warning'" class="text-end">
<span th:text="${#numbers.formatInteger(index.validFiles, 1, 'COMMA')} + '&nbsp;/&nbsp;' + ${#numbers.formatInteger(index.files, 1, 'COMMA')}"></span>
<br />
<span th:text="'(' + ${#numbers.formatPercent(index.validFilesFraction, 1, 2)} + ')'"></span>
</td>
<!--/*@thymesVar id="#byteunits" type="org.openrs2.archive.web.ByteUnits"*/-->
<td th:text="${#byteunits.format(index.size)}" class="text-end">0 B</td>
</tr>
</tbody>
</table>
</div>
</div>
</div> </div>
</main> </main>
</body> </body>

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

Loading…
Cancel
Save