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