• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package org.robolectric.internal.bytecode;
2 
3 import com.google.common.collect.ImmutableList;
4 import com.google.common.collect.ImmutableMap;
5 import com.google.common.collect.ImmutableSet;
6 import com.google.common.collect.Sets;
7 import java.util.Collection;
8 import java.util.Collections;
9 import java.util.HashMap;
10 import java.util.HashSet;
11 import java.util.List;
12 import java.util.Map;
13 import java.util.Set;
14 import org.objectweb.asm.tree.MethodInsnNode;
15 import org.robolectric.annotation.internal.DoNotInstrument;
16 import org.robolectric.annotation.internal.Instrument;
17 import org.robolectric.shadow.api.Shadow;
18 
19 /**
20  * Configuration rules for {@link SandboxClassLoader}.
21  */
22 public class InstrumentationConfiguration {
23 
newBuilder()24   public static Builder newBuilder() {
25     return new Builder();
26   }
27 
28   static final Set<String> CLASSES_TO_ALWAYS_ACQUIRE = Sets.newHashSet(
29       RobolectricInternals.class.getName(),
30       InvokeDynamicSupport.class.getName(),
31       Shadow.class.getName(),
32 
33       // these classes are deprecated and will be removed soon:
34       "org.robolectric.util.FragmentTestUtil",
35       "org.robolectric.util.FragmentTestUtil$FragmentUtilActivity"
36   );
37 
38   static final Set<String> RESOURCES_TO_ALWAYS_ACQUIRE = Sets.newHashSet("build.prop");
39 
40   private final List<String> instrumentedPackages;
41   private final Set<String> instrumentedClasses;
42   private final Set<String> classesToNotInstrument;
43   private final Map<String, String> classNameTranslations;
44   private final Set<MethodRef> interceptedMethods;
45   private final Set<String> classesToNotAcquire;
46   private final Set<String> packagesToNotAcquire;
47   private final Set<String> packagesToNotInstrument;
48   private int cachedHashCode;
49 
50   private final TypeMapper typeMapper;
51   private final Set<MethodRef> methodsToIntercept;
52 
InstrumentationConfiguration( Map<String, String> classNameTranslations, Collection<MethodRef> interceptedMethods, Collection<String> instrumentedPackages, Collection<String> instrumentedClasses, Collection<String> classesToNotAcquire, Collection<String> packagesToNotAquire, Collection<String> classesToNotInstrument, Collection<String> packagesToNotInstrument)53   protected InstrumentationConfiguration(
54       Map<String, String> classNameTranslations,
55       Collection<MethodRef> interceptedMethods,
56       Collection<String> instrumentedPackages,
57       Collection<String> instrumentedClasses,
58       Collection<String> classesToNotAcquire,
59       Collection<String> packagesToNotAquire,
60       Collection<String> classesToNotInstrument,
61       Collection<String> packagesToNotInstrument) {
62     this.classNameTranslations = ImmutableMap.copyOf(classNameTranslations);
63     this.interceptedMethods = ImmutableSet.copyOf(interceptedMethods);
64     this.instrumentedPackages = ImmutableList.copyOf(instrumentedPackages);
65     this.instrumentedClasses = ImmutableSet.copyOf(instrumentedClasses);
66     this.classesToNotAcquire = ImmutableSet.copyOf(classesToNotAcquire);
67     this.packagesToNotAcquire = ImmutableSet.copyOf(packagesToNotAquire);
68     this.classesToNotInstrument = ImmutableSet.copyOf(classesToNotInstrument);
69     this.packagesToNotInstrument = ImmutableSet.copyOf(packagesToNotInstrument);
70     this.cachedHashCode = 0;
71 
72     this.typeMapper = new TypeMapper(classNameTranslations());
73     this.methodsToIntercept = ImmutableSet.copyOf(convertToSlashes(methodsToIntercept()));
74   }
75 
76   /**
77    * Determine if {@link SandboxClassLoader} should instrument a given class.
78    *
79    * @param   mutableClass The class to check.
80    * @return  True if the class should be instrumented.
81    */
shouldInstrument(MutableClass mutableClass)82   public boolean shouldInstrument(MutableClass mutableClass) {
83     return !(mutableClass.isInterface()
84             || mutableClass.isAnnotation()
85             || mutableClass.hasAnnotation(DoNotInstrument.class))
86         && (isInInstrumentedPackage(mutableClass.getName())
87             || instrumentedClasses.contains(mutableClass.getName())
88             || mutableClass.hasAnnotation(Instrument.class))
89         && !(classesToNotInstrument.contains(mutableClass.getName()))
90         && !(isInPackagesToNotInstrument(mutableClass.getName()));
91   }
92 
93   /**
94    * Determine if {@link SandboxClassLoader} should load a given class.
95    *
96    * @param   name The fully-qualified class name.
97    * @return  True if the class should be loaded.
98    */
shouldAcquire(String name)99   public boolean shouldAcquire(String name) {
100     if (CLASSES_TO_ALWAYS_ACQUIRE.contains(name)) {
101       return true;
102     }
103 
104     if (name.equals("java.util.jar.StrictJarFile")) {
105       return true;
106     }
107 
108     // android.R and com.android.internal.R classes must be loaded from the framework jar
109     if (name.matches("(android|com\\.android\\.internal)\\.R(\\$.+)?")) {
110       return true;
111     }
112 
113     // Hack. Fixes https://github.com/robolectric/robolectric/issues/1864
114     if (name.equals("javax.net.ssl.DistinguishedNameParser")
115         || name.equals("javax.microedition.khronos.opengles.GL")) {
116       return true;
117     }
118 
119     for (String packageName : packagesToNotAcquire) {
120       if (name.startsWith(packageName)) return false;
121     }
122 
123     // R classes must be loaded from system CP
124     boolean isRClass = name.matches(".*\\.R(|\\$[a-z]+)$");
125     return !isRClass && !classesToNotAcquire.contains(name);
126   }
127 
128   /**
129    * Determine if {@link SandboxClassLoader} should load a given resource.
130    *
131    * @param name The fully-qualified resource name.
132    * @return True if the resource should be loaded.
133    */
shouldAcquireResource(String name)134   public boolean shouldAcquireResource(String name) {
135     return RESOURCES_TO_ALWAYS_ACQUIRE.contains(name);
136   }
137 
methodsToIntercept()138   public Set<MethodRef> methodsToIntercept() {
139     return Collections.unmodifiableSet(interceptedMethods);
140   }
141 
142   /**
143    * Map from a requested class to an alternate stand-in, or not.
144    *
145    * @return Mapping of class name translations.
146    */
classNameTranslations()147   public Map<String, String> classNameTranslations() {
148     return Collections.unmodifiableMap(classNameTranslations);
149   }
150 
containsStubs(String className)151   public boolean containsStubs(String className) {
152     return className.startsWith("com.google.android.maps.");
153   }
154 
isInInstrumentedPackage(String className)155   private boolean isInInstrumentedPackage(String className) {
156     for (String instrumentedPackage : instrumentedPackages) {
157       if (className.startsWith(instrumentedPackage)) {
158         return true;
159       }
160     }
161     return false;
162   }
163 
isInPackagesToNotInstrument(String className)164   private boolean isInPackagesToNotInstrument(String className) {
165     for (String notInstrumentedPackage : packagesToNotInstrument) {
166       if (className.startsWith(notInstrumentedPackage)) {
167         return true;
168       }
169     }
170     return false;
171   }
172 
173   @Override
equals(Object o)174   public boolean equals(Object o) {
175     if (this == o) return true;
176     if (o == null || getClass() != o.getClass()) return false;
177 
178     InstrumentationConfiguration that = (InstrumentationConfiguration) o;
179 
180     if (!classNameTranslations.equals(that.classNameTranslations)) return false;
181     if (!classesToNotAcquire.equals(that.classesToNotAcquire)) return false;
182     if (!instrumentedPackages.equals(that.instrumentedPackages)) return false;
183     if (!instrumentedClasses.equals(that.instrumentedClasses)) return false;
184     if (!interceptedMethods.equals(that.interceptedMethods)) return false;
185 
186 
187     return true;
188   }
189 
190   @Override
hashCode()191   public int hashCode() {
192     if (cachedHashCode != 0) {
193       return cachedHashCode;
194     }
195 
196     int result = instrumentedPackages.hashCode();
197     result = 31 * result + instrumentedClasses.hashCode();
198     result = 31 * result + classNameTranslations.hashCode();
199     result = 31 * result + interceptedMethods.hashCode();
200     result = 31 * result + classesToNotAcquire.hashCode();
201     cachedHashCode = result;
202     return result;
203   }
204 
remapParamType(String desc)205   public String remapParamType(String desc) {
206     return typeMapper.remapParamType(desc);
207   }
208 
remapParams(String desc)209   public String remapParams(String desc) {
210     return typeMapper.remapParams(desc);
211   }
212 
mappedTypeName(String internalName)213   public String mappedTypeName(String internalName) {
214     return typeMapper.mappedTypeName(internalName);
215   }
216 
shouldIntercept(MethodInsnNode targetMethod)217   boolean shouldIntercept(MethodInsnNode targetMethod) {
218     if (targetMethod.name.equals("<init>")) return false; // sorry, can't strip out calls to super() in constructor
219     return methodsToIntercept.contains(new MethodRef(targetMethod.owner, targetMethod.name))
220         || methodsToIntercept.contains(new MethodRef(targetMethod.owner, "*"));
221   }
222 
convertToSlashes(Set<MethodRef> methodRefs)223   private static Set<MethodRef> convertToSlashes(Set<MethodRef> methodRefs) {
224     HashSet<MethodRef> transformed = new HashSet<>();
225     for (MethodRef methodRef : methodRefs) {
226       transformed.add(new MethodRef(internalize(methodRef.className), methodRef.methodName));
227     }
228     return transformed;
229   }
230 
internalize(String className)231   private static String internalize(String className) {
232     return className.replace('.', '/');
233   }
234 
235   public static final class Builder {
236     public final Collection<String> instrumentedPackages = new HashSet<>();
237     public final Collection<MethodRef> interceptedMethods = new HashSet<>();
238     public final Map<String, String> classNameTranslations = new HashMap<>();
239     public final Collection<String> classesToNotAcquire = new HashSet<>();
240     public final Collection<String> packagesToNotAcquire = new HashSet<>();
241     public final Collection<String> instrumentedClasses = new HashSet<>();
242     public final Collection<String> classesToNotInstrument = new HashSet<>();
243     public final Collection<String> packagesToNotInstrument = new HashSet<>();
244 
Builder()245     public Builder() {
246     }
247 
Builder(InstrumentationConfiguration classLoaderConfig)248     public Builder(InstrumentationConfiguration classLoaderConfig) {
249       instrumentedPackages.addAll(classLoaderConfig.instrumentedPackages);
250       interceptedMethods.addAll(classLoaderConfig.interceptedMethods);
251       classNameTranslations.putAll(classLoaderConfig.classNameTranslations);
252       classesToNotAcquire.addAll(classLoaderConfig.classesToNotAcquire);
253       packagesToNotAcquire.addAll(classLoaderConfig.packagesToNotAcquire);
254       instrumentedClasses.addAll(classLoaderConfig.instrumentedClasses);
255       classesToNotInstrument.addAll(classLoaderConfig.classesToNotInstrument);
256       packagesToNotInstrument.addAll(classLoaderConfig.packagesToNotInstrument);
257     }
258 
doNotAcquireClass(Class<?> clazz)259     public Builder doNotAcquireClass(Class<?> clazz) {
260       doNotAcquireClass(clazz.getName());
261       return this;
262     }
263 
doNotAcquireClass(String className)264     public Builder doNotAcquireClass(String className) {
265       this.classesToNotAcquire.add(className);
266       return this;
267     }
268 
doNotAcquirePackage(String packageName)269     public Builder doNotAcquirePackage(String packageName) {
270       this.packagesToNotAcquire.add(packageName);
271       return this;
272     }
273 
addClassNameTranslation(String fromName, String toName)274     public Builder addClassNameTranslation(String fromName, String toName) {
275       classNameTranslations.put(fromName, toName);
276       return this;
277     }
278 
addInterceptedMethod(MethodRef methodReference)279     public Builder addInterceptedMethod(MethodRef methodReference) {
280       interceptedMethods.add(methodReference);
281       return this;
282     }
283 
addInstrumentedClass(String name)284     public Builder addInstrumentedClass(String name) {
285       instrumentedClasses.add(name);
286       return this;
287     }
288 
addInstrumentedPackage(String packageName)289     public Builder addInstrumentedPackage(String packageName) {
290       instrumentedPackages.add(packageName);
291       return this;
292     }
293 
doNotInstrumentClass(String className)294     public Builder doNotInstrumentClass(String className) {
295       this.classesToNotInstrument.add(className);
296       return this;
297     }
298 
doNotInstrumentPackage(String packageName)299     public Builder doNotInstrumentPackage(String packageName) {
300       this.packagesToNotInstrument.add(packageName);
301       return this;
302     }
303 
build()304     public InstrumentationConfiguration build() {
305       return new InstrumentationConfiguration(
306           classNameTranslations,
307           interceptedMethods,
308           instrumentedPackages,
309           instrumentedClasses,
310           classesToNotAcquire,
311           packagesToNotAcquire,
312           classesToNotInstrument,
313           packagesToNotInstrument);
314     }
315   }
316 }
317