/* * Copyright (C) 2012 The Android Open Source Project * * 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 com.motorolamobility.preflighting.core.internal.utils; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Scanner; import java.util.Set; import java.util.StringTokenizer; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import org.eclipse.core.runtime.FileLocator; import org.eclipse.core.runtime.Platform; import org.eclipse.jdt.core.dom.PrimitiveType; import org.eclipse.osgi.util.NLS; import org.osgi.framework.Bundle; import com.motorolamobility.preflighting.core.PreflightingCorePlugin; import com.motorolamobility.preflighting.core.applicationdata.Element; import com.motorolamobility.preflighting.core.applicationdata.SourceFolderElement; import com.motorolamobility.preflighting.core.exception.PreflightingToolException; import com.motorolamobility.preflighting.core.i18n.PreflightingCoreNLS; import com.motorolamobility.preflighting.core.logging.PreflightingLogger; import com.motorolamobility.preflighting.core.source.model.Constant; import com.motorolamobility.preflighting.core.source.model.Field; import com.motorolamobility.preflighting.core.source.model.Instruction; import com.motorolamobility.preflighting.core.source.model.Invoke; import com.motorolamobility.preflighting.core.source.model.Method; import com.motorolamobility.preflighting.core.source.model.SourceFileElement; import com.motorolamobility.preflighting.core.source.model.Variable; /** * Extracts smali representation for the files inside an apk */ public final class ApktoolUtils { /** * */ private static final String PARAM_SEPARATOR = ";"; private static final String INNER_CLASS = "InnerClass"; private static final String SEMICOLON = PARAM_SEPARATOR; private static final String RPAREN = ")"; private static final String LPAREN = "("; private static final String COLON = ":"; private static final String TYPE_PREFIX_L = "L"; private static final String ARRAYTYPE_PREFIX = "["; private static final String PARAMETERS_SEPARATOR = SEMICOLON; private static final String CONST = "const"; private static final String METHODITEMS_SEPARATOR2 = "->"; private static final String METHODITEMS_SEPARATOR1 = ";->"; private static final String INVOKE = "invoke"; private static final String END_METHOD = ".end method"; private static final String CONSTRUCTOR = "constructor"; private static final String ANNOTATION = ".annotation"; private static final String METHOD = ".method"; private static final String ASSIGNMENT_OPERATOR = "="; private static final String FINAL = "final"; private static final String STATIC = "static"; private static final String FIELD = ".field"; private static final String SOURCE = ".source"; private static final String SUPER = ".super"; private static final String CLASS = ".class"; private static final String LINE = ".line"; private static final String VIRTUAL_METHODS = "# virtual methods"; private static final String DIRECT_METHODS = "# direct methods"; private static final String INSTANCE_FIELDS = "# instance fields"; private static final String STATIC_FIELDS = "# static fields"; private static final String SMALI = ".smali"; private static final String APK = ".apk"; private static final String APKTOOL_JAR_PATH = "/apktool/apktool.jar"; private static Map<String, String> smaliTypeMap = new HashMap<String, String>(9); static { smaliTypeMap.put("V", PrimitiveType.VOID.toString()); smaliTypeMap.put("Z", PrimitiveType.BOOLEAN.toString()); smaliTypeMap.put("B", PrimitiveType.BYTE.toString()); smaliTypeMap.put("S", PrimitiveType.SHORT.toString()); smaliTypeMap.put("C", PrimitiveType.CHAR.toString()); smaliTypeMap.put("I", PrimitiveType.INT.toString()); smaliTypeMap.put("J", PrimitiveType.LONG.toString()); smaliTypeMap.put("F", PrimitiveType.FLOAT.toString()); smaliTypeMap.put("D", PrimitiveType.DOUBLE.toString()); } /** * Extracts smali representation for the files inside an apk * @param apkFolder * @param appName apk filename * @return * @throws InterruptedException * @throws IOException * @throws AndrolibException */ public static SourceFolderElement extractJavaModel(File apkFolder, Element parent, File smaliDir) throws IOException, InterruptedException { SourceFolderElement model = new SourceFolderElement(apkFolder, parent, true); Set<File> smaliFiles = visitFolderToIdentifySmalis(smaliDir); for (File smaliFile : smaliFiles) { SourceFileElement m = readFromSmali(smaliFile, model); model.getSourceFileElements().add(m); } return model; } /** * Extract information from methods, fields, constants, invocations, etc * from a smali file * @param smali file to read * @param parent * @return smali model * @throws FileNotFoundException */ private static SourceFileElement readFromSmali(File smali, Element parent) throws FileNotFoundException { SourceFileElement model = new SourceFileElement(smali, parent); FileInputStream fileInputStream = new FileInputStream(smali); Scanner scanner = new Scanner(fileInputStream); try { boolean readingStaticFields = false; boolean readingInstanceFields = false; boolean readingDirectMethods = false; boolean readingVirtualMethods = false; int lineInfo = -1; while (scanner.hasNextLine()) { String line = scanner.nextLine(); if ((line != null) && !line.equals("")) { if (line.trim().startsWith(STATIC_FIELDS)) { readingStaticFields = true; readingInstanceFields = readingDirectMethods = readingVirtualMethods = false; } else if (line.trim().startsWith(INSTANCE_FIELDS)) { readingInstanceFields = true; readingStaticFields = readingDirectMethods = readingVirtualMethods = false; } else if (line.trim().startsWith(DIRECT_METHODS)) { readingDirectMethods = true; readingStaticFields = readingInstanceFields = readingVirtualMethods = false; } else if (line.trim().startsWith(VIRTUAL_METHODS)) { readingVirtualMethods = true; readingStaticFields = readingInstanceFields = readingDirectMethods = false; } else if (line.trim().startsWith(LINE)) { StringTokenizer token = new StringTokenizer(line); if (token.hasMoreTokens()) { token.nextToken(); } if (token.hasMoreTokens()) { try { lineInfo = Integer.parseInt(token.nextToken()); } catch (NumberFormatException e) { PreflightingLogger.error("Could not get line Number for line " + line); } } } else if (line.trim().startsWith(CLASS)) { String[] tokens = line.trim().split(" "); for (int i = 0; i < tokens.length; i++) { if ((tokens[i] != null) && tokens[i].startsWith(TYPE_PREFIX_L)) { model.setClassFullPath(getType(tokens[i])); break; } } } else if (line.trim().startsWith(SUPER)) { String[] tokens = line.trim().split(" "); if (tokens.length == 2) { model.setSuperclassName(tokens[1].substring(1)); //remove first L } } else if (line.trim().startsWith(SOURCE)) { String[] tokens = line.trim().split(" "); if (tokens.length == 2) { String sourceName = tokens[1]; sourceName = sourceName.substring(1, sourceName.length() - 1); //Remove quotes model.setSourceName(sourceName); String pkg = getPkg(model.getClassFullPath()); model.setFile(new File(pkg.replace('.', '/') + "/" + sourceName)); //Smali file makes no sense for checkers because it's temporary. } } else if (line.trim().startsWith(FIELD)) { Field field = new Field(); if (line.contains(STATIC)) { field.setStatic(true); } if (line.contains(FINAL)) { field.setFinal(true); } String[] tokens = line.trim().split(" "); String visibility = tokens[1]; field.setVisibility(visibility); for (int i = 0; i < tokens.length; i++) { //find attribute name and type (separated by colon) if (tokens[i].trim().contains(COLON)) { String[] aux = tokens[i].trim().split(COLON); String name = model.getName(); if (name.contains("$")) { name = name.substring(name.indexOf("$") + 1, name.lastIndexOf(".")); field.setName(name + "." + aux[0]); } if (aux.length > 1) { String typeKey = aux[1]; String type = ""; type = getType(typeKey); field.setType(type); } break; } } if (ASSIGNMENT_OPERATOR.equals(tokens[tokens.length - 2])) { //there is assignment = <value> field.setValue(tokens[tokens.length - 1]); } if (readingInstanceFields) { model.getInstanceFields().add(field); } else if (readingStaticFields) { model.getStaticFields().add(field); } } else if (line.trim().startsWith(METHOD)) { //Map representing the Smali variable index(vX) -> variable name Map<String, String> varMap = new HashMap<String, String>(); Method method = new Method(); if (line.contains(STATIC)) { method.setStatic(true); } if (line.contains(CONSTRUCTOR)) { method.setConstructor(true); } String[] tokens = line.trim().split(" "); for (int i = 0; i < tokens.length; i++) { if (tokens[i].trim().contains(LPAREN)) { int lP = tokens[i].trim().indexOf(LPAREN); if (lP >= 0) { String temp = tokens[i].trim().substring(0, lP); method.setMethodName(temp); int rP = tokens[i].trim().indexOf(RPAREN); String params = null; if (rP >= 0) { params = tokens[i].trim().substring(lP + 1, rP); if (!params.equals("")) { String[] methodParams = params.split(SEMICOLON); for (String param : methodParams) { //Get all params (it gets only one in cases like IIL<type>; that results integer, integer, type, integer List<String> paramTypes = getParamTypes(param + PARAMETERS_SEPARATOR); method.getParameterTypes().addAll(paramTypes); } } String returnType = tokens[i].trim().substring(rP + 1); method.setReturnType(getType(returnType)); } break; } } } line = scanner.nextLine(); Invoke invoke = null; while (!line.trim().startsWith(END_METHOD)) { //read instructions if (line.trim().startsWith(INVOKE)) { invoke = new Invoke(); if (line.contains("-" + Method.DIRECT)) { invoke.setType(Method.DIRECT); } else if (line.contains("-" + Method.VIRTUAL)) { invoke.setType(Method.VIRTUAL); } int objectIdxStart = line.indexOf('{') + 1; int objectIdxEnd = line.indexOf('}'); String paramsIds = line.substring(objectIdxStart, objectIdxEnd); String objIdx = null; List<String> paramNames = new ArrayList<String>(); if (paramsIds.contains(",")) { String[] paramsSplit = paramsIds.split(", "); // the returned object is always the first item objIdx = paramsSplit[0]; // the other items are parameters for (int paramsSplitIndex = 1; paramsSplitIndex < paramsSplit.length; paramsSplitIndex++) { if (varMap.containsKey(paramsSplit[paramsSplitIndex])) { paramNames.add(varMap .get(paramsSplit[paramsSplitIndex])); } else { paramNames.add(paramsSplit[paramsSplitIndex]); } } } else { // doesn't have parameters objIdx = !paramsIds.equals("p0") ? paramsIds : null; } String objectName = null; if (varMap.containsKey(objIdx)) { objectName = varMap.get(objIdx); } else { objectName = objIdx; } invoke.setObjectName(objectName); invoke.setParameterNames(paramNames); int commaInd = objectIdxEnd; String callmethod; if (commaInd >= 0) { callmethod = line.substring(commaInd + 2, line.length()).trim(); String[] methodItems = callmethod.split(METHODITEMS_SEPARATOR1); invoke.setClassCalled(getType(methodItems[0])); if (methodItems.length <= 1) { //try to use -> only methodItems = callmethod.split(METHODITEMS_SEPARATOR2); } extractInvoke(invoke, methodItems); invoke.setLine(lineInfo); invoke.setSourceFileFullPath(model.getSourceFileFullPath()); } method.getInstructions().add(invoke); } else if (line.trim().startsWith(CONST)) { String[] split = line.trim().split(" "); Constant c = new Constant(); c.setType(split[0]); if (split.length > 2) { c.setValue(split[2]); } method.getInstructions().add(c); c.setSourceFileFullPath(model.getSourceFileFullPath()); } else if (line.trim().startsWith(LINE)) { StringTokenizer token = new StringTokenizer(line); if (token.hasMoreTokens()) { token.nextToken(); } if (token.hasMoreTokens()) { try { lineInfo = Integer.parseInt(token.nextToken()); } catch (NumberFormatException e) { PreflightingLogger .error("Could not get line Number for line " + line); } } } else if (line.trim().startsWith(".local ")) //Variable declaration { String[] split = line.trim().split(", "); String varMapIdx = split[0]; varMapIdx = varMapIdx.replace(".local ", ""); String[] varNameType = split[1].split(COLON); String varName = varNameType[0].trim(); String varType = varNameType[1]; varType = getType(varType); Variable variable = new Variable(); variable.setName(varName); variable.setType(varType); variable.setLineNumber(lineInfo); varMap.put(varMapIdx, varName); method.addVariable(variable); List<Instruction> instructions = method.getInstructions(); for (Instruction instruction : instructions) { if (instruction instanceof Invoke) { Invoke inv = (Invoke) instruction; String invokeReturnVar = inv.getAssignedVariable(); if ((invokeReturnVar != null) && invokeReturnVar.equals(varMapIdx)) { inv.setAssignedVariable(varName); } String objectName = inv.getObjectName(); if ((objectName != null) && objectName.equals(varMapIdx)) { inv.setObjectName(varName); } } } } else if (line.trim().startsWith("move-result-object")) { String resultVarIdx = line.trim().substring(line.trim().lastIndexOf(" ") + 1); String varName = varMap.containsKey(resultVarIdx) ? varMap.get(resultVarIdx) : resultVarIdx; if (invoke != null) { invoke.setAssignedVariable(varName); } } line = scanner.nextLine(); } if (readingDirectMethods) { model.getDirectMethods().add(method); } else if (readingVirtualMethods) { model.getVirtualMethods().add(method); } } // TODO : Here one must fetch the correct inner class name else if (line.startsWith(ANNOTATION) && line.contains(INNER_CLASS)) { model.setInnerClass(true); } } } } finally { if (fileInputStream != null) { try { fileInputStream.close(); } catch (IOException e) { //DO nothing. } } scanner.close(); } return model; } private static String getPkg(String classFullPath) { int dotLastIndex = classFullPath.lastIndexOf('.'); String pkg = ""; if (dotLastIndex > 0) { pkg = classFullPath.substring(0, dotLastIndex); } return pkg; } /** * @param params list of parameters * @return list of param types */ private static List<String> getParamTypes(String params) { StringBuilder replacementStr = new StringBuilder(); List<String> paramTypes = new ArrayList<String>(); for (int i = 0; i < params.length(); i++) { String c = "" + params.charAt(i); if (c.equals(ARRAYTYPE_PREFIX)) { replacementStr.append(c); } else if (c.equals(TYPE_PREFIX_L)) { replacementStr.append(c); do { i++; c = "" + params.charAt(i); replacementStr.append(c); } while (!c.equals(PARAM_SEPARATOR)); replacementStr.append(PARAM_SEPARATOR); } else if (smaliTypeMap.containsKey(c)) { //it is a simple type replacementStr.append(c + PARAM_SEPARATOR); } //PARAM_SEPARATOR chars are not expected to be logged else if (!c.equals(PARAM_SEPARATOR)) { PreflightingLogger .error("Chars not recognized. " + c + ". Check params: " + params); } } StringTokenizer tokenizer = new StringTokenizer(replacementStr.toString(), PARAM_SEPARATOR); while (tokenizer.hasMoreTokens()) { String type = tokenizer.nextToken(); paramTypes.add(getType(type)); } return paramTypes; } /** * @param typeKey * @return if it is an array appends to the type [], otherwise returns the type given for the source code */ private static String getType(String typeKey) { String type; boolean isArray = false; if (typeKey.startsWith(ARRAYTYPE_PREFIX)) { isArray = true; typeKey = typeKey.substring(1); } if (typeKey.startsWith(TYPE_PREFIX_L)) { type = typeKey.replaceAll("/", "."); type = type.replaceAll(PARAM_SEPARATOR, ""); type = type.contains("$") ? type.substring(1, type.lastIndexOf('$')) : type .substring(1); } else { type = smaliTypeMap.get(typeKey); } if (isArray) { type += "[]"; } if (type == null) { PreflightingLogger.error("Type not recognized. Check statement: " + typeKey); } return type; } /** * Extract information about invocation (the part after -> or ;->) * @param invoke * @param methodItems */ private static void extractInvoke(Invoke invoke, String[] methodItems) { int lparenInd = methodItems[1].indexOf(LPAREN); int rparenInd = methodItems[1].indexOf(RPAREN); String metName = methodItems[1].substring(0, lparenInd); invoke.setMethodName(metName); String param = methodItems[1].substring(lparenInd + 1, rparenInd); if (!param.equals("")) { String[] params = param.split(PARAMETERS_SEPARATOR); invoke.setParameterTypes(Arrays.asList(params)); } String retType = methodItems[1].substring(rparenInd + 1); invoke.setReturnType(getType(retType)); } /** * Runs apktool to extract java and resource files * @param apk * @param outputDir * @throws PreflightingToolException */ public static void extractFilesFromApk(File apk, File tempProjectFolder) throws PreflightingToolException { Process proc = null; try { Bundle bundle = PreflightingCorePlugin.getContext().getBundle(); URL apktoolURL = bundle.getEntry(APKTOOL_JAR_PATH); apktoolURL = FileLocator.toFileURL(apktoolURL); String path = apktoolURL.getPath(); if (Platform.getOS().equals(Platform.OS_WIN32) && path.startsWith("/")) { path = path.substring(1); } String[] args = new String[] { "java", "-Xmx512m", "-jar", path, "d", "-f", apk.getAbsolutePath(), tempProjectFolder.getAbsolutePath() }; proc = Runtime.getRuntime().exec(args); } catch (Exception e) { PreflightingLogger.error(ProjectUtils.class, PreflightingCoreNLS.ProjectUtils_ErrorExecutingApkTool, e); //$NON-NLS-1$ throw new PreflightingToolException( PreflightingCoreNLS.ProjectUtils_ErrorExecutingApkTool); } try { //verify if APK is not empty (otherwise command will not return) if (isApkValid(apk)) { //trying to extract jar - if fail, jar is probably corrupted File aux = File.createTempFile("apktemp", "folder"); aux.getParentFile().mkdirs(); File temp = new File(aux.getParentFile(), "apks"); temp.mkdir(); boolean fileOk = unpackZipFile(apk, temp.getAbsolutePath()); if (fileOk && (proc != null) && (proc.waitFor() != 0)) { throw new PreflightingToolException( PreflightingCoreNLS.ProjectUtils_ErrorExecutingApkTool); } else if (!fileOk) { //apk corrupted throw new PreflightingToolException(NLS.bind( PreflightingCoreNLS.ApkUtils_ZipExtractionFile, apk.getAbsolutePath())); } deleteDirRecursively(temp); } else { //invalid apk throw new PreflightingToolException(NLS.bind( PreflightingCoreNLS.ApkUtils_ZipExtractionFile, apk.getAbsolutePath())); } } catch (InterruptedException e) { throw new PreflightingToolException( PreflightingCoreNLS.ProjectUtils_ErrorExecutingApkTool, e); } catch (IOException e) { throw new PreflightingToolException(NLS.bind( PreflightingCoreNLS.ApkUtils_ZipExtractionFile, apk.getAbsolutePath())); } } private static boolean isApkValid(File apk) { return (apk != null) && (apk.length() > 0) && (apk.getName().toLowerCase().endsWith("apk") || apk.getName().toLowerCase() .endsWith("zip")); } private static void visitFolderToIdentifySmalis(File basefile, Set<File> classesFiles) { if ((basefile != null) && basefile.isDirectory()) { File[] subfolders = basefile.listFiles(); for (File file : subfolders) { visitFolderToIdentifySmalis(file, classesFiles); } } else if ((basefile != null) && basefile.isFile()) { if (basefile.getName().endsWith(SMALI)) { classesFiles.add(basefile); } } } private static Set<File> visitFolderToIdentifySmalis(File rootFolder) { Set<File> classesFiles = new HashSet<File>(); visitFolderToIdentifySmalis(rootFolder, classesFiles); return classesFiles; } private static void visitFolderToIdentifyApks(File basefile, Set<File> apkFiles) { if ((basefile != null) && basefile.isDirectory()) { File[] subfolders = basefile.listFiles(); for (File file : subfolders) { visitFolderToIdentifyApks(file, apkFiles); } } else if ((basefile != null) && basefile.isFile()) { if (basefile.getName().endsWith(APK)) { apkFiles.add(basefile); } } } private static Set<File> visitFolderToIdentifyApks(File rootFolder) { Set<File> classesFiles = new HashSet<File>(); visitFolderToIdentifyApks(rootFolder, classesFiles); return classesFiles; } /** * Unpack a zip file. * * @param file the file * @param destination the destination path or null to unpack at the same directory of file * @return true if unpacked, false otherwise */ private static boolean unpackZipFile(File file, String destination) throws PreflightingToolException { ZipFile zipFile = null; String extractDestination = destination != null ? destination : file.getParent(); if (!extractDestination.endsWith(File.separator)) { extractDestination += File.separator; } boolean unziped = true; try { zipFile = new ZipFile(file); } catch (Throwable e) { unziped = false; } if (zipFile != null) { Enumeration<? extends ZipEntry> entries = zipFile.entries(); InputStream input = null; OutputStream output = null; while (entries.hasMoreElements()) { try { ZipEntry entry = entries.nextElement(); String name = entry.getName(); if (name.startsWith("..")) { throw new PreflightingToolException( PreflightingCoreNLS.ApkToolUtils_MalformedAPK); } File newFile = new File(extractDestination + name); if (entry.isDirectory()) { newFile.mkdirs(); } else { newFile.getParentFile().mkdirs(); if (newFile.createNewFile()) { input = zipFile.getInputStream(entry); output = new BufferedOutputStream(new FileOutputStream(newFile)); copyStreams(input, output); } } } catch (PreflightingToolException pe) { unziped = false; throw pe; } catch (Throwable t) { unziped = false; } finally { try { if (input != null) { input.close(); } if (output != null) { output.close(); } } catch (Throwable t) { //do nothing } } } } return unziped; } /** * Copy the input stream to the output stream * @param inputStream * @param outputStream * @throws IOException */ private static void copyStreams(InputStream inputStream, OutputStream outputStream) throws IOException { byte[] buffer = new byte[1024]; int length; while ((length = inputStream.read(buffer)) >= 0) { outputStream.write(buffer, 0, length); } } /** * This method deletes the directory, all files and all subdirectories under * it. If a deletion fails, the method stops attempting to delete and * returns false. * * @param directory * The directory to be deleted * @return Returns true if all deletions were successful. If the directory * doesn't exist returns false. * @throws IOException * When the parameter isn't a directory */ private static boolean deleteDirRecursively(File directory) throws IOException { String dirName = ""; boolean success = true; if (directory.exists()) { if (directory.isDirectory()) { dirName = directory.getName(); File[] children = directory.listFiles(); for (File element : children) { if (element.isFile()) { success = success && element.delete(); } else { success = success && deleteDirRecursively(element); } } success = success && directory.delete(); } else { String errorMessage = directory.getName() + " is not a diretory."; throw new IOException(errorMessage); } } else { String errorMessage = "The directory does not exist."; success = false; throw new IOException(errorMessage); } return success; } }