Comments, comments, and comments!! And future proofing (#162)
This commit is contained in:
@@ -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) ->
|
||||
|
||||
Reference in New Issue
Block a user