Implement BytecodeFixer to address bytecode issues in JAR files and enhance Application context handling

This commit is contained in:
Kevin Rodrigues Borges
2026-06-01 01:25:27 +01:00
parent 203fa83e99
commit 6a9375164c
6 changed files with 247 additions and 4 deletions

View File

@@ -1,15 +1,16 @@
package android.app;
import android.content.ApplicationContext;
import androidx.preference.PreferenceScreen;
import android.content.ContextWrapper;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferencesImpl;
import androidx.preference.PreferenceScreen;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class Application {
public class Application extends ContextWrapper {
private static final Application INSTANCE = new Application();
@@ -18,20 +19,23 @@ public class Application {
private final ApplicationContext applicationContext;
private Application() {
super(null);
this.applicationContext = new ApplicationContext(this);
attachBaseContext(applicationContext);
}
public static Application getInstance() {
return INSTANCE;
}
@Override
public Context getApplicationContext() {
return applicationContext;
}
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
// Mode is ignored on modern Android; use name only to avoid collision bugs
return prefs.computeIfAbsent(name, k -> new SharedPreferencesImpl(k));
return prefs.computeIfAbsent(name, SharedPreferencesImpl::new);
}
public PreferenceScreen getPreferenceScreen(Long id) {
@@ -40,4 +44,7 @@ public class Application {
return new PreferenceScreen(applicationContext, pref);
});
}
// getSharedPreferences is overridden here to use Application-level caching.
// All other Context methods are inherited from ContextWrapper → ApplicationContext.
}

View File

@@ -0,0 +1,81 @@
package android.content;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.os.Looper;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
/**
* Stub ContextWrapper for desktop JVM.
*
* On real Android, ContextWrapper extends Context and delegates all
* method calls to a wrapped base Context. Application extends ContextWrapper.
* This stub provides minimal delegation so the bytecode verifier accepts
* Application-as-ContextWrapper casts.
*/
public class ContextWrapper extends Context {
private Context mBase;
public ContextWrapper() {
}
public ContextWrapper(Context base) {
this.mBase = base;
}
protected void attachBaseContext(Context base) {
if (mBase != null) {
throw new IllegalStateException("Base context already set");
}
mBase = base;
}
public Context getBaseContext() {
return mBase;
}
// ==================== Delegate to mBase ====================
@Override public AssetManager getAssets() { return mBase.getAssets(); }
@Override public Resources getResources() { return mBase.getResources(); }
@Override public PackageManager getPackageManager() { return mBase.getPackageManager(); }
@Override public ContentResolver getContentResolver() { return mBase.getContentResolver(); }
@Override public Looper getMainLooper() { return mBase.getMainLooper(); }
@Override public void setTheme(int resid) { mBase.setTheme(resid); }
@Override public Resources.Theme getTheme() { return mBase.getTheme(); }
@Override public ClassLoader getClassLoader() { return mBase.getClassLoader(); }
@Override public String getPackageName() { return mBase.getPackageName(); }
@Override public ApplicationInfo getApplicationInfo() { return mBase.getApplicationInfo(); }
@Override public String getPackageResourcePath() { return mBase.getPackageResourcePath(); }
@Override public String getPackageCodePath() { return mBase.getPackageCodePath(); }
@Override public SharedPreferences getSharedPreferences(String name, int mode) { return mBase.getSharedPreferences(name, mode); }
@Override public boolean moveSharedPreferencesFrom(Context sourceContext, String name) { return mBase.moveSharedPreferencesFrom(sourceContext, name); }
@Override public boolean deleteSharedPreferences(String name) { return mBase.deleteSharedPreferences(name); }
@Override public FileInputStream openFileInput(String name) throws FileNotFoundException { return mBase.openFileInput(name); }
@Override public FileOutputStream openFileOutput(String name, int mode) throws FileNotFoundException { return mBase.openFileOutput(name, mode); }
@Override public boolean deleteFile(String name) { return mBase.deleteFile(name); }
@Override public File getFileStreamPath(String name) { return mBase.getFileStreamPath(name); }
@Override public File getDataDir() { return mBase.getDataDir(); }
@Override public File getFilesDir() { return mBase.getFilesDir(); }
@Override public File getNoBackupFilesDir() { return mBase.getNoBackupFilesDir(); }
@Override public File getExternalFilesDir(String type) { return mBase.getExternalFilesDir(type); }
@Override public File[] getExternalFilesDirs(String type) { return mBase.getExternalFilesDirs(type); }
@Override public File getObbDir() { return mBase.getObbDir(); }
@Override public File[] getObbDirs() { return mBase.getObbDirs(); }
@Override public File getCacheDir() { return mBase.getCacheDir(); }
@Override public File getCodeCacheDir() { return mBase.getCodeCacheDir(); }
@Override public File getExternalCacheDir() { return mBase.getExternalCacheDir(); }
@Override public File[] getExternalCacheDirs() { return mBase.getExternalCacheDirs(); }
@Override public File[] getExternalMediaDirs() { return mBase.getExternalMediaDirs(); }
@Override public String[] fileList() { return mBase.fileList(); }
@Override public File getDir(String name, int mode) { return mBase.getDir(name, mode); }
@Override public Object getSystemService(String name) { return mBase.getSystemService(name); }
@Override public Context getApplicationContext() { return mBase.getApplicationContext(); }
}

View File

@@ -94,6 +94,10 @@
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy-agent</artifactId>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
</dependency>
<dependency>
<groupId>uy.kohesive.injekt</groupId>
<artifactId>injekt-core</artifactId>

View File

@@ -5,6 +5,7 @@ import androidx.preference.PreferenceScreen;
import dev.k3vinb5.unyo.core.exceptions.UnyoCoreException;
import dev.k3vinb5.unyo.core.loader.ExtensionLoader;
import dev.k3vinb5.unyo.core.loader.ExtensionPatches;
import dev.k3vinb5.unyo.core.loader.BytecodeFixer;
import dev.k3vinb5.unyo.core.loader.JarInspector;
import dev.k3vinb5.unyo.core.utils.ApkUtils;
import dev.k3vinb5.unyo.core.utils.MediaSourceUtils;
@@ -78,6 +79,7 @@ public class AniyomiBridge {
try {
assert jarFile != null;
animeExtensionJarPaths.put(packageName, jarFile.getPath());
BytecodeFixer.fixJar(jarFile);
String className = JarInspector.findMainClass(jarFile.getPath(), AnimeHttpSource.class);
if (className == null || className.isEmpty()) {
className = JarInspector.getClassName(jarFile.getPath(), extensionName);
@@ -139,6 +141,7 @@ public class AniyomiBridge {
try {
assert jarFile != null;
mangaExtensionJarPaths.put(packageName, jarFile.getPath());
BytecodeFixer.fixJar(jarFile);
String className = JarInspector.findMainClass(jarFile.getPath(), CatalogueSource.class);
if (className == null || className.isEmpty()) {
className = JarInspector.getClassName(jarFile.getPath(), extensionName);

View File

@@ -0,0 +1,142 @@
package dev.k3vinb5.unyo.core.loader;
import org.jboss.logging.Logger;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;
import java.io.*;
import java.util.jar.*;
public class BytecodeFixer {
private static final Logger LOGGER = Logger.getLogger(BytecodeFixer.class);
/**
* Post-processes a dex2jar-converted JAR to fix bytecode issues:
* 1. Upgrades class version from Java 6 (50) to Java 11 (55)
* 2. Computes StackMapTable frames (missing from dex2jar output)
*
* dex2jar does not generate StackMapTable attributes, which are required
* by the JVM verifier and JIT compiler for methods with branching.
* ASM's COMPUTE_FRAMES auto-generates them.
*/
public static void fixJar(File jarFile) {
File tempFile = new File(jarFile.getAbsolutePath() + ".fixing");
int fixedCount = 0;
int totalClasses = 0;
try (JarInputStream jis = new JarInputStream(new FileInputStream(jarFile));
JarOutputStream jos = new JarOutputStream(new FileOutputStream(tempFile))) {
JarEntry entry;
while ((entry = jis.getNextJarEntry()) != null) {
byte[] entryBytes = jis.readAllBytes();
String name = entry.getName();
if (name.endsWith(".class") && !name.equals("module-info.class")) {
totalClasses++;
try {
byte[] fixedBytes = fixClassBytes(entryBytes);
if (fixedBytes != entryBytes) {
fixedCount++;
}
entryBytes = fixedBytes;
} catch (Exception e) {
LOGGER.warnf("Failed to fix class %s: %s", name, e.getMessage());
// Keep original bytes
}
}
JarEntry newEntry = new JarEntry(name);
newEntry.setTime(entry.getTime());
jos.putNextEntry(newEntry);
jos.write(entryBytes);
jos.closeEntry();
}
} catch (IOException e) {
LOGGER.errorf(e, "Failed to fix JAR: %s", jarFile.getAbsolutePath());
tempFile.delete();
throw new RuntimeException("Failed to fix JAR bytecode: " + e.getMessage(), e);
}
// Replace original with fixed
if (!jarFile.delete()) {
tempFile.delete();
throw new RuntimeException("Failed to delete original JAR: " + jarFile.getAbsolutePath());
}
if (!tempFile.renameTo(jarFile)) {
throw new RuntimeException("Failed to rename fixed JAR to: " + jarFile.getAbsolutePath());
}
LOGGER.infof("BytecodeFixer: processed %d classes, fixed %d in %s", totalClasses, fixedCount, jarFile.getName());
}
static byte[] fixClassBytes(byte[] classBytes) {
int version = ((classBytes[6] & 0xFF) << 8 | (classBytes[7] & 0xFF));
boolean needsVersionUpgrade = version < 51; // 50 = Java 6, StackMapTable is optional
boolean needsFrames = hasBranches(classBytes);
if (!needsVersionUpgrade && !needsFrames) {
return classBytes; // Nothing to fix
}
ClassReader reader = new ClassReader(classBytes);
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES) {
@Override
protected String getCommonSuperClass(String type1, String type2) {
if (type1.equals(type2)) return type1;
try {
return super.getCommonSuperClass(type1, type2);
} catch (RuntimeException e) {
return "java/lang/Object";
}
}
};
final int targetVersion = Math.max(version, Opcodes.V11);
ClassVisitor classVisitor = new ClassVisitor(Opcodes.ASM9, writer) {
@Override
public void visit(int v, int access, String name, String signature,
String superName, String[] interfaces) {
super.visit(targetVersion, access, name, signature, superName, interfaces);
}
};
reader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
return writer.toByteArray();
}
/**
* Quick check whether a class has any branch instructions (ifeq, goto, etc.)
* that would require StackMapTable frames.
*/
private static boolean hasBranches(byte[] classBytes) {
final boolean[] found = {false};
ClassReader reader = new ClassReader(classBytes);
reader.accept(new ClassVisitor(Opcodes.ASM9) {
@Override
public org.objectweb.asm.MethodVisitor visitMethod(
int access, String name, String desc, String sig, String[] ex) {
return new org.objectweb.asm.MethodVisitor(Opcodes.ASM9) {
@Override
public void visitJumpInsn(int opcode, org.objectweb.asm.Label label) {
found[0] = true;
}
@Override
public void visitLookupSwitchInsn(
org.objectweb.asm.Label dflt, int[] keys, org.objectweb.asm.Label[] labels) {
found[0] = true;
}
@Override
public void visitTableSwitchInsn(
int min, int max, org.objectweb.asm.Label dflt, org.objectweb.asm.Label... labels) {
found[0] = true;
}
};
}
}, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
return found[0];
}
}

View File

@@ -36,6 +36,7 @@
<quickjs-kt-jvm.version>1.0.5</quickjs-kt-jvm.version>
<dex2jar.version>v59</dex2jar.version>
<injekt-core.version>1.16.1</injekt-core.version>
<asm.version>9.7</asm.version>
</properties>
<dependencyManagement>
@@ -143,6 +144,11 @@
<artifactId>injekt-core</artifactId>
<version>${injekt-core.version}</version>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>${asm.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-serialization-json-okio</artifactId>