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

com.getkeepsafe.dexcount.PackageTree.groovy Maven / Gradle / Ivy

/*
 * Copyright (C) 2015-2016 KeepSafe Software
 *
 * 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.getkeepsafe.dexcount

import com.android.dexdeps.FieldRef
import com.android.dexdeps.HasDeclaringClass
import com.android.dexdeps.MethodRef
import com.android.dexdeps.Output
import com.google.gson.stream.JsonWriter
import groovy.transform.CompileStatic
import groovy.transform.stc.ClosureParams
import groovy.transform.stc.FirstParam

import java.nio.CharBuffer

@CompileStatic  // necessary to avoid JDK verifier bugs (issues #11 and #12)
class PackageTree {
    // A cached sum of classes. -1 means that there is no cached value.
    // Set by `getClassCount()`, and invalidated by adding new nodes.
    private int classTotal_ = -1

    // A cached sum of this node and all children's method ref counts.
    // -1 means that there is no cached value.  Set by `getMethodCount()`, and
    // invalidated by adding new nodes.
    private int methodTotal_ = -1

    // A cached sum of this node and all children's field-ref counts.
    // Same semantics as methodTotal_.
    private int fieldTotal_ = -1

    private final boolean isClass_
    private final String name_
    private final SortedMap children_ = new TreeMap<>()
    private final Deobfuscator deobfuscator_

    // The set of methods declared on this node.  Will be empty for package
    // nodes and possibly non-empty for class nodes.
    private final Set methods_ = new HashSet<>()

    // The set of fields declared on this node.  Will be empty for package
    // nodes and possibly non-empty for class nodes.
    private final Set fields_ = new HashSet<>()

    PackageTree() {
        this("", false, null)
    }

    PackageTree(Deobfuscator deobfuscator) {
        this("", false, deobfuscator)
    }

    PackageTree(String name, Deobfuscator deobfuscator) {
        this(name, isClassName(name), deobfuscator)
    }

    private PackageTree(name, isClass, Deobfuscator deobfuscator) {
        this.name_ = name
        this.isClass_ = isClass
        this.deobfuscator_ = (deobfuscator ?: new Deobfuscator(null))
    }

    private static boolean isClassName(String name) {
        return Character.isUpperCase(name.charAt(0)) || name.contains("[]")
    }

    void addMethodRef(MethodRef method) {
        addInternal(descriptorToDot(method), 0, true, method)
    }

    void addFieldRef(FieldRef field) {
        addInternal(descriptorToDot(field), 0, false, field)
    }

    private void addInternal(String name, int startIndex, boolean isMethod, HasDeclaringClass ref) {
        def ix = name.indexOf('.', startIndex)
        def segment = ix == -1 ? name.substring(startIndex) : name.substring(startIndex, ix)
        def child = children_[segment]
        if (child == null) {
            child = children_[segment] = new PackageTree(segment, deobfuscator_)
        }

        if (ix == -1) {
            if (isMethod) {
                child.methods_.add((MethodRef) ref)
            } else {
                child.fields_.add((FieldRef) ref)
            }
        } else {
            if (isMethod) {
                methodTotal_ = -1
            } else {
                fieldTotal_ = -1
            }
            child.addInternal(name, ix + 1, isMethod, ref)
        }
    }

    int getClassCount() {
        if (classTotal_ == -1) {
            if (isClass_) {
                classTotal_ = 1
            } else {
                classTotal_ = (int) children_.values().inject(0) {
                    int sum, PackageTree child -> sum + child.getClassCount() }
            }
        }
        return classTotal_
    }

    int getMethodCount() {
        if (methodTotal_ == -1) {
            methodTotal_ = (int) children_.values().inject(methods_.size()) {
                int sum, PackageTree child -> sum + child.getMethodCount() }
        }
        return methodTotal_
    }

    int getFieldCount() {
        if (fieldTotal_ == -1) {
            fieldTotal_ = (int) children_.values().inject(fields_.size()) {
                int sum, PackageTree child -> sum + child.getFieldCount() }
        }
        return fieldTotal_
    }

    void print(Appendable out, OutputFormat format, PrintOptions opts) {
        switch (format) {
            case OutputFormat.LIST:
                printPackageList(out, opts)
                break

            case OutputFormat.TREE:
                printTree(out, opts)
                break

            case OutputFormat.JSON:
                printJson(out, opts)
                break

            case OutputFormat.YAML:
                printYaml(out, opts)
                break

            default:
                throw new IllegalArgumentException("Unknown format: $format")
        }
    }

    void printPackageList(Appendable out, PrintOptions opts) {
        def sb = new StringBuilder(64)

        if (opts.includeTotalMethodCount) {
            out.append("Total methods: ${this.getMethodCount()}\n")
        }

        if (opts.printHeader) {
            printPackageListHeader(out, opts)
        }

        forEach(getChildren(opts)) { it -> it.printPackageListRecursively(out, sb, 0, opts) }
    }

    private static printPackageListHeader(Appendable out, PrintOptions opts) {
        if (opts.includeClassCount) {
            out.append(String.format("%-8s ", "classes"))
        }

        if (opts.includeMethodCount) {
            out.append(String.format("%-8s ", "methods"))
        }

        if (opts.includeFieldCount) {
            out.append(String.format("%-8s ", "fields"))
        }

        out.append("package/class name\n")
    }

    private void printPackageListRecursively(Appendable out, StringBuilder sb, int depth, PrintOptions opts) {
        if (depth >= opts.maxTreeDepth) {
            return
        }

        def len = sb.length()
        if (len > 0) {
            sb.append(".")
        }
        sb.append(name_)

        if (isPrintable(opts)) {
            if (opts.includeClassCount) {
                out.append(String.format("%-8d ", getClassCount()))
            }

            if (opts.includeMethodCount) {
                out.append(String.format("%-8d ", getMethodCount()))
            }

            if (opts.includeFieldCount) {
                out.append(String.format("%-8d ", getFieldCount()))
            }

            out.append(sb.toString())
            out.append('\n')
        }

        forEach(getChildren(opts)) { PackageTree it -> it.printPackageListRecursively(out, sb, depth + 1, opts) }
        sb.setLength(len)
    }

    void printTree(Appendable out, PrintOptions opts) {
        forEach(getChildren(opts)) { PackageTree it -> it.printTreeRecursively(out, 0, opts) }
    }

    private void printTreeRecursively(Appendable out, int indent, PrintOptions opts) {
        // 'indent' here is equal to the current tree depth
        if (indent >= opts.maxTreeDepth) {
            return
        }

        indent.times { out.append("  ") }
        out.append(name_)

        if (opts.includeFieldCount || opts.includeMethodCount || opts.includeClassCount) {
            out.append(" (")

            def appended = false
            if (opts.includeClassCount) {
                out.append(String.valueOf(getClassCount()))
                out.append(" ")
                out.append(pluralizedClasses(getClassCount()))
                appended = true
            }

            if (opts.includeMethodCount) {
                if (appended) {
                    out.append(", ")
                }
                out.append(String.valueOf(getMethodCount()))
                out.append(" ")
                out.append(pluralizedMethods(getMethodCount()))
                appended = true
            }

            if (opts.includeFieldCount) {
                if (appended) {
                    out.append(", ")
                }
                out.append(String.valueOf(getFieldCount()))
                out.append(" ")
                out.append(pluralizeFields(getFieldCount()))
            }

            out.append(")")
        }

        out.append("\n")

        forEach(getChildren(opts)) { PackageTree it -> it.printTreeRecursively(out, indent + 1, opts) }
    }

    void printJson(Appendable out, PrintOptions opts) {
        def json = new JsonWriter(new Writer() {
            @Override
            void write(char[] cbuf, int off, int len) throws IOException {
                CharSequence seq = CharBuffer.wrap(cbuf, off, len)
                out.append(seq)
            }

            @Override
            void flush() throws IOException {

            }

            @Override
            void close() throws IOException {

            }
        })

        // Setting an indentation enables pretty-printing
        json.indent = "  "

        printJsonRecursively(json, 0, opts)
    }

    private void printJsonRecursively(JsonWriter json, int depth, PrintOptions opts) {
        if (depth >= opts.maxTreeDepth) {
            return
        }

        json.beginObject()

        json.name("name").value(name_)

        if (opts.includeClassCount) {
            json.name("classes").value(classCount)
        }

        if (opts.includeMethodCount) {
            json.name("methods").value(methodCount)
        }

        if (opts.includeFieldCount) {
            json.name("fields").value(fieldCount)
        }

        json.name("children")
        json.beginArray()

        forEach(getChildren(opts)) { PackageTree it -> it.printJsonRecursively(json, depth + 1, opts) }

        json.endArray()

        json.endObject()
    }

    void printYaml(Appendable out, PrintOptions opts) {
        out.append("---\n")

        if (opts.includeClassCount) {
            out.append("classes: " + classCount + "\n")
        }

        if (opts.includeMethodCount) {
            out.append("methods: " + methodCount + "\n")
        }

        if (opts.includeFieldCount) {
            out.append("fields: " + fieldCount + "\n")
        }

        out.append("counts:\n")

        forEach(getChildren(opts)) { it.printYamlRecursively(out, 0, opts) }
    }

    private void printYamlRecursively(Appendable out, int depth, PrintOptions opts) {
        if (depth > opts.maxTreeDepth) {
            return
        }

        String indentText = "  " * ((depth * 2) + 1)

        out.append(indentText + "- name: ")
        out.append(name_)
        out.append("\n")

        indentText += "  "

        if (opts.includeClassCount) {
            out.append(indentText)
            out.append("classes: " + classCount)
            out.append("\n")
        }

        if (opts.includeMethodCount) {
            out.append(indentText)
            out.append("methods: " + methodCount)
            out.append("\n")
        }

        if (opts.includeFieldCount) {
            out.append(indentText)
            out.append("fields: " + fieldCount)
            out.append("\n")
        }

        def children = (depth + 1) == opts.maxTreeDepth ? (Collection) [] : getChildren(opts)
        if (children.empty) {
            out.append(indentText)
            out.append("children: []\n")
        } else {
            out.append(indentText)
            out.append("children:\n")
            forEach(children) { PackageTree child -> child.printYamlRecursively(out, depth + 1, opts) }
        }
    }

    private Collection getChildren(PrintOptions opts) {
        def printableChildren = children_.values().findAll {
            it.isPrintable(opts)
        }

        if (opts.orderByMethodCount) {
            // Return the child nodes sorted in descending order by method count.
            printableChildren = printableChildren.sort(false) { PackageTree it -> -it.methodCount }
        }

        return printableChildren
    }

    private boolean isPrintable(PrintOptions opts) {
        return opts.includeClasses || !isClass_
    }

    private static String pluralizedClasses(int n) {
        return n == 1 ? "class" : "classes"
    }

    private static String pluralizedMethods(int n) {
        return n == 1 ? "method" : "methods"
    }

    private static String pluralizeFields(int n) {
        return n == 1 ? "field" : "fields"
    }

    private String descriptorToDot(HasDeclaringClass ref) {
        def descriptor = ref.getDeclClassName()
        def dot = Output.descriptorToDot(descriptor)
        dot = deobfuscator_.deobfuscate(dot)
        if (dot.indexOf('.') == -1) {
            // Classes in the unnamed package (e.g. primitive arrays)
            // will not appear in the output in the current PackageTree
            // implementation if classes are not included.  To work around,
            // we make an artificial package named "".
            dot = "." + dot
        }
        return dot
    }

    /**
     * Iterates through the elements of a collection, applying the given
     * closure to each element.
     *
     * Workaround for old versions of Gradle that do not have the method
     * {@link org.codehaus.groovy.runtime.DefaultGroovyMethods#each(Collection, Closure)}.
     *
     * Without any workaround, projects building using these versions of Groovy,
     * we will fail with a MethodMissingException.
     *
     * This is observed in projects using Gradle 2.2.1, which bundles Groovy 2.3.7.
     *
     * @param collection
     * @param closure
     */
    private static  void forEach(
            Collection collection,
            @ClosureParams(FirstParam.FirstGenericType.class) Closure closure) {
        for (E element : collection) {
            closure.call(element)
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy