Comments, comments, and comments!! And future proofing (#162)

This commit is contained in:
Syer10
2021-07-29 21:26:13 -04:00
committed by GitHub
parent 21d7cf5d6a
commit b327df732c

View File

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