ClassProto.java revision 490a8b2cd01ab7e441ba7017ee74f4778c97abec
1/*
2 * Copyright 2013, Google Inc.
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are
7 * met:
8 *
9 *     * Redistributions of source code must retain the above copyright
10 * notice, this list of conditions and the following disclaimer.
11 *     * Redistributions in binary form must reproduce the above
12 * copyright notice, this list of conditions and the following disclaimer
13 * in the documentation and/or other materials provided with the
14 * distribution.
15 *     * Neither the name of Google Inc. nor the names of its
16 * contributors may be used to endorse or promote products derived from
17 * this software without specific prior written permission.
18 *
19 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 */
31
32package org.jf.dexlib2.analysis;
33
34import com.google.common.base.Predicate;
35import com.google.common.base.Predicates;
36import com.google.common.collect.FluentIterable;
37import com.google.common.collect.Iterables;
38import com.google.common.collect.Lists;
39import com.google.common.collect.Maps;
40import org.jf.dexlib2.AccessFlags;
41import org.jf.dexlib2.analysis.util.TypeProtoUtils;
42import org.jf.dexlib2.iface.ClassDef;
43import org.jf.dexlib2.iface.Field;
44import org.jf.dexlib2.iface.Method;
45import org.jf.dexlib2.iface.reference.FieldReference;
46import org.jf.dexlib2.iface.reference.MethodReference;
47import org.jf.dexlib2.util.FieldUtil;
48import org.jf.util.ExceptionWithContext;
49import org.jf.util.SparseArray;
50
51import javax.annotation.Nonnull;
52import javax.annotation.Nullable;
53import java.util.*;
54
55/**
56 * A class "prototype". This contains things like the interfaces, the superclass, the vtable and the instance fields
57 * and their offsets.
58 */
59public class ClassProto implements TypeProto {
60    @Nonnull protected final ClassPath classPath;
61    @Nonnull protected final String type;
62    @Nullable protected ClassDef classDef;
63    @Nullable protected LinkedHashMap<String, ClassDef> interfaces;
64    @Nullable protected Method[] vtable;
65    @Nullable protected SparseArray<FieldReference> instanceFields;
66    protected boolean interfacesFullyResolved = true;
67
68    public ClassProto(@Nonnull ClassPath classPath, @Nonnull String type) {
69        if (type.charAt(0) != 'L') {
70            throw new ExceptionWithContext("Cannot construct ClassProto for non reference type: %s", type);
71        }
72        this.classPath = classPath;
73        this.type = type;
74    }
75
76    @Override public String toString() { return type; }
77    @Nonnull @Override public ClassPath getClassPath() { return classPath; }
78    @Nonnull @Override public String getType() { return type; }
79
80    @Nonnull
81    public ClassDef getClassDef() {
82        if (classDef == null) {
83            classDef = classPath.getClassDef(type);
84        }
85        return classDef;
86    }
87
88    @Nonnull
89    Method[] getVtable() {
90        if (vtable == null) {
91            vtable = loadVtable();
92        }
93        return vtable;
94    }
95
96    @Nonnull
97    SparseArray<FieldReference> getInstanceFields() {
98        if (instanceFields == null) {
99            instanceFields = loadFields();
100        }
101        return instanceFields;
102    }
103
104    /**
105     * Returns true if this class is an interface.
106     *
107     * If this class is not defined, then this will throw an UnresolvedClassException
108     *
109     * @return True if this class is an interface
110     */
111    public boolean isInterface() {
112        ClassDef classDef = getClassDef();
113        return (classDef.getAccessFlags() & AccessFlags.INTERFACE.getValue()) != 0;
114    }
115
116    @Nonnull
117    protected LinkedHashMap<String, ClassDef> getInterfaces() {
118        if (interfaces != null) {
119            return interfaces;
120        }
121
122        interfaces = Maps.newLinkedHashMap();
123
124        try {
125            for (String interfaceType: getClassDef().getInterfaces()) {
126                if (!interfaces.containsKey(interfaceType)) {
127                    ClassDef interfaceDef;
128                    try {
129                        interfaceDef = classPath.getClassDef(interfaceType);
130                        interfaces.put(interfaceType, interfaceDef);
131                    } catch (UnresolvedClassException ex) {
132                        interfaces.put(interfaceType, null);
133                        interfacesFullyResolved = false;
134                    }
135
136                    ClassProto interfaceProto = (ClassProto) classPath.getClass(interfaceType);
137                    for (String superInterface: interfaceProto.getInterfaces().keySet()) {
138                        if (!interfaces.containsKey(superInterface)) {
139                            interfaces.put(superInterface, interfaceProto.getInterfaces().get(superInterface));
140                        }
141                    }
142                    if (!interfaceProto.interfacesFullyResolved) {
143                        interfacesFullyResolved = false;
144                    }
145                }
146            }
147        } catch (UnresolvedClassException ex) {
148            interfacesFullyResolved = false;
149        }
150
151        // now add self and super class interfaces, required for common super class lookup
152        // we don't really need ClassDef's for that, so let's just use null
153
154        if (isInterface() && !interfaces.containsKey(getType())) {
155            interfaces.put(getType(), null);
156        }
157
158        try {
159            String superclass = getSuperclass();
160            if (superclass != null) {
161                ClassProto superclassProto = (ClassProto) classPath.getClass(superclass);
162                for (String superclassInterface: superclassProto.getInterfaces().keySet()) {
163                    if (!interfaces.containsKey(superclassInterface)) {
164                        interfaces.put(superclassInterface, null);
165                    }
166                }
167                if (!superclassProto.interfacesFullyResolved) {
168                    interfacesFullyResolved = false;
169                }
170            }
171        } catch (UnresolvedClassException ex) {
172            interfacesFullyResolved = false;
173        }
174
175        return interfaces;
176    }
177
178    @Nonnull
179    protected Iterable<ClassDef> getDirectInterfaces() {
180        if (!interfacesFullyResolved) {
181            throw new UnresolvedClassException("Interfaces for class %s not fully resolved", getType());
182        }
183
184        return FluentIterable.from(getInterfaces().values()).filter(Predicates.notNull());
185    }
186
187    /**
188     * Checks if this class implements the given interface.
189     *
190     * If the interfaces of this class cannot be fully resolved then this
191     * method will either return true or throw an UnresolvedClassException
192     *
193     * @param iface The interface to check for
194     * @return true if this class implements the given interface, otherwise false
195     */
196    @Override
197    public boolean implementsInterface(@Nonnull String iface) {
198        if (getInterfaces().containsKey(iface)) {
199            return true;
200        }
201        if (!interfacesFullyResolved) {
202            throw new UnresolvedClassException("Interfaces for class %s not fully resolved", getType());
203        }
204        return false;
205    }
206
207    @Nullable @Override
208    public String getSuperclass() {
209        return getClassDef().getSuperclass();
210    }
211
212    /**
213     * This is a helper method for getCommonSuperclass
214     *
215     * It checks if this class is an interface, and if so, if other implements it.
216     *
217     * If this class is undefined, we go ahead and check if it is listed in other's interfaces. If not, we throw an
218     * UndefinedClassException
219     *
220     * If the interfaces of other cannot be fully resolved, we check the interfaces that can be resolved. If not found,
221     * we throw an UndefinedClassException
222     *
223     * @param other The class to check the interfaces of
224     * @return true if this class is an interface (or is undefined) other implements this class
225     *
226     */
227    private boolean checkInterface(@Nonnull ClassProto other) {
228        boolean isResolved = true;
229        boolean isInterface = true;
230        try {
231            isInterface = isInterface();
232        } catch (UnresolvedClassException ex) {
233            isResolved = false;
234            // if we don't know if this class is an interface or not,
235            // we can still try to call other.implementsInterface(this)
236        }
237        if (isInterface) {
238            try {
239                if (other.implementsInterface(getType())) {
240                    return true;
241                }
242            } catch (UnresolvedClassException ex) {
243                // There are 2 possibilities here, depending on whether we were able to resolve this class.
244                // 1. If this class is resolved, then we know it is an interface class. The other class either
245                //    isn't defined, or its interfaces couldn't be fully resolved.
246                //    In this case, we throw an UnresolvedClassException
247                // 2. If this class is not resolved, we had tried to call implementsInterface anyway. We don't
248                //    know for sure if this class is an interface or not. We return false, and let processing
249                //    continue in getCommonSuperclass
250                if (isResolved) {
251                    throw ex;
252                }
253            }
254        }
255        return false;
256    }
257
258    @Override @Nonnull
259    public TypeProto getCommonSuperclass(@Nonnull TypeProto other) {
260        // use the other type's more specific implementation
261        if (!(other instanceof ClassProto)) {
262            return other.getCommonSuperclass(this);
263        }
264
265        if (this == other || getType().equals(other.getType())) {
266            return this;
267        }
268
269        if (this.getType().equals("Ljava/lang/Object;")) {
270            return this;
271        }
272
273        if (other.getType().equals("Ljava/lang/Object;")) {
274            return other;
275        }
276
277        boolean gotException = false;
278        try {
279            if (checkInterface((ClassProto)other)) {
280                return this;
281            }
282        } catch (UnresolvedClassException ex) {
283            gotException = true;
284        }
285
286        try {
287            if (((ClassProto)other).checkInterface(this)) {
288                return other;
289            }
290        } catch (UnresolvedClassException ex) {
291            gotException = true;
292        }
293        if (gotException) {
294            return classPath.getUnknownClass();
295        }
296
297        List<TypeProto> thisChain = Lists.<TypeProto>newArrayList(this);
298        Iterables.addAll(thisChain, TypeProtoUtils.getSuperclassChain(this));
299
300        List<TypeProto> otherChain = Lists.newArrayList(other);
301        Iterables.addAll(otherChain, TypeProtoUtils.getSuperclassChain(other));
302
303        // reverse them, so that the first entry is either Ljava/lang/Object; or Ujava/lang/Object;
304        thisChain = Lists.reverse(thisChain);
305        otherChain = Lists.reverse(otherChain);
306
307        for (int i=Math.min(thisChain.size(), otherChain.size())-1; i>=0; i--) {
308            TypeProto typeProto = thisChain.get(i);
309            if (typeProto.getType().equals(otherChain.get(i).getType())) {
310                return typeProto;
311            }
312        }
313
314        return classPath.getUnknownClass();
315    }
316
317    @Override
318    @Nullable
319    public FieldReference getFieldByOffset(int fieldOffset) {
320        if (getInstanceFields().size() == 0) {
321            return null;
322        }
323        return getInstanceFields().get(fieldOffset);
324    }
325
326    @Override
327    @Nullable
328    public MethodReference getMethodByVtableIndex(int vtableIndex) {
329        if (vtableIndex < 0 || vtableIndex >= getVtable().length) {
330            return null;
331        }
332        return getVtable()[vtableIndex];
333    }
334
335    @Nonnull
336    private SparseArray<FieldReference> loadFields() {
337        //This is a bit of an "involved" operation. We need to follow the same algorithm that dalvik uses to
338        //arrange fields, so that we end up with the same field offsets (which is needed for deodexing).
339        //See mydroid/dalvik/vm/oo/Class.c - computeFieldOffsets()
340
341        final byte REFERENCE = 0;
342        final byte WIDE = 1;
343        final byte OTHER = 2;
344
345        ArrayList<Field> loadedFields = getInstanceFields(getClassDef());
346        Field[] fields = new Field[loadedFields.size()];
347        //the "type" for each field in fields. 0=reference,1=wide,2=other
348        byte[] fieldTypes = new byte[fields.length];
349        for (int i=0;i<fields.length;i++) {
350            fields[i] = loadedFields.get(i);
351            fieldTypes[i] = getFieldType(fields[i].getType());
352        }
353
354        //The first operation is to move all of the reference fields to the front. To do this, find the first
355        //non-reference field, then find the last reference field, swap them and repeat
356        int back = fields.length - 1;
357        int front;
358        for (front = 0; front<fields.length; front++) {
359            if (fieldTypes[front] != REFERENCE) {
360                while (back > front) {
361                    if (fieldTypes[back] == REFERENCE) {
362                        swap(fieldTypes, fields, front, back--);
363                        break;
364                    }
365                    back--;
366                }
367            }
368
369            if (fieldTypes[front] != REFERENCE) {
370                break;
371            }
372        }
373
374        int startFieldOffset = 8;
375        String superclassType = getSuperclass();
376        ClassProto superclass = null;
377        if (superclassType != null) {
378            superclass = (ClassProto) classPath.getClass(superclassType);
379            if (superclass != null) {
380                startFieldOffset = superclass.getNextFieldOffset();
381            }
382        }
383
384        int fieldIndexMod;
385        if ((startFieldOffset % 8) == 0) {
386            fieldIndexMod = 0;
387        } else {
388            fieldIndexMod = 1;
389        }
390
391        //next, we need to group all the wide fields after the reference fields. But the wide fields have to be
392        //8-byte aligned. If we're on an odd field index, we need to insert a 32-bit field. If the next field
393        //is already a 32-bit field, use that. Otherwise, find the first 32-bit field from the end and swap it in.
394        //If there are no 32-bit fields, do nothing for now. We'll add padding when calculating the field offsets
395        if (front < fields.length && (front % 2) != fieldIndexMod) {
396            if (fieldTypes[front] == WIDE) {
397                //we need to swap in a 32-bit field, so the wide fields will be correctly aligned
398                back = fields.length - 1;
399                while (back > front) {
400                    if (fieldTypes[back] == OTHER) {
401                        swap(fieldTypes, fields, front++, back);
402                        break;
403                    }
404                    back--;
405                }
406            } else {
407                //there's already a 32-bit field here that we can use
408                front++;
409            }
410        }
411
412        //do the swap thing for wide fields
413        back = fields.length - 1;
414        for (; front<fields.length; front++) {
415            if (fieldTypes[front] != WIDE) {
416                while (back > front) {
417                    if (fieldTypes[back] == WIDE) {
418                        swap(fieldTypes, fields, front, back--);
419                        break;
420                    }
421                    back--;
422                }
423            }
424
425            if (fieldTypes[front] != WIDE) {
426                break;
427            }
428        }
429
430        int superFieldCount = 0;
431        if (superclass != null) {
432            superFieldCount = superclass.instanceFields.size();
433        }
434
435        //now the fields are in the correct order. Add them to the SparseArray and lookup, and calculate the offsets
436        int totalFieldCount = superFieldCount + fields.length;
437        SparseArray<FieldReference> instanceFields = new SparseArray<FieldReference>(totalFieldCount);
438
439        int fieldOffset;
440
441        if (superclass != null && superFieldCount > 0) {
442            for (int i=0; i<superFieldCount; i++) {
443                instanceFields.append(superclass.instanceFields.keyAt(i), superclass.instanceFields.valueAt(i));
444            }
445
446            fieldOffset = instanceFields.keyAt(superFieldCount-1);
447
448            FieldReference lastSuperField = superclass.instanceFields.valueAt(superFieldCount-1);
449            char fieldType = lastSuperField.getType().charAt(0);
450            if (fieldType == 'J' || fieldType == 'D') {
451                fieldOffset += 8;
452            } else {
453                fieldOffset += 4;
454            }
455        } else {
456            //the field values start at 8 bytes into the DataObject dalvik structure
457            fieldOffset = 8;
458        }
459
460        boolean gotDouble = false;
461        for (int i=0; i<fields.length; i++) {
462            FieldReference field = fields[i];
463
464            //add padding to align the wide fields, if needed
465            if (fieldTypes[i] == WIDE && !gotDouble) {
466                if (!gotDouble) {
467                    if (fieldOffset % 8 != 0) {
468                        assert fieldOffset % 8 == 4;
469                        fieldOffset += 4;
470                    }
471                    gotDouble = true;
472                }
473            }
474
475            instanceFields.append(fieldOffset, field);
476            if (fieldTypes[i] == WIDE) {
477                fieldOffset += 8;
478            } else {
479                fieldOffset += 4;
480            }
481        }
482
483        return instanceFields;
484    }
485
486    @Nonnull
487    private ArrayList<Field> getInstanceFields(@Nonnull ClassDef classDef) {
488        ArrayList<Field> instanceFields = Lists.newArrayList();
489        for (Field field: classDef.getFields()) {
490            if (!FieldUtil.isStatic(field)) {
491                instanceFields.add(field);
492            }
493        }
494        return instanceFields;
495    }
496
497    private byte getFieldType(String fieldType) {
498        switch (fieldType.charAt(0)) {
499            case '[':
500            case 'L':
501                return 0; //REFERENCE
502            case 'J':
503            case 'D':
504                return 1; //WIDE
505            default:
506                return 2; //OTHER
507        }
508    }
509
510    private void swap(byte[] fieldTypes, FieldReference[] fields, int position1, int position2) {
511        byte tempType = fieldTypes[position1];
512        fieldTypes[position1] = fieldTypes[position2];
513        fieldTypes[position2] = tempType;
514
515        FieldReference tempField = fields[position1];
516        fields[position1] = fields[position2];
517        fields[position2] = tempField;
518    }
519
520    private int getNextFieldOffset() {
521        SparseArray<FieldReference> instanceFields = getInstanceFields();
522        if (instanceFields.size() == 0) {
523            return 8;
524        }
525
526        int lastItemIndex = instanceFields.size()-1;
527        int fieldOffset = instanceFields.keyAt(lastItemIndex);
528        FieldReference lastField = instanceFields.valueAt(lastItemIndex);
529
530        switch (lastField.getType().charAt(0)) {
531            case 'J':
532            case 'D':
533                return fieldOffset + 8;
534            default:
535                return fieldOffset + 4;
536        }
537    }
538
539    //TODO: check the case when we have a package private method that overrides an interface method
540    @Nonnull
541    private Method[] loadVtable() {
542        //TODO: it might be useful to keep track of which class's implementation is used for each virtual method. In other words, associate the implementing class type with each vtable entry
543        List<Method> virtualMethodList = Lists.newLinkedList();
544
545        //copy the virtual methods from the superclass
546        String superclassType = getSuperclass();
547        if (superclassType != null) {
548            ClassProto superclass = (ClassProto) classPath.getClass(superclassType);
549            for (int i=0; i<superclass.getVtable().length; i++) {
550                virtualMethodList.add(superclass.getVtable()[i]);
551            }
552        }
553
554        //iterate over the virtual methods in the current class, and only add them when we don't already have the
555        //method (i.e. if it was implemented by the superclass)
556        if (!isInterface()) {
557            addToVtable(getClassDef().getVirtualMethods(), virtualMethodList);
558
559            for (ClassDef interfaceDef: getDirectInterfaces()) {
560                addToVtable(interfaceDef.getVirtualMethods(), virtualMethodList);
561            }
562        }
563
564        Method[] vtable = new Method[virtualMethodList.size()];
565        for (int i=0; i<virtualMethodList.size(); i++) {
566            vtable[i] = virtualMethodList.get(i);
567        }
568
569        return vtable;
570    }
571
572    private void addToVtable(@Nonnull Iterable<? extends Method> localMethods, @Nonnull List<Method> vtable) {
573        List<? extends Method> methods = Lists.newArrayList(localMethods);
574        Collections.sort(methods);
575
576        for (Method virtualMethod: methods) {
577            boolean found = false;
578            for (int i=0; i<vtable.size(); i++) {
579                Method superMethod = vtable.get(i);
580                if (methodSignaturesMatch(superMethod, virtualMethod)) {
581                    if (classPath.getApi() < 17 || canAccess(superMethod)) {
582                        found = true;
583                        vtable.set(i, virtualMethod);
584                        break;
585                    }
586                }
587            }
588            if (!found) {
589                vtable.add(virtualMethod);
590            }
591        }
592    }
593
594    private boolean methodSignaturesMatch(@Nonnull Method a, @Nonnull Method b) {
595        return (a.getName().equals(b.getName())
596                && a.getReturnType().equals(b.getReturnType())
597                && a.getParameters().equals(b.getParameters()));
598    }
599
600    private boolean canAccess(@Nonnull Method virtualMethod) {
601        if (!methodIsPackagePrivate(virtualMethod.getAccessFlags())) {
602            return true;
603        }
604
605        String otherPackage = getPackage(virtualMethod.getDefiningClass());
606        String ourPackage = getPackage(getClassDef().getType());
607        return otherPackage.equals(ourPackage);
608    }
609
610    @Nonnull
611    private String getPackage(@Nonnull String classType) {
612        int lastSlash = classType.lastIndexOf('/');
613        if (lastSlash < 0) {
614            return "";
615        }
616        return classType.substring(1, lastSlash);
617    }
618
619    private static boolean methodIsPackagePrivate(int accessFlags) {
620        return (accessFlags & (AccessFlags.PRIVATE.getValue() |
621                AccessFlags.PROTECTED.getValue() |
622                AccessFlags.PUBLIC.getValue())) == 0;
623    }
624}
625