From b327df732cc8a777c09e831fe448b6ba1842d08d Mon Sep 17 00:00:00 2001 From: Syer10 Date: Thu, 29 Jul 2021 21:26:13 -0400 Subject: [PATCH] Comments, comments, and comments!! And future proofing (#162) --- .../manga/impl/util/BytecodeEditor.kt | 135 +++++++++++++++--- 1 file changed, 118 insertions(+), 17 deletions(-) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/util/BytecodeEditor.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/util/BytecodeEditor.kt index 475f1a39..384b7000 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/util/BytecodeEditor.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/util/BytecodeEditor.kt @@ -27,6 +27,12 @@ import java.util.zip.ZipInputStream object BytecodeEditor { 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) { val nodes = loadClasses(jarFile) .mapValues { (className, classFileBuffer) -> @@ -37,6 +43,13 @@ object BytecodeEditor { 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 { return JarFile(jar).use { jarFile -> 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? { return try { jar.getInputStream(entry).use { stream -> @@ -83,20 +104,66 @@ object BytecodeEditor { 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 simpleDateFormat = "java/text/SimpleDateFormat" - private const val replacementSimpleDateFormat = "$replacementPath/$simpleDateFormat" - private fun String?.replaceFormatFully() = if (this == simpleDateFormat) { - replacementSimpleDateFormat - } else this - private fun String?.replaceFormat() = this?.replace(simpleDateFormat, replacementSimpleDateFormat) + /** + * List of classes that will be replaced + */ + 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 { + // Read the class and prepare to modify it val cr = ClassReader(classfileBuffer) val cw = ClassWriter(cr, 0) + // Modify the class cr.accept( object : ClassVisitor(Opcodes.ASM5, cw) { + // Modify field descriptor, for example + // class MangaYes { + // val format = SimpleDateFormat("YYYY-MM-dd") + // } override fun visitField( access: Int, name: String?, @@ -104,8 +171,8 @@ object BytecodeEditor { signature: String?, cst: Any? ): FieldVisitor? { - logger.trace { "CLass Field" to "${desc.replaceFormat()}: ${cst?.let { it::class.java.simpleName }}: $cst" } - return super.visitField(access, name, desc.replaceFormat(), signature, cst) + logger.trace { "CLass Field" to "${desc.replaceIndirectly()}: ${cst?.let { it::class.java.simpleName }}: $cst" } + return super.visitField(access, name, desc.replaceIndirectly(), signature, cst) } override fun visit( @@ -120,6 +187,12 @@ object BytecodeEditor { super.visit(version, access, name, signature, superName, interfaces) } + // Modify method bytecode, for example + // class MangaYes { + // fun fetchChapterList() { + // SimpleDateFormat("YYYY-MM-dd") + // } + // } override fun visitMethod( access: Int, name: String, @@ -127,9 +200,9 @@ object BytecodeEditor { signature: String?, exceptions: Array? ): MethodVisitor { - logger.trace { "Processing method $name: ${desc.replaceFormat()}: $signature" } + logger.trace { "Processing method $name: ${desc.replaceIndirectly()}: $signature" } val mv: MethodVisitor? = super.visitMethod( - access, name, desc.replaceFormat(), signature, exceptions + access, name, desc.replaceIndirectly(), signature, exceptions ) return object : MethodVisitor(Opcodes.ASM5, mv) { override fun visitLdcInsn(cst: Any?) { @@ -137,16 +210,25 @@ object BytecodeEditor { super.visitLdcInsn(cst) } + // Replace method type, for example + // val format = DateFormat() + // fun fetchChapterList() { + // if (format is SimpleDateFormat) + // } override fun visitTypeInsn(opcode: Int, type: String?) { logger.trace { - "Type" to "$opcode: ${type.replaceFormatFully()}" + "Type" to "$opcode: ${type.replaceDirectly()}" } super.visitTypeInsn( opcode, - type.replaceFormatFully() + type.replaceDirectly() ) } + // Replace method field, for example + // fun fetchChapterList() { + // val format = SimpleDateFormat("YYYY-MM-dd") + // } override fun visitMethodInsn( opcode: Int, owner: String?, @@ -155,25 +237,30 @@ object BytecodeEditor { itf: Boolean ) { logger.trace { - "Method" to "$opcode: ${owner.replaceFormatFully()}: $name: ${desc.replaceFormat()}" + "Method" to "$opcode: ${owner.replaceDirectly()}: $name: ${desc.replaceIndirectly()}" } super.visitMethodInsn( opcode, - owner.replaceFormatFully(), + owner.replaceDirectly(), name, - desc.replaceFormat(), + desc.replaceIndirectly(), itf ) } + // Replace class field call from method, for example + // val format = SimpleDateFormat("YYYY-MM-dd") + // fun fetchChapterList() { + // format.format(Date()) + // } override fun visitFieldInsn( opcode: Int, owner: String?, name: String?, desc: String? ) { - logger.trace { "Field" to "$opcode: $owner: $name: ${desc.replaceFormat()}" } - super.visitFieldInsn(opcode, owner, name, desc.replaceFormat()) + logger.trace { "Field" to "$opcode: $owner: $name: ${desc.replaceIndirectly()}" } + super.visitFieldInsn(opcode, owner, name, desc.replaceIndirectly()) } override fun visitInvokeDynamicInsn( @@ -193,12 +280,20 @@ object BytecodeEditor { 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 { val entries = mutableMapOf() ZipInputStream(jarFile.inputStream()).use { stream -> var nextEntry: ZipEntry? while (stream.nextEntry.also { nextEntry = it } != null) { nextEntry?.use(stream) { entry -> + // If it ends with class or is a directory ignore it if (!entry.name.endsWith(".class") && !entry.isDirectory) { val bytes = stream.readBytes() entries[entry.name] = bytes @@ -209,6 +304,12 @@ object BytecodeEditor { 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, file: File) { JarOutputStream(file.outputStream()).use { out -> outBytes.forEach { (entry, value) ->