mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-03 10:54:38 -05:00
Comments, comments, and comments!! And future proofing (#162)
This commit is contained in:
@@ -27,6 +27,12 @@ import java.util.zip.ZipInputStream
|
|||||||
|
|
||||||
object BytecodeEditor {
|
object BytecodeEditor {
|
||||||
private val logger = KotlinLogging.logger {}
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace some java class references inside a jar with new ones that behave like Androids
|
||||||
|
*
|
||||||
|
* @param jarFile The JarFile to replace class references in
|
||||||
|
*/
|
||||||
fun fixAndroidClasses(jarFile: File) {
|
fun fixAndroidClasses(jarFile: File) {
|
||||||
val nodes = loadClasses(jarFile)
|
val nodes = loadClasses(jarFile)
|
||||||
.mapValues { (className, classFileBuffer) ->
|
.mapValues { (className, classFileBuffer) ->
|
||||||
@@ -37,6 +43,13 @@ object BytecodeEditor {
|
|||||||
saveAsJar(nodes, jarFile)
|
saveAsJar(nodes, jarFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load all classes inside the [jar] [File]
|
||||||
|
*
|
||||||
|
* @param jar The JarFile to load classes from
|
||||||
|
*
|
||||||
|
* @return [Map] with class names and [ByteArray]s of bytecode
|
||||||
|
*/
|
||||||
private fun loadClasses(jar: File): Map<String, ByteArray> {
|
private fun loadClasses(jar: File): Map<String, ByteArray> {
|
||||||
return JarFile(jar).use { jarFile ->
|
return JarFile(jar).use { jarFile ->
|
||||||
jarFile.entries()
|
jarFile.entries()
|
||||||
@@ -48,6 +61,14 @@ object BytecodeEditor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get class file in [jar] for [entry]
|
||||||
|
*
|
||||||
|
* @param jar The jar to get the class from
|
||||||
|
* @param entry The entry in the jar
|
||||||
|
*
|
||||||
|
* @return [Pair] of the class name plus the class [ByteArray], or null if it's not a valid class
|
||||||
|
*/
|
||||||
private fun readJar(jar: JarFile, entry: JarEntry): Pair<String, ByteArray>? {
|
private fun readJar(jar: JarFile, entry: JarEntry): Pair<String, ByteArray>? {
|
||||||
return try {
|
return try {
|
||||||
jar.getInputStream(entry).use { stream ->
|
jar.getInputStream(entry).use { stream ->
|
||||||
@@ -83,20 +104,66 @@ object BytecodeEditor {
|
|||||||
return ClassNode().also { cr.accept(it, ClassReader.EXPAND_FRAMES) }
|
return ClassNode().also { cr.accept(it, ClassReader.EXPAND_FRAMES) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The path where replacement classes will reside
|
||||||
|
*/
|
||||||
private const val replacementPath = "xyz/nulldev/androidcompat/replace"
|
private const val replacementPath = "xyz/nulldev/androidcompat/replace"
|
||||||
private const val simpleDateFormat = "java/text/SimpleDateFormat"
|
|
||||||
private const val replacementSimpleDateFormat = "$replacementPath/$simpleDateFormat"
|
|
||||||
|
|
||||||
private fun String?.replaceFormatFully() = if (this == simpleDateFormat) {
|
/**
|
||||||
replacementSimpleDateFormat
|
* List of classes that will be replaced
|
||||||
} else this
|
*/
|
||||||
private fun String?.replaceFormat() = this?.replace(simpleDateFormat, replacementSimpleDateFormat)
|
private val classesToReplace = listOf(
|
||||||
|
"java/text/SimpleDateFormat"
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace direct references to the class, used on places
|
||||||
|
* that don't have any other text then the class
|
||||||
|
*
|
||||||
|
* @return [String] of class or null if [String] was null
|
||||||
|
*/
|
||||||
|
private fun String?.replaceDirectly() = when (this) {
|
||||||
|
null -> this
|
||||||
|
in classesToReplace -> "$replacementPath/$this"
|
||||||
|
else -> this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace references to the class, used in places that have
|
||||||
|
* other text around the class references
|
||||||
|
*
|
||||||
|
* @return [String] with class references replaced,
|
||||||
|
* or null if [String] was null
|
||||||
|
*/
|
||||||
|
private fun String?.replaceIndirectly(): String? {
|
||||||
|
var classReference = this
|
||||||
|
if (classReference != null) {
|
||||||
|
classesToReplace.forEach {
|
||||||
|
classReference = classReference?.replace(it, "$replacementPath/$it")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return classReference
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace all references to certain classes inside the class file
|
||||||
|
* with ones that behave more like Androids
|
||||||
|
*
|
||||||
|
* @param classfileBuffer Class bytecode to load into ASM for ease of modification
|
||||||
|
*
|
||||||
|
* @return [ByteArray] with modified bytecode
|
||||||
|
*/
|
||||||
private fun transform(classfileBuffer: ByteArray): ByteArray {
|
private fun transform(classfileBuffer: ByteArray): ByteArray {
|
||||||
|
// Read the class and prepare to modify it
|
||||||
val cr = ClassReader(classfileBuffer)
|
val cr = ClassReader(classfileBuffer)
|
||||||
val cw = ClassWriter(cr, 0)
|
val cw = ClassWriter(cr, 0)
|
||||||
|
// Modify the class
|
||||||
cr.accept(
|
cr.accept(
|
||||||
object : ClassVisitor(Opcodes.ASM5, cw) {
|
object : ClassVisitor(Opcodes.ASM5, cw) {
|
||||||
|
// Modify field descriptor, for example
|
||||||
|
// class MangaYes {
|
||||||
|
// val format = SimpleDateFormat("YYYY-MM-dd")
|
||||||
|
// }
|
||||||
override fun visitField(
|
override fun visitField(
|
||||||
access: Int,
|
access: Int,
|
||||||
name: String?,
|
name: String?,
|
||||||
@@ -104,8 +171,8 @@ object BytecodeEditor {
|
|||||||
signature: String?,
|
signature: String?,
|
||||||
cst: Any?
|
cst: Any?
|
||||||
): FieldVisitor? {
|
): FieldVisitor? {
|
||||||
logger.trace { "CLass Field" to "${desc.replaceFormat()}: ${cst?.let { it::class.java.simpleName }}: $cst" }
|
logger.trace { "CLass Field" to "${desc.replaceIndirectly()}: ${cst?.let { it::class.java.simpleName }}: $cst" }
|
||||||
return super.visitField(access, name, desc.replaceFormat(), signature, cst)
|
return super.visitField(access, name, desc.replaceIndirectly(), signature, cst)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun visit(
|
override fun visit(
|
||||||
@@ -120,6 +187,12 @@ object BytecodeEditor {
|
|||||||
super.visit(version, access, name, signature, superName, interfaces)
|
super.visit(version, access, name, signature, superName, interfaces)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Modify method bytecode, for example
|
||||||
|
// class MangaYes {
|
||||||
|
// fun fetchChapterList() {
|
||||||
|
// SimpleDateFormat("YYYY-MM-dd")
|
||||||
|
// }
|
||||||
|
// }
|
||||||
override fun visitMethod(
|
override fun visitMethod(
|
||||||
access: Int,
|
access: Int,
|
||||||
name: String,
|
name: String,
|
||||||
@@ -127,9 +200,9 @@ object BytecodeEditor {
|
|||||||
signature: String?,
|
signature: String?,
|
||||||
exceptions: Array<String?>?
|
exceptions: Array<String?>?
|
||||||
): MethodVisitor {
|
): MethodVisitor {
|
||||||
logger.trace { "Processing method $name: ${desc.replaceFormat()}: $signature" }
|
logger.trace { "Processing method $name: ${desc.replaceIndirectly()}: $signature" }
|
||||||
val mv: MethodVisitor? = super.visitMethod(
|
val mv: MethodVisitor? = super.visitMethod(
|
||||||
access, name, desc.replaceFormat(), signature, exceptions
|
access, name, desc.replaceIndirectly(), signature, exceptions
|
||||||
)
|
)
|
||||||
return object : MethodVisitor(Opcodes.ASM5, mv) {
|
return object : MethodVisitor(Opcodes.ASM5, mv) {
|
||||||
override fun visitLdcInsn(cst: Any?) {
|
override fun visitLdcInsn(cst: Any?) {
|
||||||
@@ -137,16 +210,25 @@ object BytecodeEditor {
|
|||||||
super.visitLdcInsn(cst)
|
super.visitLdcInsn(cst)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Replace method type, for example
|
||||||
|
// val format = DateFormat()
|
||||||
|
// fun fetchChapterList() {
|
||||||
|
// if (format is SimpleDateFormat)
|
||||||
|
// }
|
||||||
override fun visitTypeInsn(opcode: Int, type: String?) {
|
override fun visitTypeInsn(opcode: Int, type: String?) {
|
||||||
logger.trace {
|
logger.trace {
|
||||||
"Type" to "$opcode: ${type.replaceFormatFully()}"
|
"Type" to "$opcode: ${type.replaceDirectly()}"
|
||||||
}
|
}
|
||||||
super.visitTypeInsn(
|
super.visitTypeInsn(
|
||||||
opcode,
|
opcode,
|
||||||
type.replaceFormatFully()
|
type.replaceDirectly()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Replace method field, for example
|
||||||
|
// fun fetchChapterList() {
|
||||||
|
// val format = SimpleDateFormat("YYYY-MM-dd")
|
||||||
|
// }
|
||||||
override fun visitMethodInsn(
|
override fun visitMethodInsn(
|
||||||
opcode: Int,
|
opcode: Int,
|
||||||
owner: String?,
|
owner: String?,
|
||||||
@@ -155,25 +237,30 @@ object BytecodeEditor {
|
|||||||
itf: Boolean
|
itf: Boolean
|
||||||
) {
|
) {
|
||||||
logger.trace {
|
logger.trace {
|
||||||
"Method" to "$opcode: ${owner.replaceFormatFully()}: $name: ${desc.replaceFormat()}"
|
"Method" to "$opcode: ${owner.replaceDirectly()}: $name: ${desc.replaceIndirectly()}"
|
||||||
}
|
}
|
||||||
super.visitMethodInsn(
|
super.visitMethodInsn(
|
||||||
opcode,
|
opcode,
|
||||||
owner.replaceFormatFully(),
|
owner.replaceDirectly(),
|
||||||
name,
|
name,
|
||||||
desc.replaceFormat(),
|
desc.replaceIndirectly(),
|
||||||
itf
|
itf
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Replace class field call from method, for example
|
||||||
|
// val format = SimpleDateFormat("YYYY-MM-dd")
|
||||||
|
// fun fetchChapterList() {
|
||||||
|
// format.format(Date())
|
||||||
|
// }
|
||||||
override fun visitFieldInsn(
|
override fun visitFieldInsn(
|
||||||
opcode: Int,
|
opcode: Int,
|
||||||
owner: String?,
|
owner: String?,
|
||||||
name: String?,
|
name: String?,
|
||||||
desc: String?
|
desc: String?
|
||||||
) {
|
) {
|
||||||
logger.trace { "Field" to "$opcode: $owner: $name: ${desc.replaceFormat()}" }
|
logger.trace { "Field" to "$opcode: $owner: $name: ${desc.replaceIndirectly()}" }
|
||||||
super.visitFieldInsn(opcode, owner, name, desc.replaceFormat())
|
super.visitFieldInsn(opcode, owner, name, desc.replaceIndirectly())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun visitInvokeDynamicInsn(
|
override fun visitInvokeDynamicInsn(
|
||||||
@@ -193,12 +280,20 @@ object BytecodeEditor {
|
|||||||
return cw.toByteArray()
|
return cw.toByteArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load non-class files from the jar, such as icons and the manifest
|
||||||
|
*
|
||||||
|
* @param [jarFile] The file to load resources from
|
||||||
|
*
|
||||||
|
* @return [Map] of resources
|
||||||
|
*/
|
||||||
private fun loadNonClasses(jarFile: File): Map<String, ByteArray> {
|
private fun loadNonClasses(jarFile: File): Map<String, ByteArray> {
|
||||||
val entries = mutableMapOf<String, ByteArray>()
|
val entries = mutableMapOf<String, ByteArray>()
|
||||||
ZipInputStream(jarFile.inputStream()).use { stream ->
|
ZipInputStream(jarFile.inputStream()).use { stream ->
|
||||||
var nextEntry: ZipEntry?
|
var nextEntry: ZipEntry?
|
||||||
while (stream.nextEntry.also { nextEntry = it } != null) {
|
while (stream.nextEntry.also { nextEntry = it } != null) {
|
||||||
nextEntry?.use(stream) { entry ->
|
nextEntry?.use(stream) { entry ->
|
||||||
|
// If it ends with class or is a directory ignore it
|
||||||
if (!entry.name.endsWith(".class") && !entry.isDirectory) {
|
if (!entry.name.endsWith(".class") && !entry.isDirectory) {
|
||||||
val bytes = stream.readBytes()
|
val bytes = stream.readBytes()
|
||||||
entries[entry.name] = bytes
|
entries[entry.name] = bytes
|
||||||
@@ -209,6 +304,12 @@ object BytecodeEditor {
|
|||||||
return entries
|
return entries
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save jar with modified content
|
||||||
|
*
|
||||||
|
* @param outBytes [Map] of names and [ByteArray]s of content to save inside the jar
|
||||||
|
* @param file JarFile to save to
|
||||||
|
*/
|
||||||
private fun saveAsJar(outBytes: Map<String, ByteArray>, file: File) {
|
private fun saveAsJar(outBytes: Map<String, ByteArray>, file: File) {
|
||||||
JarOutputStream(file.outputStream()).use { out ->
|
JarOutputStream(file.outputStream()).use { out ->
|
||||||
outBytes.forEach { (entry, value) ->
|
outBytes.forEach { (entry, value) ->
|
||||||
|
|||||||
Reference in New Issue
Block a user