All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.spyhunter99.dex.Main Maven / Gradle / Ivy

The newest version!
/*
 * 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.github.spyhunter99.dex;

import com.android.dexdeps.DexData;
import com.android.dexdeps.DexDataException;
import com.github.spyhunter99.dex.model.CountData;
import com.github.spyhunter99.dex.model.Node;
import com.github.spyhunter99.dex.writers.FormattedHtml;
import com.github.spyhunter99.dex.writers.FormattedText;

import java.io.*;
import java.nio.charset.Charset;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;

/**
 * this is a fork of https://github.com/mihaip/dex-method-counts
 */
public class Main {

    private boolean includeClasses;
    private String packageFilter;
    private int maxDepth = Integer.MAX_VALUE;
    private DexMethodCounts.Filter filter = DexMethodCounts.Filter.ALL;
    private List data =new ArrayList();
    private File outputDir = new File(".");
    private boolean fileout=true;
    private boolean stdout=true;

    public static void main(String[] args) {
        Main main = new Main();
        System.exit(main.run(args));
    }

    /**
     * processes the input files for field and method counts
     * @param args
     */
    public int run(String[] args) {
        try {
            String[] inputFileNames = parseArgs(args);
            data.clear();
            for (String fileName : collectFileNames(inputFileNames)) {
                System.out.println("Processing " + fileName);
                DexCount methods = new DexMethodCounts();

                Map dexFiles = openInputFiles(fileName);

                for (String dexFilenName : dexFiles.keySet()) {
                    RandomAccessFile dexFile = dexFiles.get(dexFilenName);
                    DexData dexData = new DexData(dexFilenName, dexFile);
                    try {
                        dexData.load();
                        CountData generate1 = methods.generate(dexData, includeClasses, packageFilter, maxDepth, filter);
                        generate1.fileName=fileName;
                        boolean merged=false;

                        //https://github.com/spyhunter99/dex-method-counts/issues/3
                        //search for existing report in the case of multidex setups
                        for (int i=0; i < data.size(); i++){
                            if (data.get(i).fileName.equals(generate1.fileName)){
                                //merge the data
                                data.get(i).isMultiDex=true;
                                merge(data.get(i), generate1);
                                merged=true;
                                break;
                            }
                        }

                        if (!merged)
                            data.add(generate1);
                        dexFile.close();
                        continue;
                    } catch (DexDataException d){
                        //not a dex'd file, plan b;
                    }
                    try {
                        data.add(DynamicLoader.getClasses(new File(fileName), includeClasses, packageFilter, maxDepth, filter));
                    } catch (Throwable e) {
                        e.printStackTrace();
                    }

                }
            }

            //then output the data

            processOutput(data);



        } catch (UsageException ue) {
            usage();
            return (2);
        } catch (IOException ioe) {
            if (ioe.getMessage() != null) {
                System.err.println("Failed: " + ioe);
            }
            return (1);
        }
        return 0;
    }

    private void merge(CountData output, CountData newrecord) {
        output.isMultiDex=true;
        output.overallMetrics.fieldCount+=newrecord.overallMetrics.fieldCount;
        output.overallMetrics.methodCount+=newrecord.overallMetrics.methodCount;
        merge(output.packageTree, newrecord.packageTree);
    }

    private void merge(Node lhs, Node rhs) {
        lhs.count.fieldCount+=rhs.count.fieldCount;
        lhs.count.methodCount+=rhs.count.methodCount;
        //foreach item in right, check to see if left has it
            //if not, add it
            //if yes, merge it
            //then merge all children of the node

        Iterator> iterator = rhs.children.entrySet().iterator();
        while (iterator.hasNext()){
            Map.Entry next = iterator.next();
            Node rightNode = next.getValue();
            if (lhs.children.containsKey(next.getKey())){
                Node leftNode = lhs.children.get(next.getKey());
                merge (leftNode, rightNode);
            } else {
                //add to left node as is
                lhs.children.put(next.getKey(), next.getValue());
            }
        }

    }


    public void enableStdOut(boolean value){
        stdout=value;

    }

    public void enableFileOutput(boolean value){
        fileout=value;
    }


    /**
     * if set and text or html output is enabled, the output will end up this directory
     * @param directory
     */
    public void setOutputDirectory(File directory){
        outputDir=directory;
    }


    private void closeAndClear(List outputStreams) {
        for (int i=0; i < outputStreams.size(); i++){
            try {
                outputStreams.get(i).close();
            } catch (Exception e) {
            }
        }
        outputStreams.clear();

    }

    /**
     * gets the results of the last analysis, useful for wiring in your own output mechanism
     * @return
     */
    public List getData(){
        return data;
    }

    private void processOutput(List data) {
        FormattedText text = new FormattedText();
        FormattedHtml html = new FormattedHtml();

        if (data.size()>1) {
            //one report for all files processed
            String report = text.getReport(data);
            if (stdout)
                System.out.println(report);
            if (fileout){
                try {
                    FileOutputStream fos = new FileOutputStream(outputDir.getAbsolutePath() + File.separator + "dex-count-report.txt");
                    fos.write(report.getBytes(Charset.defaultCharset()));
                    fos.close();
                }catch (Exception ex){
                    ex.printStackTrace();
                }
            }

            report=null;
            report = html.getReport(data);
            if (fileout){
                try {
                    FileOutputStream fos = new FileOutputStream(outputDir.getAbsolutePath() + File.separator + "index.html");
                    fos.write(report.getBytes(Charset.defaultCharset()));
                    fos.close();
                }catch (Exception ex){
                    ex.printStackTrace();
                }
            }
            //individual reports for each file processed
            for (int i = 0; i < data.size(); i++) {
                report = text.getReport(data.get(i));
                if (stdout)
                    System.out.println(report);
                if (fileout) {
                    try {
                        FileOutputStream fos = new FileOutputStream(outputDir.getAbsolutePath() + File.separator + new File(data.get(i).fileName).getName() + ".txt");
                        fos.write(report.getBytes(Charset.defaultCharset()));
                        fos.close();
                    } catch (Exception ex) {
                        ex.printStackTrace();
                    }
                }

                report = null;
                report = html.getReport(data.get(i));
                if (fileout) {
                    try {
                        FileOutputStream fos = new FileOutputStream(outputDir.getAbsolutePath() + File.separator + new File(data.get(i).fileName).getName() + ".html");
                        fos.write(report.getBytes(Charset.defaultCharset()));
                        fos.close();
                    } catch (Exception ex) {
                        ex.printStackTrace();
                    }
                }
            }
        } else {
            //one report for all files processed
            String report = text.getReport(data);
            if (stdout)
                System.out.println(report);
            if (fileout){
                try {
                    FileOutputStream fos = new FileOutputStream(outputDir.getAbsolutePath() + File.separator + "dex-count-report.txt");
                    fos.write(report.getBytes(Charset.defaultCharset()));
                    fos.close();
                }catch (Exception ex){
                    ex.printStackTrace();
                }
            }

            report=null;
            report = html.getReport(data);
            if (fileout){
                try {
                    FileOutputStream fos = new FileOutputStream(outputDir.getAbsolutePath() + File.separator + "dex-count-report.html");
                    fos.write(report.getBytes(Charset.defaultCharset()));
                    fos.close();
                }catch (Exception ex){
                    ex.printStackTrace();
                }
            }
        }
    }



    /**
     * Opens an input file, which could be a .dex or a .jar/.apk with a
     * classes.dex inside.  If the latter, we extract the contents to a
     * temporary file.
     */
    Map openInputFiles(String fileName) throws IOException {
        Map dexFiles = new HashMap();

        openInputFileAsZip(fileName, dexFiles);
        if (dexFiles.size() == 0) {
            File inputFile = new File(fileName);
            RandomAccessFile dexFile = new RandomAccessFile(inputFile, "r");
            dexFiles.put(fileName, dexFile);
        }

        return dexFiles;
    }

    /**
     * Tries to open an input file as a Zip archive (jar/apk) with a
     * "classes.dex" inside.
     */
    void openInputFileAsZip(String fileName, Map dexFiles) throws IOException {
        ZipFile zipFile;

        // Try it as a zip file.
        try {
            zipFile = new ZipFile(fileName);
        } catch (FileNotFoundException fnfe) {
            // not found, no point in retrying as non-zip.
            System.err.println("Unable to open '" + fileName + "': " +
                    fnfe.getMessage());
            throw fnfe;
        } catch (ZipException ze) {
            // not a zip
            return;
        }

        // Open and add all files matching "classes.*\.dex" in the zip file.
        for (ZipEntry entry : Collections.list(zipFile.entries())) {
            if (entry.getName().matches("classes.*\\.dex")) {
                dexFiles.put(entry.getName(), openDexFile(zipFile, entry));
            }
        }

        zipFile.close();
    }

    RandomAccessFile openDexFile(ZipFile zipFile, ZipEntry entry) throws IOException  {
        // We know it's a zip; see if there's anything useful inside.  A
        // failure here results in some type of IOException (of which
        // ZipException is a subclass).
        InputStream zis = zipFile.getInputStream(entry);

        // Create a temp file to hold the DEX data, open it, and delete it
        // to ensure it doesn't hang around if we fail.
        File tempFile = File.createTempFile("dexdeps", ".dex");
        RandomAccessFile dexFile = new RandomAccessFile(tempFile, "rw");
        tempFile.delete();

        // Copy all data from input stream to output file.
        byte copyBuf[] = new byte[32768];
        int actual;

        while (true) {
            actual = zis.read(copyBuf);
            if (actual == -1)
                break;

            dexFile.write(copyBuf, 0, actual);
        }

        dexFile.seek(0);

        return dexFile;
    }

    private String[] parseArgs(String[] args) {
        //TODO replace with commons cli, because this is just silly
        int idx;

        for (idx = 0; idx < args.length; idx++) {
            String arg = args[idx];

            if (arg.equals("--") || !arg.startsWith("--")) {
                break;
            } else if (arg.equals("--disableStdOut")) {
                stdout=false;
            } else if (arg.equals("--enableFileOut")) {
                fileout=true;
            } else if (arg.equals("--include-classes")) {
                includeClasses = true;
            } else if (arg.startsWith("--package-filter=")) {
                packageFilter = arg.substring(arg.indexOf('=') + 1);
            } else if (arg.startsWith("--max-depth=")) {
                maxDepth =
                    Integer.parseInt(arg.substring(arg.indexOf('=') + 1));
            } else if (arg.startsWith("--filter=")) {
                filter = Enum.valueOf(
                    DexMethodCounts.Filter.class,
                    arg.substring(arg.indexOf('=') + 1).toUpperCase());
            } else {
                System.err.println("Unknown option '" + arg + "'");
                throw new UsageException();
            }
        }

        // We expect at least one more argument (file name).
        int fileCount = args.length - idx;
        if (fileCount == 0) {
            throw new UsageException();
        }
        String[] inputFileNames = new String[fileCount];
        System.arraycopy(args, idx, inputFileNames, 0, fileCount);
        return inputFileNames;
    }

    private void usage() {
        System.err.print(
            "DEX per-package/class method counts v1.5\n" +
            "Usage: dex-method-counts [options]  ...\n" +
            "Options:\n" +
            "  --disableStdOut\n" +
            "  --enableFileOut\n" +
            "  --include-classes\n" +
            "  --package-filter=com.foo.bar\n" +
            "  --max-depth=N\n" +
            "  --filter=ALL|DEFINED_ONLY|REFERENCED_ONLY\n"
        );
    }

    /**
     * Checks if input files array contain directories and
     * adds it's contents to the file list if so.
     * Otherwise just adds a file to the list.
     *
     * @return a List of file names to process
     */
    private List collectFileNames(String[] inputFileNames) {
        List fileNames = new ArrayList();
        for (String inputFileName : inputFileNames) {
            File file = new File(inputFileName);
            if (file.isDirectory()) {
                String dirPath = file.getAbsolutePath();
                for (String fileInDir: file.list()){
                    fileNames.add(dirPath + File.separator + fileInDir);
                }
            } else {
                fileNames.add(inputFileName);
            }
        }
        return fileNames;
    }

    private static class UsageException extends RuntimeException {}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy