forked from openrs2/openrs2
There are still some gaps but I want to get this committed and possibly deployed before doing further work. Remaining items include: - Mach-O support - New engine loader ArtifactLink support - Post-668 client support - FunOrb support Signed-off-by: Graham <gpe@openrs2.org>
parent
c65cc2ff59
commit
73e959a3cb
@ -0,0 +1,11 @@ |
|||||||
|
package org.openrs2.archive.client |
||||||
|
|
||||||
|
public enum class Architecture { |
||||||
|
INDEPENDENT, |
||||||
|
UNIVERSAL, |
||||||
|
X86, |
||||||
|
AMD64, |
||||||
|
POWERPC, |
||||||
|
SPARC, |
||||||
|
SPARCV9 |
||||||
|
} |
@ -0,0 +1,35 @@ |
|||||||
|
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) |
||||||
|
} |
@ -0,0 +1,46 @@ |
|||||||
|
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") |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
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 |
||||||
|
} |
@ -0,0 +1,14 @@ |
|||||||
|
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() |
||||||
|
) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,427 @@ |
|||||||
|
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 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 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 links = mutableListOf<ArtifactLinkExport>() |
||||||
|
|
||||||
|
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, 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 |
||||||
|
) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,740 @@ |
|||||||
|
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.tree.ClassNode |
||||||
|
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.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.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 |
||||||
|
|
||||||
|
@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>) { |
||||||
|
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" } |
||||||
|
import(parse(buf)) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public suspend fun import(artifact: Artifact) { |
||||||
|
database.execute { connection -> |
||||||
|
importer.prepare(connection) |
||||||
|
import(connection, artifact) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private fun import(connection: Connection, artifact: Artifact) { |
||||||
|
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() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public suspend fun refresh() { |
||||||
|
database.execute { connection -> |
||||||
|
importer.prepare(connection) |
||||||
|
|
||||||
|
var lastId: Long? = null |
||||||
|
val blobs = mutableListOf<ByteArray>() |
||||||
|
|
||||||
|
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()) { |
||||||
|
lastId = rows.getLong(1) |
||||||
|
blobs += rows.getBytes(2) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (blobs.isEmpty()) { |
||||||
|
return@execute |
||||||
|
} |
||||||
|
|
||||||
|
for (blob in blobs) { |
||||||
|
Unpooled.wrappedBuffer(blob).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 { |
||||||
|
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 namePointer = buf.readerIndex() + buf.getIntLE(buf.readerIndex() + namePointerTable + 4 * i) |
||||||
|
|
||||||
|
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 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(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 = parseLoaderBuild(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("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(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) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
for (match in NEW_ENGINE_VERSION_MATCHER.match(method)) { |
||||||
|
val new = match[0] as TypeInsnNode |
||||||
|
if (new.desc != "client") { |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
val candidates = mutableListOf<Int>() |
||||||
|
|
||||||
|
for (insn in match) { |
||||||
|
val candidate = insn.intConstant |
||||||
|
if (candidate != null && candidate in NEW_ENGINE_BUILDS) { |
||||||
|
candidates += candidate |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
candidates -= NEW_ENGINE_RESOLUTIONS |
||||||
|
|
||||||
|
val version = candidates.singleOrNull() |
||||||
|
if (version != null) { |
||||||
|
return CacheExporter.Build(version, null) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return null |
||||||
|
} |
||||||
|
|
||||||
|
private fun parseLoaderBuild(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 |
||||||
|
) |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
// TODO(gpe): new engine support |
||||||
|
return emptyList() |
||||||
|
} |
||||||
|
|
||||||
|
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_VERSION_MATCHER = InsnMatcher.compile("NEW .*? RETURN") |
||||||
|
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)+") |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,30 @@ |
|||||||
|
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() |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,25 @@ |
|||||||
|
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.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 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) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,7 @@ |
|||||||
|
package org.openrs2.archive.client |
||||||
|
|
||||||
|
public enum class Jvm { |
||||||
|
INDEPENDENT, |
||||||
|
SUN, |
||||||
|
MICROSOFT |
||||||
|
} |
@ -0,0 +1,43 @@ |
|||||||
|
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") |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
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() |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,82 @@ |
|||||||
|
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) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,95 @@ |
|||||||
|
-- @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) |
||||||
|
); |
@ -0,0 +1,63 @@ |
|||||||
|
<!DOCTYPE html> |
||||||
|
<html xmlns:th="http://www.thymeleaf.org" lang="en"> |
||||||
|
<head th:replace="layout.html :: head(title='Clients')"> |
||||||
|
<title>Clients - OpenRS2 Archive</title> |
||||||
|
<link rel="stylesheet" href="/webjars/bootstrap/css/bootstrap.min.css" /> |
||||||
|
<link rel="stylesheet" href="/static/css/openrs2.css" /> |
||||||
|
<script src="/webjars/jquery/jquery.min.js" defer></script> |
||||||
|
<script src="/webjars/bootstrap/js/bootstrap.bundle.min.js" defer></script> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
<nav th:replace="layout.html :: nav(active='clients')"></nav> |
||||||
|
<main class="container"> |
||||||
|
<h1>Clients</h1> |
||||||
|
<div class="table-responsive"> |
||||||
|
<table class="table table-striped table-bordered table-hover" data-toggle="table" data-filter-control="true" data-sticky-header="true" data-custom-sort="customSort"> |
||||||
|
<thead class="table-dark"> |
||||||
|
<tr> |
||||||
|
<th data-field="game" data-filter-control="select">Game</th> |
||||||
|
<th data-field="environment" data-filter-control="select">Env</th> |
||||||
|
<th data-field="build" data-filter-control="input" data-sortable="true">Build</th> |
||||||
|
<th data-field="timestamp" data-sortable="true">Timestamp</th> |
||||||
|
<th data-field="type" data-filter-control="select">Type</th> |
||||||
|
<th data-field="format" data-filter-control="select">Format</th> |
||||||
|
<th data-field="os" data-filter-control="select">OS</th> |
||||||
|
<th data-field="arch" data-filter-control="select">Arch</th> |
||||||
|
<th data-field="jvm" data-filter-control="select">JVM</th> |
||||||
|
<th>Size</th> |
||||||
|
<th>Links</th> |
||||||
|
</tr> |
||||||
|
</thead> |
||||||
|
<tbody> |
||||||
|
<!--/*@thymesVar id="artifacts" type="java.util.List<org.openrs2.archive.client.ClientExporter.ArtifactSummary>"*/--> |
||||||
|
<tr th:each="artifact : ${artifacts}"> |
||||||
|
<td th:text="${artifact.game}">runescape</td> |
||||||
|
<td th:text="${artifact.environment}">live</td> |
||||||
|
<td th:text="${artifact.build}" class="text-end">550</td> |
||||||
|
<td> |
||||||
|
<span th:if="${artifact.timestamp}" th:remove="tag"> |
||||||
|
<span th:text="${#temporals.format(artifact.timestamp, 'yyyy-MM-dd')}"></span> |
||||||
|
<br /> |
||||||
|
<span th:text="${#temporals.format(artifact.timestamp, 'HH:mm:ss')}"></span> |
||||||
|
</span> |
||||||
|
</td> |
||||||
|
<td th:text="${artifact.type.toString().toLowerCase()}">client_gl</td> |
||||||
|
<td th:text="${artifact.format.toString().toLowerCase()}">pack200</td> |
||||||
|
<td th:text="${artifact.os.toString().toLowerCase()}">independent</td> |
||||||
|
<td th:text="${artifact.arch.toString().toLowerCase()}">independent</td> |
||||||
|
<td th:text="${artifact.jvm.toString().toLowerCase()}">independent</td> |
||||||
|
<!--/*@thymesVar id="#byteunits" type="org.openrs2.archive.web.ByteUnits"*/--> |
||||||
|
<td th:text="${#byteunits.format(artifact.size)}" class="text-end">494 KiB</td> |
||||||
|
<td> |
||||||
|
<div class="btn-group"> |
||||||
|
<a th:href="${'/clients/' + artifact.id + '.dat'}" class="btn btn-primary btn-sm">Download</a> |
||||||
|
<a th:href="${'/clients/' + artifact.id}" class="btn btn-secondary btn-sm">More</a> |
||||||
|
</div> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
</div> |
||||||
|
</main> |
||||||
|
</body> |
||||||
|
</html> |
@ -0,0 +1,129 @@ |
|||||||
|
<!DOCTYPE html> |
||||||
|
<html xmlns:th="http://www.thymeleaf.org" lang="en"> |
||||||
|
<head th:replace="layout.html :: head(title='Cache')"> |
||||||
|
<title>Client - OpenRS2 Archive</title> |
||||||
|
<link rel="stylesheet" href="/webjars/bootstrap/css/bootstrap.min.css" /> |
||||||
|
<link rel="stylesheet" href="/static/css/openrs2.css" /> |
||||||
|
<script src="/webjars/jquery/jquery.min.js" defer></script> |
||||||
|
<script src="/webjars/bootstrap/js/bootstrap.bundle.min.js" defer></script> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
<nav th:replace="layout.html :: nav"></nav> |
||||||
|
<main class="container"> |
||||||
|
<h1>Client</h1> |
||||||
|
|
||||||
|
<!--/*@thymesVar id="artifact" type="org.openrs2.archive.client.ClientExporter.Artifact"*/--> |
||||||
|
<table class="table table-striped table-bordered table-hover"> |
||||||
|
<tr> |
||||||
|
<th class="table-dark">Game</th> |
||||||
|
<td th:text="${artifact.summary.game}">runescape</td> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<th class="table-dark">Environment</th> |
||||||
|
<td th:text="${artifact.summary.environment}">live</td> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<th class="table-dark">Build</th> |
||||||
|
<td th:text="${artifact.summary.build}">550</td> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<th class="table-dark">Timestamp</th> |
||||||
|
<td th:text="${#temporals.format(artifact.summary.timestamp, 'yyyy-MM-dd HH:mm:ss')}"></td> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<th class="table-dark">Type</th> |
||||||
|
<td th:text="${artifact.summary.type.toString().toLowerCase()}">client_gl</td> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<th class="table-dark">Format</th> |
||||||
|
<td th:text="${artifact.summary.format.toString().toLowerCase()}">pack200</td> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<th class="table-dark">OS</th> |
||||||
|
<td th:text="${artifact.summary.os.toString().toLowerCase()}">independent</td> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<th class="table-dark">Architecture</th> |
||||||
|
<td th:text="${artifact.summary.arch.toString().toLowerCase()}">independent</td> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<th class="table-dark">JVM</th> |
||||||
|
<td th:text="${artifact.summary.jvm.toString().toLowerCase()}">independent</td> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<th class="table-dark">Size</th> |
||||||
|
<!--/*@thymesVar id="#byteunits" type="org.openrs2.archive.web.ByteUnits"*/--> |
||||||
|
<td th:text="${#byteunits.format(artifact.summary.size)}">494 KiB</td> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<th class="table-dark">Checksum</th> |
||||||
|
<td th:text="${artifact.crc32}"></td> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<th class="table-dark">SHA-1</th> |
||||||
|
<td> |
||||||
|
<code th:text="${artifact.sha1Hex}"></code> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<th class="table-dark">Download</th> |
||||||
|
<td> |
||||||
|
<a th:href="${'/clients/' + artifact.summary.id + '.dat'}" class="btn btn-primary btn-sm">Download</a> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
</table> |
||||||
|
|
||||||
|
<div th:if="${artifact.links}" th:tag="remove"> |
||||||
|
<h2>Links</h2> |
||||||
|
|
||||||
|
<div class="table-responsive"> |
||||||
|
<table class="table table-striped table-bordered table-hover"> |
||||||
|
<thead class="table-dark"> |
||||||
|
<tr> |
||||||
|
<th>Build</th> |
||||||
|
<th>Timestamp</th> |
||||||
|
<th>Type</th> |
||||||
|
<th>Format</th> |
||||||
|
<th>OS</th> |
||||||
|
<th>Arch</th> |
||||||
|
<th>JVM</th> |
||||||
|
<th>Checksum</th> |
||||||
|
<th>SHA-1</th> |
||||||
|
<th>Size</th> |
||||||
|
<th>Links</th> |
||||||
|
</tr> |
||||||
|
</thead> |
||||||
|
<tbody> |
||||||
|
<tr th:each="entry : ${artifact.links}" th:classappend="${entry.id}? 'table-success' : 'table-danger'"> |
||||||
|
<td th:text="${entry.build}" class="text-end">550</td> |
||||||
|
<td> |
||||||
|
<span th:if="${entry.timestamp}" th:remove="tag"> |
||||||
|
<span th:text="${#temporals.format(entry.timestamp, 'yyyy-MM-dd')}"></span> |
||||||
|
<br /> |
||||||
|
<span th:text="${#temporals.format(entry.timestamp, 'HH:mm:ss')}"></span> |
||||||
|
</span> |
||||||
|
</td> |
||||||
|
<td th:text="${entry.link.type.toString().toLowerCase()}">client_gl</td> |
||||||
|
<td th:text="${entry.link.format.toString().toLowerCase()}">pack200</td> |
||||||
|
<td th:text="${entry.link.os.toString().toLowerCase()}">independent</td> |
||||||
|
<td th:text="${entry.link.arch.toString().toLowerCase()}">independent</td> |
||||||
|
<td th:text="${entry.link.jvm.toString().toLowerCase()}">independent</td> |
||||||
|
<td th:text="${entry.link.crc32}"></td> |
||||||
|
<td><code th:text="${entry.link.sha1Hex}"></code></td> |
||||||
|
<!--/*@thymesVar id="#byteunits" type="org.openrs2.archive.web.ByteUnits"*/--> |
||||||
|
<td th:text="${#byteunits.format(entry.link.size)}">494 KiB</td> |
||||||
|
<td th:if="${entry.id}"> |
||||||
|
<div class="btn-group"> |
||||||
|
<a th:href="${'/clients/' + entry.id + '.dat'}" class="btn btn-primary btn-sm">Download</a> |
||||||
|
<a th:href="${'/clients/' + entry.id}" class="btn btn-secondary btn-sm">More</a> |
||||||
|
</div> |
||||||
|
</td> |
||||||
|
<td th:unless="${entry.id}"></td> |
||||||
|
</tr> |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</main> |
||||||
|
</body> |
||||||
|
</html> |
Loading…
Reference in new issue