Update BytecodeEditor to use Java NIO Paths (#200)

This commit is contained in:
Mitchell Syer
2021-09-18 13:27:15 -04:00
committed by GitHub
parent 2c5114c770
commit 77e057f244
3 changed files with 44 additions and 115 deletions

View File

@@ -82,7 +82,7 @@ object PackageTools {
) )
handler.dump(errorFile, emptyArray<String>()) handler.dump(errorFile, emptyArray<String>())
} else { } else {
BytecodeEditor.fixAndroidClasses(jarFilePath.toFile()) BytecodeEditor.fixAndroidClasses(jarFilePath)
} }
} }

View File

@@ -15,15 +15,10 @@ import org.objectweb.asm.FieldVisitor
import org.objectweb.asm.Handle import org.objectweb.asm.Handle
import org.objectweb.asm.MethodVisitor import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.Opcodes import org.objectweb.asm.Opcodes
import org.objectweb.asm.tree.ClassNode import java.nio.file.FileSystems
import suwayomi.tachidesk.manga.impl.util.storage.use import java.nio.file.Files
import java.io.File import java.nio.file.Path
import java.io.IOException import kotlin.streams.asSequence
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 { object BytecodeEditor {
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
@@ -33,50 +28,31 @@ object BytecodeEditor {
* *
* @param jarFile The JarFile to replace class references in * @param jarFile The JarFile to replace class references in
*/ */
fun fixAndroidClasses(jarFile: File) { fun fixAndroidClasses(jarFile: Path) {
val nodes = loadClasses(jarFile) FileSystems.newFileSystem(jarFile, null as ClassLoader?)?.use {
.mapValues { (className, classFileBuffer) -> Files.walk(it.getPath("/")).asSequence()
logger.trace { "Processing class $className" } .filterNotNull()
transform(classFileBuffer) .filterNot(Files::isDirectory)
} + loadNonClasses(jarFile) .mapNotNull(::getClassBytes)
.map(::transform)
saveAsJar(nodes, jarFile) .forEach(::write)
}
/**
* 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()
.asSequence()
.mapNotNull {
readJar(jarFile, it)
}
.toMap()
} }
} }
/** /**
* Get class file in [jar] for [entry] * Get class bytes from a [Path]
* *
* @param jar The jar to get the class from * @param path The path entry to get the class bytes 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 * @return [Pair] of the [Path] 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 getClassBytes(path: Path): Pair<Path, ByteArray>? {
return try { return try {
jar.getInputStream(entry).use { stream -> if (path.toString().endsWith(".class")) {
if (entry.name.endsWith(".class")) { val bytes = Files.readAllBytes(path)
val bytes = stream.readBytes()
if (bytes.size < 4) { if (bytes.size < 4) {
// Invalid class size // Invalid class size
return@use null return null
} }
val cafebabe = String.format( val cafebabe = String.format(
"%02X%02X%02X%02X", "%02X%02X%02X%02X",
@@ -87,23 +63,17 @@ object BytecodeEditor {
) )
if (cafebabe.lowercase() != "cafebabe") { if (cafebabe.lowercase() != "cafebabe") {
// Corrupted class // Corrupted class
return@use null return null
} }
getNode(bytes).name to bytes path to bytes
} else null } else null
} } catch (e: Exception) {
} catch (e: IOException) { logger.error(e) { "Error loading class from Path: $path" }
logger.error(e) { "Error loading jar file" }
null null
} }
} }
private fun getNode(bytes: ByteArray): ClassNode {
val cr = ClassReader(bytes)
return ClassNode().also { cr.accept(it, ClassReader.EXPAND_FRAMES) }
}
/** /**
* The path where replacement classes will reside * The path where replacement classes will reside
*/ */
@@ -153,9 +123,9 @@ object BytecodeEditor {
* *
* @return [ByteArray] with modified bytecode * @return [ByteArray] with modified bytecode
*/ */
private fun transform(classfileBuffer: ByteArray): ByteArray { private fun transform(pair: Pair<Path, ByteArray>): Pair<Path, ByteArray> {
// Read the class and prepare to modify it // Read the class and prepare to modify it
val cr = ClassReader(classfileBuffer) val cr = ClassReader(pair.second)
val cw = ClassWriter(cr, 0) val cw = ClassWriter(cr, 0)
// Modify the class // Modify the class
cr.accept( cr.accept(
@@ -277,51 +247,10 @@ object BytecodeEditor {
}, },
0 0
) )
return cw.toByteArray() return pair.first to cw.toByteArray()
} }
/** private fun write(pair: Pair<Path, ByteArray>) {
* Load non-class files from the jar, such as icons and the manifest Files.write(pair.first, pair.second)
*
* @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
}
}
}
}
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) ->
// Append extension to class entries
out.putNextEntry(
ZipEntry(
entry + if (entry.contains(".")) "" else ".class"
)
)
out.write(value)
out.closeEntry()
}
}
} }
} }

View File

@@ -82,7 +82,7 @@ object PackageTools {
) )
handler.dump(errorFile, emptyArray<String>()) handler.dump(errorFile, emptyArray<String>())
} else { } else {
BytecodeEditor.fixAndroidClasses(jarFilePath.toFile()) BytecodeEditor.fixAndroidClasses(jarFilePath)
} }
} }