/*
* Copyright (C) 2012 Sony Mobile Communications AB
*
* This file is part of ApkAnalyser.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package analyser.logic;
import gui.AppException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.jar.JarFile;
import java.util.zip.ZipFile;
import jerl.bcm.inj.Injection;
import jerl.bcm.inj.InjectionClass;
import jerl.bcm.inj.InjectionEngine;
import jerl.bcm.inj.InjectionMethod;
import jerl.bcm.util.ClassInfoLoader;
import jerl.bcm.util.ClassInjContainer;
import jerl.bcm.util.InjectionUtil;
import mereflect.JarClassContext;
import mereflect.MEClass;
import mereflect.MEClassContext;
import mereflect.MEMethod;
import util.JarFileModifier;
import util.StringReplacer;
import analyser.gui.ProgressReporter;
import analyser.gui.Settings;
import andreflect.ApkClassContext;
import andreflect.DexMethod;
import andreflect.injection.DalvikBytecodeModificationMediator;
import andreflect.injection.abs.DalvikInjectionMethod;
public class BytecodeModificationMediator {
protected static BytecodeModificationMediator m_inst = null;
protected Map<MEClassContext, Map<MEClass, ClassInjContainer>> m_bytecodeMods; // Map(Key:MEClassContext Value:Map(Key:MEClass Value:ClassInjContainer))
static final String MOD_LIST_FILENAME = "modlist.cfg";
static final String MANIFEST_FILENAME = "META-INF/MANIFEST.MF";
protected BytecodeModificationMediator() {
m_bytecodeMods = new TreeMap<MEClassContext, Map<MEClass, ClassInjContainer>>();
}
public static synchronized BytecodeModificationMediator getInstance() {
if (m_inst == null) {
m_inst = new BytecodeModificationMediator();
}
return m_inst;
}
public void unregisterModifications(MEClassContext ctx, MEClass clazz,
MEMethod method) {
Map<MEClass, ClassInjContainer> classMods = m_bytecodeMods.get(ctx);
if (classMods != null) {
ClassInjContainer injections = classMods.get(clazz);
if (injections != null) {
Enumeration<InjectionClass> e;
e = injections.classInjections();
while (e.hasMoreElements()) {
InjectionClass ic = e.nextElement();
injections.removeClassInjection(ic);
}
Enumeration<InjectionMethod> em;
em = injections.methodInjections();
while (em.hasMoreElements()) {
InjectionMethod im = em.nextElement();
injections.removeMethodInjection(im);
}
boolean removeContainer = injections.getNumClassInjections() == 0;
removeContainer &= injections.getNumClassInjections() == 0;
if (removeContainer) {
classMods.remove(clazz);
}
}
}
}
public void registerModification(MEClassContext ctx, MEClass clazz,
Injection inj, MEMethod method) {
if (method instanceof DexMethod
&& inj instanceof InjectionMethod) {
((DalvikInjectionMethod) inj).setMethod((DexMethod) method);
}
Map<MEClass, ClassInjContainer> classMods = m_bytecodeMods.get(ctx);
if (classMods == null) {
classMods = new HashMap<MEClass, ClassInjContainer>();
m_bytecodeMods.put(ctx, classMods);
}
ClassInjContainer injections = classMods.get(clazz);
if (injections == null) {
injections = new ClassInjContainer(clazz.getName());
classMods.put(clazz, injections);
}
if (inj instanceof InjectionMethod) {
injections.addMethodInjection((InjectionMethod) inj);
} else if (inj instanceof InjectionClass) {
injections.addClassInjection((InjectionClass) inj);
}
}
/**
* Returns list of modifications as <code>Map</code>, keyed
* <code>MEClass</code>/<code>ClassInjContainer</code>, or null if
* there are no modifications for specified context.
*
* @param ctx
* The context
* @return Map of modifications per class or null.
*/
public Map<MEClass, ClassInjContainer> getModifications(MEClassContext ctx) {
return m_bytecodeMods.get(ctx);
}
public File performRegisteredModifications(ProgressReporter pr,
MEClassContext ctx, final String midletNamePostfix) throws IOException,
Exception {
if (!(ctx instanceof JarClassContext)) {
if (ctx instanceof ApkClassContext) {
return DalvikBytecodeModificationMediator.performRegisteredModifications(this, pr, (ApkClassContext) ctx, midletNamePostfix);
} else {
throw new AppException("The context " + ctx.getContextDescription()
+ " is not a jarfile");
}
}
Map<MEClass, ClassInjContainer> classInjectionsInContext = m_bytecodeMods.get(ctx);
if (classInjectionsInContext != null) {
// perform bytecode modifications
final Map<String, byte[]> modifiedClasses = modifyClasses(pr, ctx,
classInjectionsInContext);
// Create new jarfile with modified classes
// create list with modified classes
final List<String> modifiedJarEntries = new ArrayList<String>();
Iterator<String> classI = modifiedClasses.keySet().iterator();
while (classI.hasNext()) {
String className = classI.next();
modifiedJarEntries.add(className.replace('.', '/') + ".class");
}
modifiedJarEntries.add(MANIFEST_FILENAME);
// create modification spec
final ByteArrayOutputStream modSpecOut = new ByteArrayOutputStream();
Iterator<MEClass> classIM;
classIM = classInjectionsInContext.keySet().iterator();
while (classIM.hasNext()) {
MEClass clazz = classIM.next();
ClassInjContainer injContainer = classInjectionsInContext.get(clazz);
InjectionUtil.deconstructClassToStream(injContainer,
new PrintStream(modSpecOut));
} // per class
// create list with new entries (only modify mod spec if first
// modification)
final JarFile jarFile = ((JarClassContext) ctx).getJar();
final List<String> newEntries = new ArrayList<String>();
if (jarFile.getEntry(MOD_LIST_FILENAME) == null) {
newEntries.add(MOD_LIST_FILENAME);
}
// write new jar file
JarFileModifier jfModder = new JarFileModifier(jarFile) {
@Override
public List<String> excludeEntries() {
return null;
}
@Override
public List<String> newEntries() {
return newEntries;
}
@Override
public InputStream getNewEntry(String entryName, boolean modified) {
if (entryName.equals(MOD_LIST_FILENAME)) {
return new ByteArrayInputStream(modSpecOut.toByteArray());
} else if (modifiedJarEntries.contains(entryName)) {
if (entryName.equalsIgnoreCase(MANIFEST_FILENAME)) {
try {
String midletName = jarFile.getManifest().getMainAttributes().getValue("MIDlet-Name");
String newMidletName = midletName + midletNamePostfix;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
jarFile.getManifest().write(baos);
String manifest = baos.toString();
String newManifest = StringReplacer.replaceAllStatements(manifest, midletName, newMidletName);
ByteArrayInputStream bais = new ByteArrayInputStream(newManifest.getBytes());
return bais;
} catch (IOException ignore) {
ignore.printStackTrace();
return null;
}
} else {
String className =
entryName.replace('/', '.').substring(0, entryName.length() - ".class".length());
return new ByteArrayInputStream(modifiedClasses.get(className));
}
} else {
return null;
}
}
};
File resultJar = jfModder.createModifiedJar();
return resultJar;
} else {
return null;
}
}
/**
* Performs the actual bytecode modification, returns a map keying classname
* as string to a byte array containing modified class data.
*
* @param ctx
* MEClassContext
* @param classInjection
* Map with key of MEClass, having value ClassInjContainer
* @throws IOException
*/
public Map<String, byte[]> modifyClasses(ProgressReporter pr, MEClassContext ctx,
Map<MEClass, ClassInjContainer> classInjections) throws IOException {
// the modified bytecode per class name
Map<String, byte[]> modClasses = new HashMap<String, byte[]>();
if (ctx instanceof JarClassContext) {
InjectionEngine injEngine = new InjectionEngine();
JarFile midletJar = ((JarClassContext) ctx).getJar();
ClassInfoLoader cr = new ClassInfoLoader(midletJar);
// Add classpath libraries for common superclass lookup
String[] classpaths = Settings.breakString(Settings.getClasspath(), ";");
for (int i = 0; i < classpaths.length; i++) {
if (classpaths[i].toLowerCase().endsWith(".jar")) {
File libFile = new File(classpaths[i]);
cr.addArchive(new ZipFile(libFile));
}
}
injEngine.setCommonSuperClassIF(cr);
Iterator<MEClass> classI = classInjections.keySet().iterator();
if (pr != null) {
pr.reportStart(classInjections.keySet().size());
}
// modify each class
int ci = 0;
while (classI.hasNext()) {
if (pr != null) {
pr.reportWork(ci++);
}
MEClass clazz = classI.next();
// get modifications for class
ClassInjContainer injContainer = classInjections.get(clazz);
// modify
String className = clazz.getName();
InputStream classIS = ctx.getClassResource(className).getInputStream();
byte[] classData;
try {
classData = injEngine.preformInjection(classIS, injContainer.methodInjectionsToArray());
} finally {
if (classIS != null) {
classIS.close();
}
}
// and store it
modClasses.put(className, classData);
} // per class
if (pr != null) {
pr.reportEnd();
}
}
return modClasses;
}
public Set<MEClassContext> getModifiedContexts() {
return m_bytecodeMods.keySet();
}
public void unregisterAllModifications() {
m_bytecodeMods.clear();
}
}