001/* 002 * Copyright (C) 2015-2021 KeepSafe Software 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package com.getkeepsafe.dexcount; 017 018import com.android.dexdeps.FieldRef; 019import com.android.dexdeps.HasDeclaringClass; 020import com.android.dexdeps.MethodRef; 021import com.android.dexdeps.Output; 022import com.google.gson.stream.JsonWriter; 023import org.jetbrains.annotations.NotNull; 024 025import java.io.IOException; 026import java.io.Writer; 027import java.nio.CharBuffer; 028import java.util.Arrays; 029import java.util.Collections; 030import java.util.LinkedHashMap; 031import java.util.LinkedHashSet; 032import java.util.List; 033import java.util.Map; 034import java.util.Map.Entry; 035import java.util.Set; 036import java.util.SortedMap; 037import java.util.TreeMap; 038import java.util.stream.Collectors; 039import java.util.stream.Stream; 040 041public class PackageTree { 042 private enum Type { 043 DECLARED, 044 REFERENCED, 045 } 046 047 private final String name; 048 private final boolean isClass; 049 private final Deobfuscator deobfuscator; 050 051 private final LinkedHashMap<Type, Integer> classTotal = new LinkedHashMap<>(); 052 private final LinkedHashMap<Type, Integer> methodTotal = new LinkedHashMap<>(); 053 private final LinkedHashMap<Type, Integer> fieldTotal = new LinkedHashMap<>(); 054 private final SortedMap<String, PackageTree> children = new TreeMap<>(); 055 private final LinkedHashMap<Type, LinkedHashSet<MethodRef>> methods = new LinkedHashMap<>(); 056 private final LinkedHashMap<Type, LinkedHashSet<FieldRef>> fields = new LinkedHashMap<>(); 057 058 public PackageTree() { 059 this("", false, null); 060 } 061 062 public PackageTree(Deobfuscator deobfuscator) { 063 this("", false, deobfuscator); 064 } 065 066 public PackageTree(String name, Deobfuscator deobfuscator) { 067 this(name, isClassName(name), deobfuscator); 068 } 069 070 public PackageTree(String name, boolean isClass, Deobfuscator deobfuscator) { 071 if (name == null) { 072 throw new NullPointerException("name"); 073 } 074 075 if (deobfuscator == null) { 076 deobfuscator = Deobfuscator.EMPTY; 077 } 078 079 this.name = name; 080 this.isClass = isClass; 081 this.deobfuscator = deobfuscator; 082 083 for (Type type : Type.values()) { 084 methods.put(type, new LinkedHashSet<>()); 085 fields.put(type, new LinkedHashSet<>()); 086 } 087 } 088 089 public String getName() { 090 return name; 091 } 092 093 public boolean isClass() { 094 return isClass; 095 } 096 097 public int getClassCount() { 098 return getClassCount(Type.REFERENCED); 099 } 100 101 public int getClassCountDeclared() { 102 return getClassCount(Type.DECLARED); 103 } 104 105 public int getMethodCount() { 106 return getMethodCount(Type.REFERENCED); 107 } 108 109 public int getMethodCountDeclared() { 110 return getMethodCount(Type.DECLARED); 111 } 112 113 public int getFieldCount() { 114 return getFieldCount(Type.REFERENCED); 115 } 116 117 public int getFieldCountDeclared() { 118 return getFieldCount(Type.DECLARED); 119 } 120 121 private int getClassCount(Type type) { 122 Integer maybeTotal = classTotal.get(type); 123 if (maybeTotal != null) { 124 return maybeTotal; 125 } 126 127 if (isClass) { 128 classTotal.put(type, 1); 129 return 1; 130 } 131 132 int result = children.values().parallelStream().mapToInt(child -> child.getClassCount(type)).sum(); 133 classTotal.put(type, result); 134 135 return result; 136 } 137 138 private int getMethodCount(Type type) { 139 Integer maybeTotal = methodTotal.get(type); 140 if (maybeTotal != null) { 141 return maybeTotal; 142 } 143 144 int result = methods.get(type).size() + children.values().parallelStream().mapToInt(child -> child.getMethodCount(type)).sum(); 145 methodTotal.put(type, result); 146 147 return result; 148 } 149 150 private int getFieldCount(Type type) { 151 Integer maybeTotal = fieldTotal.get(type); 152 if (maybeTotal != null) { 153 return maybeTotal; 154 } 155 156 int result = fields.get(type).size() + children.values().parallelStream().mapToInt(child -> child.getFieldCount(type)).sum(); 157 fieldTotal.put(type, result); 158 159 return result; 160 } 161 162 public void addMethodRef(MethodRef ref) { 163 addInternal(descriptorToDot(ref), 0, true, Type.REFERENCED, ref); 164 } 165 166 public void addFieldRef(FieldRef ref) { 167 addInternal(descriptorToDot(ref), 0, false, Type.REFERENCED, ref); 168 } 169 170 public void addDeclaredMethodRef(MethodRef ref) { 171 addInternal(descriptorToDot(ref), 0, true, Type.DECLARED, ref); 172 } 173 174 public void addDeclaredFieldRef(FieldRef ref) { 175 addInternal(descriptorToDot(ref), 0, false, Type.DECLARED, ref); 176 } 177 178 private void addInternal(String name, int startIndex, boolean isMethod, Type type, HasDeclaringClass ref) { 179 int ix = name.indexOf('.', startIndex); 180 String segment; 181 if (ix == -1) { 182 segment = name.substring(startIndex); 183 } else { 184 segment = name.substring(startIndex, ix); 185 } 186 187 PackageTree child = children.get(segment); 188 if (child == null) { 189 child = new PackageTree(segment, deobfuscator); 190 children.put(segment, child); 191 } 192 193 if (ix == -1) { 194 if (isMethod) { 195 child.methods.get(type).add((MethodRef) ref); 196 } else { 197 child.fields.get(type).add((FieldRef) ref); 198 } 199 } else { 200 if (isMethod) { 201 methodTotal.remove(type); 202 } else { 203 fieldTotal.remove(type); 204 } 205 child.addInternal(name, ix + 1, isMethod, type, ref); 206 } 207 } 208 209 public void print(Appendable out, OutputFormat format, PrintOptions opts) throws IOException { 210 switch (format) { 211 case LIST: 212 printPackageList(out, opts); 213 break; 214 215 case TREE: 216 printTree(out, opts); 217 break; 218 219 case JSON: 220 printJson(out, opts); 221 break; 222 223 case YAML: 224 printYaml(out, opts); 225 break; 226 227 default: 228 throw new IllegalArgumentException("Unexpected OutputFormat: " + format); 229 } 230 } 231 232 public void printPackageList(Appendable out, PrintOptions opts) throws IOException { 233 StringBuilder sb = new StringBuilder(64); 234 235 if (opts.getIncludeTotalMethodCount()) { 236 if (opts.isAndroidProject()) { 237 out.append("Total methods: ").append(String.valueOf(getMethodCount())).append("\n"); 238 } 239 240 if (opts.getPrintDeclarations()) { 241 out.append("Total declared methods: ").append(String.valueOf(getClassCountDeclared())).append("\n"); 242 } 243 } 244 245 if (opts.getPrintHeader()) { 246 printPackageListHeader(out, opts); 247 } 248 249 for (PackageTree child : getChildren(opts)) { 250 child.printPackageListRecursively(out, sb, 0, opts); 251 } 252 } 253 254 private void printPackageListHeader(Appendable out, PrintOptions opts) throws IOException { 255 if (opts.getIncludeClassCount()) { 256 out.append(String.format("%-8s ", "classes")); 257 } 258 259 if (opts.isAndroidProject()) { 260 if (opts.getIncludeMethodCount()) { 261 out.append(String.format("%-8s ", "methods")); 262 } 263 264 if (opts.getIncludeFieldCount()) { 265 out.append(String.format("%-8s ", "fields")); 266 } 267 } 268 269 if (opts.getPrintDeclarations()) { 270 out.append(String.format("%-16s ", "declared methods")); 271 out.append(String.format("%-16s ", "declared fields")); 272 } 273 274 out.append("package/class name\n"); 275 } 276 277 private void printPackageListRecursively(Appendable out, StringBuilder sb, int depth, PrintOptions opts) throws IOException { 278 if (depth >= opts.getMaxTreeDepth()) { 279 return; 280 } 281 282 if (!isPrintable(opts)) { 283 // Should be guaranteed by `getChildren()` 284 throw new IllegalStateException("We should never recursively print a non-printable"); 285 } 286 287 int len = sb.length(); 288 if (len > 0) { 289 sb.append('.'); 290 } 291 sb.append(getName()); 292 293 if (opts.getIncludeClassCount()) { 294 out.append(String.format("%-8d ", getClassCount())); 295 } 296 297 if (opts.isAndroidProject()) { 298 if (opts.getIncludeMethodCount()) { 299 out.append(String.format("%-8d ", getMethodCount())); 300 } 301 302 if (opts.getIncludeFieldCount()) { 303 out.append(String.format("%-8d ", getFieldCount())); 304 } 305 } 306 307 if (opts.getPrintDeclarations()) { 308 if (opts.getPrintHeader()) { 309 // The header for the these two columns uses more space. 310 out.append(String.format("%-16d ", getMethodCountDeclared())); 311 out.append(String.format("%-16d ", getFieldCountDeclared())); 312 } else { 313 out.append(String.format("%-8d ", getMethodCountDeclared())); 314 out.append(String.format("%-8d ", getFieldCountDeclared())); 315 } 316 } 317 318 out.append(sb.toString()).append("\n"); 319 320 for (PackageTree child : getChildren(opts)) { 321 child.printPackageListRecursively(out, sb, depth + 1, opts); 322 } 323 324 sb.setLength(len); 325 } 326 327 public void printTree(Appendable out, PrintOptions opts) throws IOException { 328 for (PackageTree child : getChildren(opts)) { 329 child.printTreeRecursively(out, 0, opts); 330 } 331 } 332 333 private void printTreeRecursively(Appendable out, int depth, PrintOptions opts) throws IOException { 334 if (depth >= opts.getMaxTreeDepth()) { 335 return; 336 } 337 338 for (int i = 0; i < depth; i++) { 339 out.append(" "); 340 } 341 out.append(getName()); 342 343 if (opts.getIncludeFieldCount() || opts.getIncludeMethodCount() || opts.getIncludeClassCount()) { 344 out.append(" ("); 345 346 boolean appended = false; 347 if (opts.getIncludeClassCount()) { 348 out.append(String.valueOf(getClassCount())) 349 .append(" ") 350 .append(pluralizedClasses(getClassCount())); 351 appended = true; 352 } 353 354 if (opts.isAndroidProject()) { 355 if (opts.getIncludeMethodCount()) { 356 if (appended) { 357 out.append(", "); 358 } 359 out.append(String.valueOf(getMethodCount())) 360 .append(" ") 361 .append(pluralizedMethods(getMethodCount())); 362 appended = true; 363 } 364 365 if (opts.getIncludeFieldCount()) { 366 if (appended) { 367 out.append(", "); 368 } 369 out.append(String.valueOf(getFieldCount())) 370 .append(" ") 371 .append(pluralizedFields(getFieldCount())); 372 appended = true; 373 } 374 } 375 376 if (opts.getPrintDeclarations()) { 377 if (appended) { 378 out.append(", "); 379 } 380 out.append(String.valueOf(getMethodCountDeclared())) 381 .append(" declared ") 382 .append(pluralizedMethods(getMethodCountDeclared())) 383 .append(", ") 384 .append(String.valueOf(getFieldCountDeclared())) 385 .append(" declared ") 386 .append(pluralizedFields(getFieldCountDeclared())); 387 } 388 389 out.append(")\n"); 390 } 391 392 for (PackageTree child : getChildren(opts)) { 393 child.printTreeRecursively(out, depth + 1, opts); 394 } 395 } 396 397 public void printJson(Appendable out, PrintOptions opts) throws IOException { 398 JsonWriter json = new JsonWriter(new Writer() { 399 @Override 400 public void write(@NotNull char[] chars, int offset, int length) throws IOException { 401 out.append(CharBuffer.wrap(chars, offset, length)); 402 } 403 404 @Override 405 public void flush() { 406 // no-op 407 } 408 409 @Override 410 public void close() { 411 // no-op 412 } 413 }); 414 415 json.setIndent(" "); 416 417 printJsonRecursively(json, 0, opts); 418 } 419 420 private void printJsonRecursively(JsonWriter json, int depth, PrintOptions opts) throws IOException { 421 if (depth >= opts.getMaxTreeDepth()) { 422 return; 423 } 424 425 json.beginObject(); 426 427 json.name("name").value(getName()); 428 429 if (opts.getIncludeClassCount()) { 430 json.name("classes").value(getClassCount()); 431 } 432 433 if (opts.isAndroidProject()) { 434 if (opts.getIncludeMethodCount()) { 435 json.name("methods").value(getMethodCount()); 436 } 437 438 if (opts.getIncludeFieldCount()) { 439 json.name("fields").value(getFieldCount()); 440 } 441 } 442 443 if (opts.getPrintDeclarations()) { 444 json.name("declared_methods").value(getMethodCountDeclared()); 445 json.name("declared_fields").value(getFieldCountDeclared()); 446 } 447 448 json.name("children"); 449 json.beginArray(); 450 for (PackageTree child : getChildren(opts)) { 451 child.printJsonRecursively(json, depth + 1, opts); 452 } 453 json.endArray(); 454 455 json.endObject(); 456 } 457 458 public void printYaml(Appendable out, PrintOptions opts) throws IOException { 459 out.append("---\n"); 460 461 if (opts.getIncludeClassCount()) { 462 out.append("classes: ").append(String.valueOf(getClassCount())).append("\n"); 463 } 464 465 if (opts.isAndroidProject()) { 466 if (opts.getIncludeMethodCount()) { 467 out.append("methods: ").append(String.valueOf(getMethodCount())).append("\n"); 468 } 469 470 if (opts.getIncludeFieldCount()) { 471 out.append("fields: ").append(String.valueOf(getFieldCount())).append("\n"); 472 } 473 } 474 475 if (opts.getPrintDeclarations()) { 476 out.append("declared_methods: ").append(String.valueOf(getMethodCountDeclared())).append("\n"); 477 out.append("declared_fields: ").append(String.valueOf(getFieldCountDeclared())).append("\n"); 478 } 479 480 out.append("counts:\n"); 481 482 for (PackageTree child : getChildren(opts)) { 483 child.printYamlRecursively(out, 0, opts); 484 } 485 } 486 487 private void printYamlRecursively(Appendable out, int depth, PrintOptions opts) throws IOException { 488 if (depth > opts.getMaxTreeDepth()) { 489 return; 490 } 491 492 StringBuilder indentBuilder = new StringBuilder(); 493 for (int i = 0; i < (depth * 2) + 1; ++i) { 494 indentBuilder.append(" "); 495 } 496 String indent = indentBuilder.toString(); 497 498 out.append(indent).append("- name: ").append(getName()).append("\n"); 499 500 indent += " "; 501 502 if (opts.getIncludeClassCount()) { 503 out.append(indent).append("classes: ").append(String.valueOf(getClassCount())).append("\n"); 504 } 505 506 if (opts.isAndroidProject()) { 507 if (opts.getIncludeMethodCount()) { 508 out.append(indent).append("methods: ").append(String.valueOf(getMethodCount())).append("\n"); 509 } 510 511 if (opts.getIncludeFieldCount()) { 512 out.append(indent).append("fields: ").append(String.valueOf(getFieldCount())).append("\n"); 513 } 514 } 515 516 if (opts.getPrintDeclarations()) { 517 out.append(indent).append("declared_methods: ").append(String.valueOf(getMethodCountDeclared())).append("\n"); 518 out.append(indent).append("declared_fields: ").append(String.valueOf(getFieldCountDeclared())).append("\n"); 519 } 520 521 List<PackageTree> childNodes; 522 if (depth + 1 == opts.getMaxTreeDepth()) { 523 childNodes = Collections.emptyList(); 524 } else { 525 childNodes = getChildren(opts); 526 } 527 528 if (childNodes.isEmpty()) { 529 out.append(indent).append("children: []\n"); 530 return; 531 } 532 533 out.append(indent).append("children:\n"); 534 for (PackageTree child : getChildren(opts)) { 535 child.printYamlRecursively(out, depth + 1, opts); 536 } 537 } 538 539 private List<PackageTree> getChildren(PrintOptions opts) { 540 Stream<PackageTree> result = children.values().stream().filter(it -> it.isPrintable(opts)); 541 542 if (opts.getOrderByMethodCount()) { 543 result = result.sorted((lhs, rhs) -> Integer.compare(rhs.getMethodCount(), lhs.getMethodCount())); 544 } 545 546 return result.collect(Collectors.toList()); 547 } 548 549 private boolean isPrintable(PrintOptions opts) { 550 return opts.getIncludeClasses() || !isClass; 551 } 552 553 private String pluralizedClasses(int n) { 554 if (n == 1) { 555 return "class"; 556 } else { 557 return "classes"; 558 } 559 } 560 561 private String pluralizedMethods(int n) { 562 if (n == 1) { 563 return "method"; 564 } else { 565 return "methods"; 566 } 567 } 568 569 private String pluralizedFields(int n) { 570 if (n == 1) { 571 return "field"; 572 } else { 573 return "fields"; 574 } 575 } 576 577 private String descriptorToDot(HasDeclaringClass ref) { 578 String descriptor = ref.getDeclClassName(); 579 String dot = Output.descriptorToDot(descriptor); 580 String deobfuscated = deobfuscator.deobfuscate(dot); 581 if (deobfuscated.indexOf('.') == -1) { 582 // Classes in the unnamed package (e.g. primitive arrays) 583 // will not appear in the output in the current PackageTree 584 // implementation if classes are not included. To work around, 585 // we make an artificial package named "<unnamed>". 586 return "<unnamed>." + deobfuscated; 587 } else { 588 return deobfuscated; 589 } 590 } 591 592 @Override 593 public boolean equals(Object o) { 594 if (this == o) return true; 595 if (o == null || getClass() != o.getClass()) return false; 596 597 PackageTree that = (PackageTree) o; 598 599 if (isClass != that.isClass) return false; 600 if (!name.equals(that.name)) return false; 601 if (!children.equals(that.children)) return false; 602 if (!methods.equals(that.methods)) return false; 603 return fields.equals(that.fields); 604 } 605 606 @Override 607 public int hashCode() { 608 int result = name.hashCode(); 609 result = 31 * result + (isClass ? 1 : 0); 610 result = 31 * result + children.hashCode(); 611 result = 31 * result + methods.hashCode(); 612 result = 31 * result + fields.hashCode(); 613 return result; 614 } 615 616 private static boolean isClassName(String name) { 617 return Character.isUpperCase(name.charAt(0)) || name.contains("[]"); 618 } 619 620 private static com.getkeepsafe.dexcount.thrift.MethodRef methodRefToThrift(MethodRef methodRef) { 621 return new com.getkeepsafe.dexcount.thrift.MethodRef.Builder() 622 .declaringClass(methodRef.getDeclClassName()) 623 .returnType(methodRef.getReturnTypeName()) 624 .methodName(methodRef.getName()) 625 .argumentTypes(Arrays.asList(methodRef.getArgumentTypeNames())) 626 .build(); 627 } 628 629 private static MethodRef methodRefFromThrift(com.getkeepsafe.dexcount.thrift.MethodRef methodRef) { 630 String[] argTypes = new String[0]; 631 if (methodRef.argumentTypes != null) { 632 argTypes = methodRef.argumentTypes.toArray(argTypes); 633 } 634 635 return new MethodRef( 636 methodRef.declaringClass, 637 argTypes, 638 methodRef.returnType, 639 methodRef.methodName 640 ); 641 } 642 643 private static com.getkeepsafe.dexcount.thrift.FieldRef fieldRefToThrift(FieldRef fieldRef) { 644 return new com.getkeepsafe.dexcount.thrift.FieldRef.Builder() 645 .declaringClass(fieldRef.getDeclClassName()) 646 .fieldType(fieldRef.getTypeName()) 647 .fieldName(fieldRef.getName()) 648 .build(); 649 } 650 651 private static FieldRef fieldRefFromThrift(com.getkeepsafe.dexcount.thrift.FieldRef fieldRef) { 652 return new FieldRef( 653 fieldRef.declaringClass, 654 fieldRef.fieldType, 655 fieldRef.fieldName 656 ); 657 } 658 659 public static com.getkeepsafe.dexcount.thrift.PackageTree toThrift(PackageTree tree) { 660 Map<String, com.getkeepsafe.dexcount.thrift.PackageTree> children = new LinkedHashMap<>(); 661 for (Entry<String, PackageTree> entry : tree.children.entrySet()) { 662 children.put(entry.getKey(), toThrift(entry.getValue())); 663 } 664 665 Set<com.getkeepsafe.dexcount.thrift.MethodRef> thriftMethodDecls = 666 tree.methods.get(Type.DECLARED).stream().map(PackageTree::methodRefToThrift).collect(Collectors.toCollection(LinkedHashSet::new)); 667 Set<com.getkeepsafe.dexcount.thrift.MethodRef> thriftMethodRefs = 668 tree.methods.get(Type.REFERENCED).stream().map(PackageTree::methodRefToThrift).collect(Collectors.toCollection(LinkedHashSet::new)); 669 Set<com.getkeepsafe.dexcount.thrift.FieldRef> thriftFieldDecls = 670 tree.fields.get(Type.DECLARED).stream().map(PackageTree::fieldRefToThrift).collect(Collectors.toCollection(LinkedHashSet::new)); 671 Set<com.getkeepsafe.dexcount.thrift.FieldRef> thriftFieldRefs = 672 tree.fields.get(Type.REFERENCED).stream().map(PackageTree::fieldRefToThrift).collect(Collectors.toCollection(LinkedHashSet::new)); 673 674 return new com.getkeepsafe.dexcount.thrift.PackageTree.Builder() 675 .name(tree.getName()) 676 .isClass(tree.isClass()) 677 .children(children) 678 .declaredMethods(thriftMethodDecls) 679 .referencedMethods(thriftMethodRefs) 680 .declaredFields(thriftFieldDecls) 681 .referencedFields(thriftFieldRefs) 682 .build(); 683 } 684 685 public static PackageTree fromThrift(com.getkeepsafe.dexcount.thrift.PackageTree tree) { 686 String name = tree.name != null ? tree.name : ""; 687 boolean isClass = tree.isClass != null ? tree.isClass : false; 688 689 PackageTree result = new PackageTree(name, isClass, Deobfuscator.EMPTY); 690 691 if (tree.children != null) { 692 for (String key : tree.children.keySet()) { 693 result.children.put(key, fromThrift(tree.children.get(key))); 694 } 695 } 696 697 if (tree.declaredMethods != null) { 698 for (com.getkeepsafe.dexcount.thrift.MethodRef declaredMethod : tree.declaredMethods) { 699 result.methods.get(Type.DECLARED).add(methodRefFromThrift(declaredMethod)); 700 } 701 } 702 703 if (tree.referencedMethods != null) { 704 for (com.getkeepsafe.dexcount.thrift.MethodRef referencedMethod : tree.referencedMethods) { 705 result.methods.get(Type.REFERENCED).add(methodRefFromThrift(referencedMethod)); 706 } 707 } 708 709 if (tree.declaredFields != null) { 710 for (com.getkeepsafe.dexcount.thrift.FieldRef declaredField : tree.declaredFields) { 711 result.fields.get(Type.DECLARED).add(fieldRefFromThrift(declaredField)); 712 } 713 } 714 715 if (tree.referencedFields != null) { 716 for (com.getkeepsafe.dexcount.thrift.FieldRef referencedField : tree.referencedFields) { 717 result.fields.get(Type.REFERENCED).add(fieldRefFromThrift(referencedField)); 718 } 719 } 720 721 return result; 722 } 723}