Modify extension bytecode to fix SimpleDateFormat cannot parse errors

This commit is contained in:
Syer10
2021-06-26 12:50:04 -04:00
parent d06c3586fd
commit fc8bb10ca3
13 changed files with 1338 additions and 17 deletions
@@ -22,6 +22,7 @@ import org.kodein.di.conf.global
import org.kodein.di.instance
import org.w3c.dom.Element
import org.w3c.dom.Node
import suwayomi.tachidesk.manga.impl.util.BytecodeEditor
import suwayomi.tachidesk.server.ApplicationDirs
import xyz.nulldev.androidcompat.pm.InstalledPackage.Companion.toList
import xyz.nulldev.androidcompat.pm.toPackageInfo
@@ -80,6 +81,8 @@ object PackageTools {
""".trimIndent()
)
handler.dump(errorFile, emptyArray<String>())
} else {
BytecodeEditor.fixAndroidClasses(jarFilePath.toFile())
}
}
@@ -0,0 +1,218 @@
package suwayomi.tachidesk.manga.impl.util
import mu.KotlinLogging
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.ClassWriter
import org.objectweb.asm.FieldVisitor
import org.objectweb.asm.Handle
import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.Opcodes
import org.objectweb.asm.tree.ClassNode
import suwayomi.tachidesk.manga.impl.util.storage.use
import java.io.File
import java.io.IOException
import java.util.jar.JarEntry
import java.util.jar.JarFile
import java.util.jar.JarOutputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
object BytecodeEditor {
private val logger = KotlinLogging.logger {}
fun fixAndroidClasses(jarFile: File) {
val nodes = loadClasses(jarFile)
.mapValues { (className, classFileBuffer) ->
logger.trace { "Processing class $className" }
transform(classFileBuffer)
} + loadNonClasses(jarFile)
saveAsJar(nodes, jarFile)
}
private fun loadClasses(jar: File): Map<String, ByteArray> {
return JarFile(jar).use { jarFile ->
jarFile.entries()
.asSequence()
.mapNotNull {
readJar(jarFile, it)
}
.toMap()
}
}
private fun readJar(jar: JarFile, entry: JarEntry): Pair<String, ByteArray>? {
return try {
jar.getInputStream(entry).use { stream ->
if (entry.name.endsWith(".class")) {
val bytes = stream.readBytes()
if (bytes.size < 4) {
// Invalid class size
return@use null
}
val cafebabe = String.format(
"%02X%02X%02X%02X",
bytes[0],
bytes[1],
bytes[2],
bytes[3]
)
if (cafebabe.toLowerCase() != "cafebabe") {
// Corrupted class
return@use null
}
getNode(bytes).name to bytes
} else null
}
} catch (e: IOException) {
logger.error(e) { "Error loading jar file" }
null
}
}
private fun getNode(bytes: ByteArray): ClassNode {
val cr = ClassReader(bytes)
return ClassNode().also { cr.accept(it, ClassReader.EXPAND_FRAMES) }
}
private const val simpleDateFormat = "java/text/SimpleDateFormat"
private const val replacementSimpleDateFormat = "xyz/nulldev/androidcompat/replace/SimpleDateFormat"
private fun String?.replaceFormatFully() = if (this == simpleDateFormat) {
replacementSimpleDateFormat
} else this
private fun String?.replaceFormat() = this?.replace(simpleDateFormat, replacementSimpleDateFormat)
private fun transform(classfileBuffer: ByteArray): ByteArray {
val cr = ClassReader(classfileBuffer)
val cw = ClassWriter(cr, 0)
cr.accept(
object : ClassVisitor(Opcodes.ASM5, cw) {
override fun visitField(
access: Int,
name: String?,
desc: String?,
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)
}
override fun visit(
version: Int,
access: Int,
name: String?,
signature: String?,
superName: String?,
interfaces: Array<out String>?
) {
logger.trace { "Visiting $name: $signature: $superName" }
super.visit(version, access, name, signature, superName, interfaces)
}
override fun visitMethod(
access: Int,
name: String,
desc: String,
signature: String?,
exceptions: Array<String?>?
): MethodVisitor {
logger.trace { "Processing method $name: ${desc.replaceFormat()}: $signature" }
val mv: MethodVisitor? = super.visitMethod(
access, name, desc.replaceFormat(), signature, exceptions
)
return object : MethodVisitor(Opcodes.ASM5, mv) {
override fun visitLdcInsn(cst: Any?) {
logger.trace { "Ldc" to "${cst?.let { "${it::class.java.simpleName}: $it" }}" }
super.visitLdcInsn(cst)
}
override fun visitTypeInsn(opcode: Int, type: String?) {
logger.trace {
"Type" to "$opcode: ${type.replaceFormatFully()}"
}
super.visitTypeInsn(
opcode,
type.replaceFormatFully()
)
}
override fun visitMethodInsn(
opcode: Int,
owner: String?,
name: String?,
desc: String?,
itf: Boolean
) {
logger.trace {
"Method" to "$opcode: ${owner.replaceFormatFully()}: $name: ${desc.replaceFormat()}"
}
super.visitMethodInsn(
opcode,
owner.replaceFormatFully(),
name,
desc.replaceFormat(),
itf
)
}
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())
}
override fun visitInvokeDynamicInsn(
name: String?,
desc: String?,
bsm: Handle?,
vararg bsmArgs: Any?
) {
logger.trace { "InvokeDynamic" to "$name: $desc" }
super.visitInvokeDynamicInsn(name, desc, bsm, *bsmArgs)
}
}
}
},
0
)
return cw.toByteArray()
}
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 (!entry.name.endsWith(".class") && !entry.isDirectory) {
val bytes = stream.readBytes()
entries[entry.name] = bytes
}
}
}
}
return entries
}
private fun saveAsJar(outBytes: Map<String, ByteArray>, file: File) {
JarOutputStream(file.outputStream()).use { out ->
outBytes.forEach { (entry, value) ->
// Append extension to class entries
out.putNextEntry(
ZipEntry(
entry + if (entry.contains(".")) "" else ".class"
)
)
out.write(value)
out.closeEntry()
}
}
}
}
@@ -81,6 +81,8 @@ object PackageTools {
""".trimIndent()
)
handler.dump(errorFile, emptyArray<String>())
} else {
BytecodeEditor.fixAndroidClasses(jarFilePath.toFile())
}
}
@@ -0,0 +1,24 @@
package suwayomi.tachidesk.manga.impl.util.storage
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
fun ZipEntry.use(stream: ZipInputStream, block: (ZipEntry) -> Unit) {
var exception: Throwable? = null
try {
return block(this)
} catch (e: Throwable) {
exception = e
throw e
} finally {
if (exception == null) {
stream.closeEntry()
} else {
try {
stream.closeEntry()
} catch (closeException: Throwable) {
exception.addSuppressed(closeException)
}
}
}
}
@@ -31,6 +31,7 @@ import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
import suwayomi.tachidesk.manga.model.dataclass.ExtensionDataClass
import suwayomi.tachidesk.server.applicationSetup
import java.io.File
import java.util.concurrent.atomic.AtomicInteger
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class TestExtensions {
@@ -48,7 +49,7 @@ class TestExtensions {
@BeforeAll
fun setup() {
val dataRoot = File("tmp/TestDesk").absolutePath
System.setProperty("suwayomi.tachidesk.rootDir", dataRoot)
System.setProperty("suwayomi.tachidesk.server.rootDir", dataRoot)
applicationSetup()
setLoggingEnabled(false)
@@ -63,6 +64,7 @@ class TestExtensions {
updateExtension(it.pkgName)
}
else -> {
uninstallExtension(it.pkgName)
installExtension(it.pkgName)
}
}
@@ -77,10 +79,11 @@ class TestExtensions {
fun runTest() {
runBlocking(Dispatchers.Default) {
val semaphore = Semaphore(10)
sources.mapIndexed { index, source ->
val popularCount = AtomicInteger(1)
sources.map { source ->
async {
semaphore.withPermit {
logger.info { "$index - Now fetching popular manga from $source" }
logger.info { "${popularCount.getAndIncrement()} - Now fetching popular manga from $source" }
try {
mangaToFetch += source to (
source.fetchPopularManga(1)
@@ -102,10 +105,11 @@ class TestExtensions {
)
logger.info { "Now fetching manga info from ${mangaToFetch.size} sources" }
mangaToFetch.mapIndexed { index, (source, manga) ->
val mangaCount = AtomicInteger(1)
mangaToFetch.map { (source, manga) ->
async {
semaphore.withPermit {
logger.info { "$index - Now fetching manga from $source" }
logger.info { "${mangaCount.getAndIncrement()} - Now fetching manga from $source" }
try {
manga.copyFrom(source.fetchMangaDetails(manga).awaitSingleRepeat())
manga.initialized = true
@@ -127,10 +131,11 @@ class TestExtensions {
)
logger.info { "Now fetching manga chapters from ${mangaToFetch.size} sources" }
mangaToFetch.filter { it.second.initialized }.mapIndexed { index, (source, manga) ->
val chapterCount = AtomicInteger(1)
mangaToFetch.filter { it.second.initialized }.map { (source, manga) ->
async {
semaphore.withPermit {
logger.info { "$index - Now fetching manga chapters from $source" }
logger.info { "${chapterCount.getAndIncrement()} - Now fetching manga chapters from $source" }
try {
chaptersToFetch += Triple(
source,
@@ -160,10 +165,11 @@ class TestExtensions {
}
)
chaptersToFetch.mapIndexed { index, (source, manga, chapter) ->
val pageListCount = AtomicInteger(1)
chaptersToFetch.map { (source, manga, chapter) ->
async {
semaphore.withPermit {
logger.info { "$index - Now fetching page list from $source" }
logger.info { "${pageListCount.getAndIncrement()} - Now fetching page list from $source" }
try {
source.fetchPageList(chapter).awaitSingleRepeat()
} catch (e: Exception) {