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
@@ -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<String, ByteArray> {
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<String, ByteArray>? {
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<String?>?
): 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<String, ByteArray> {
val entries = mutableMapOf<String, ByteArray>()
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<String, ByteArray>, file: File) {
JarOutputStream(file.outputStream()).use { out ->
outBytes.forEach { (entry, value) ->