• 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 java.util.ArrayList;
7 import java.util.Collection;
8 import java.util.Collections;
9 import java.util.HashMap;
10 import java.util.HashSet;
11 import java.util.Iterator;
12 import java.util.List;
13 import java.util.Map;
14 import java.util.Set;
15 import java.util.stream.Collectors;
16 import org.objectweb.asm.tree.MethodInsnNode;
17 import org.robolectric.annotation.internal.DoNotInstrument;
18 import org.robolectric.annotation.internal.Instrument;
19 import org.robolectric.shadow.api.Shadow;
20 
21 /** Configuration rules for {@link SandboxClassLoader}. */
22 public class InstrumentationConfiguration {
23 
newBuilder()24   public static Builder newBuilder() {
25     return new Builder();
26   }
27 
28   static final ImmutableSet<String> CLASSES_TO_ALWAYS_ACQUIRE =
29       ImmutableSet.of(
30           RobolectricInternals.class.getName(),
31           InvokeDynamicSupport.class.getName(),
32           Shadow.class.getName());
33 
34   static final ImmutableSet<String> PACKAGES_TO_NEVER_ACQUIRE =
35       ImmutableSet.of(
36           "com.sun.",
37           "java.",
38           "javax.",
39           "jdk.internal.",
40           "org.junit.",
41           "org.robolectric.annotation.",
42           "org.robolectric.internal.",
43           "org.robolectric.pluginapi.",
44           "org.robolectric.simulator.pluginapi.",
45           "org.robolectric.util.",
46           "sun.");
47 
48   // Must always acquire these as they change from API level to API level
49   static final ImmutableSet<String> RESOURCES_TO_ALWAYS_ACQUIRE =
50       ImmutableSet.of("build.prop", "usr/share/zoneinfo/tzdata");
51 
52   private final List<String> instrumentedPackages;
53   private final Set<String> instrumentedClasses;
54   private final Set<String> classesToNotInstrument;
55   private final String classesToNotInstrumentRegex;
56   private final Map<String, String> classNameTranslations;
57   private final Set<MethodRef> interceptedMethods;
58   private final Set<String> classesToNotAcquire;
59   private final Set<String> packagesToNotAcquire;
60   private final Set<String> packagesToNotInstrument;
61   private int cachedHashCode;
62 
63   private final TypeMapper typeMapper;
64   private final Set<MethodRef> methodsToIntercept;
65 
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, String classesToNotInstrumentRegex)66   protected InstrumentationConfiguration(
67       Map<String, String> classNameTranslations,
68       Collection<MethodRef> interceptedMethods,
69       Collection<String> instrumentedPackages,
70       Collection<String> instrumentedClasses,
71       Collection<String> classesToNotAcquire,
72       Collection<String> packagesToNotAquire,
73       Collection<String> classesToNotInstrument,
74       Collection<String> packagesToNotInstrument,
75       String classesToNotInstrumentRegex) {
76     this.classNameTranslations = ImmutableMap.copyOf(classNameTranslations);
77     this.interceptedMethods = ImmutableSet.copyOf(interceptedMethods);
78     this.instrumentedPackages = ImmutableList.copyOf(instrumentedPackages);
79     this.instrumentedClasses = ImmutableSet.copyOf(instrumentedClasses);
80     this.classesToNotAcquire = ImmutableSet.copyOf(classesToNotAcquire);
81     this.packagesToNotAcquire = ImmutableSet.copyOf(packagesToNotAquire);
82     this.classesToNotInstrument = ImmutableSet.copyOf(classesToNotInstrument);
83     this.packagesToNotInstrument = ImmutableSet.copyOf(packagesToNotInstrument);
84     this.classesToNotInstrumentRegex = classesToNotInstrumentRegex;
85     this.cachedHashCode = 0;
86 
87     this.typeMapper = new TypeMapper(classNameTranslations());
88     this.methodsToIntercept = ImmutableSet.copyOf(convertToSlashes(methodsToIntercept()));
89   }
90 
91   /**
92    * Determine if {@link SandboxClassLoader} should instrument a given class.
93    *
94    * @param classDetails The class to check.
95    * @return True if the class should be instrumented.
96    */
shouldInstrument(ClassDetails classDetails)97   public boolean shouldInstrument(ClassDetails classDetails) {
98     return !classDetails.isAnnotation()
99         && !classesToNotInstrument.contains(classDetails.getName())
100         && !isInPackagesToNotInstrument(classDetails.getName())
101         && !classMatchesExclusionRegex(classDetails.getName())
102         && !classDetails.isInstrumented()
103         && !classDetails.hasAnnotation(DoNotInstrument.class)
104         && (isInInstrumentedPackage(classDetails.getName())
105             || instrumentedClasses.contains(classDetails.getName())
106             || classDetails.hasAnnotation(Instrument.class))
107         && !classDetails.hasAnnotation(
108             "org.junit.runner.RunWith"); // Don't instrument test classes.
109   }
110 
classMatchesExclusionRegex(String className)111   private boolean classMatchesExclusionRegex(String className) {
112     return classesToNotInstrumentRegex != null && className.matches(classesToNotInstrumentRegex);
113   }
114 
115   /**
116    * Determine if {@link SandboxClassLoader} should load a given class.
117    *
118    * @param name The fully-qualified class name.
119    * @return True if the class should be loaded.
120    */
shouldAcquire(String name)121   public boolean shouldAcquire(String name) {
122     if (CLASSES_TO_ALWAYS_ACQUIRE.contains(name)) {
123       return true;
124     }
125 
126     if (name.equals("java.util.jar.StrictJarFile")) {
127       return true;
128     }
129 
130     // android.R and com.android.internal.R classes must be loaded from the framework jar
131     if (name.matches("(android|com\\.android\\.internal)\\.R(\\$.+)?")) {
132       return true;
133     }
134 
135     // Hack. Fixes https://github.com/robolectric/robolectric/issues/1864
136     if (name.equals("javax.net.ssl.DistinguishedNameParser")
137         || name.equals("javax.microedition.khronos.opengles.GL")) {
138       return true;
139     }
140 
141     for (String packageName : PACKAGES_TO_NEVER_ACQUIRE) {
142       if (name.startsWith(packageName)) {
143         return false;
144       }
145     }
146 
147     for (String packageName : packagesToNotAcquire) {
148       if (name.startsWith(packageName)) {
149         return false;
150       }
151     }
152     return !classesToNotAcquire.contains(name);
153   }
154 
155   /**
156    * Determine if {@link SandboxClassLoader} should load a given resource.
157    *
158    * @param name The fully-qualified resource name.
159    * @return True if the resource should be loaded.
160    */
shouldAcquireResource(String name)161   public boolean shouldAcquireResource(String name) {
162     if (name.contains("android_runtime")) {
163       return true;
164     }
165     if (name.contains("icudt75l.dat")) {
166       return true;
167     }
168     return RESOURCES_TO_ALWAYS_ACQUIRE.contains(name);
169   }
170 
methodsToIntercept()171   public Set<MethodRef> methodsToIntercept() {
172     return Collections.unmodifiableSet(interceptedMethods);
173   }
174 
175   /**
176    * Map from a requested class to an alternate stand-in, or not.
177    *
178    * @return Mapping of class name translations.
179    */
classNameTranslations()180   public Map<String, String> classNameTranslations() {
181     return Collections.unmodifiableMap(classNameTranslations);
182   }
183 
isInInstrumentedPackage(String className)184   private boolean isInInstrumentedPackage(String className) {
185     for (String instrumentedPackage : instrumentedPackages) {
186       if (className.startsWith(instrumentedPackage)) {
187         return true;
188       }
189     }
190     return false;
191   }
192 
isInPackagesToNotInstrument(String className)193   private boolean isInPackagesToNotInstrument(String className) {
194     for (String notInstrumentedPackage : packagesToNotInstrument) {
195       if (className.startsWith(notInstrumentedPackage)) {
196         return true;
197       }
198     }
199     return false;
200   }
201 
202   @Override
equals(Object o)203   public boolean equals(Object o) {
204     if (this == o) return true;
205     if (!(o instanceof InstrumentationConfiguration)) return false;
206 
207     InstrumentationConfiguration that = (InstrumentationConfiguration) o;
208 
209     if (!classNameTranslations.equals(that.classNameTranslations)) return false;
210     if (!classesToNotAcquire.equals(that.classesToNotAcquire)) return false;
211     if (!instrumentedPackages.equals(that.instrumentedPackages)) return false;
212     if (!instrumentedClasses.equals(that.instrumentedClasses)) return false;
213     if (!interceptedMethods.equals(that.interceptedMethods)) return false;
214 
215     return true;
216   }
217 
218   @Override
hashCode()219   public int hashCode() {
220     if (cachedHashCode != 0) {
221       return cachedHashCode;
222     }
223 
224     int result = instrumentedPackages.hashCode();
225     result = 31 * result + instrumentedClasses.hashCode();
226     result = 31 * result + classNameTranslations.hashCode();
227     result = 31 * result + interceptedMethods.hashCode();
228     result = 31 * result + classesToNotAcquire.hashCode();
229     cachedHashCode = result;
230     return result;
231   }
232 
remapParamType(String desc)233   public String remapParamType(String desc) {
234     return typeMapper.remapParamType(desc);
235   }
236 
remapParams(String desc)237   public String remapParams(String desc) {
238     return typeMapper.remapParams(desc);
239   }
240 
mappedTypeName(String internalName)241   public String mappedTypeName(String internalName) {
242     return typeMapper.mappedTypeName(internalName);
243   }
244 
shouldIntercept(MethodInsnNode targetMethod)245   boolean shouldIntercept(MethodInsnNode targetMethod) {
246     if (targetMethod.name.equals("<init>")) {
247       return false; // sorry, can't strip out calls to super() in constructor
248     }
249     return methodsToIntercept.contains(new MethodRef(targetMethod.owner, targetMethod.name))
250         || methodsToIntercept.contains(new MethodRef(targetMethod.owner, "*"));
251   }
252 
convertToSlashes(Set<MethodRef> methodRefs)253   private static Set<MethodRef> convertToSlashes(Set<MethodRef> methodRefs) {
254     HashSet<MethodRef> transformed = new HashSet<>();
255     for (MethodRef methodRef : methodRefs) {
256       transformed.add(new MethodRef(internalize(methodRef.className), methodRef.methodName));
257     }
258     return transformed;
259   }
260 
internalize(String className)261   private static String internalize(String className) {
262     return className.replace('.', '/');
263   }
264 
265   public static final class Builder {
266     public final Collection<String> instrumentedPackages = new HashSet<>();
267     public final Collection<MethodRef> interceptedMethods = new HashSet<>();
268     public final Map<String, String> classNameTranslations = new HashMap<>();
269     public final Collection<String> classesToNotAcquire = new HashSet<>();
270     public final Collection<String> packagesToNotAcquire = new HashSet<>();
271     public final Collection<String> instrumentedClasses = new HashSet<>();
272     public final Collection<String> classesToNotInstrument = new HashSet<>();
273     public final Collection<String> packagesToNotInstrument = new HashSet<>();
274     public String classesToNotInstrumentRegex;
275 
Builder()276     public Builder() {}
277 
Builder(InstrumentationConfiguration classLoaderConfig)278     public Builder(InstrumentationConfiguration classLoaderConfig) {
279       instrumentedPackages.addAll(classLoaderConfig.instrumentedPackages);
280       interceptedMethods.addAll(classLoaderConfig.interceptedMethods);
281       classNameTranslations.putAll(classLoaderConfig.classNameTranslations);
282       classesToNotAcquire.addAll(classLoaderConfig.classesToNotAcquire);
283       packagesToNotAcquire.addAll(classLoaderConfig.packagesToNotAcquire);
284       instrumentedClasses.addAll(classLoaderConfig.instrumentedClasses);
285       classesToNotInstrument.addAll(classLoaderConfig.classesToNotInstrument);
286       packagesToNotInstrument.addAll(classLoaderConfig.packagesToNotInstrument);
287       classesToNotInstrumentRegex = classLoaderConfig.classesToNotInstrumentRegex;
288     }
289 
doNotAcquireClass(Class<?> clazz)290     public Builder doNotAcquireClass(Class<?> clazz) {
291       doNotAcquireClass(clazz.getName());
292       return this;
293     }
294 
doNotAcquireClass(String className)295     public Builder doNotAcquireClass(String className) {
296       this.classesToNotAcquire.add(className);
297       return this;
298     }
299 
doNotAcquirePackage(String packageName)300     public Builder doNotAcquirePackage(String packageName) {
301       this.packagesToNotAcquire.add(packageName);
302       return this;
303     }
304 
addClassNameTranslation(String fromName, String toName)305     public Builder addClassNameTranslation(String fromName, String toName) {
306       classNameTranslations.put(fromName, toName);
307       return this;
308     }
309 
addInterceptedMethod(MethodRef methodReference)310     public Builder addInterceptedMethod(MethodRef methodReference) {
311       interceptedMethods.add(methodReference);
312       return this;
313     }
314 
addInstrumentedClass(String name)315     public Builder addInstrumentedClass(String name) {
316       instrumentedClasses.add(name);
317       return this;
318     }
319 
addInstrumentedPackage(String packageName)320     public Builder addInstrumentedPackage(String packageName) {
321       instrumentedPackages.add(packageName);
322       return this;
323     }
324 
doNotInstrumentClass(String className)325     public Builder doNotInstrumentClass(String className) {
326       this.classesToNotInstrument.add(className);
327       return this;
328     }
329 
doNotInstrumentPackage(String packageName)330     public Builder doNotInstrumentPackage(String packageName) {
331       this.packagesToNotInstrument.add(packageName);
332       return this;
333     }
334 
setDoNotInstrumentClassRegex(String classNameRegex)335     public Builder setDoNotInstrumentClassRegex(String classNameRegex) {
336       this.classesToNotInstrumentRegex = classNameRegex;
337       return this;
338     }
339 
build()340     public InstrumentationConfiguration build() {
341       // Remove redundant packages, e.g. remove 'android.os' if 'android.' is present.
342       List<String> minimalPackages = new ArrayList<>(instrumentedPackages);
343       if (!instrumentedPackages.isEmpty()) {
344         Collections.sort(minimalPackages);
345         Iterator<String> iterator = minimalPackages.iterator();
346         String cur = iterator.next();
347         while (iterator.hasNext()) {
348           String element = iterator.next();
349           if (element.startsWith(cur)) {
350             iterator.remove();
351           } else {
352             cur = element;
353           }
354         }
355       }
356       // Remove redundant classes that are already specified by a package. We do this to avoid
357       // unnecessarily creating sandboxes if a class is specified to be instrumented via
358       // '@Config(shadows=...)'.
359       List<String> minimalClasses =
360           instrumentedClasses.stream()
361               .filter(className -> minimalPackages.stream().noneMatch(className::startsWith))
362               .collect(Collectors.toList());
363 
364       return new InstrumentationConfiguration(
365           classNameTranslations,
366           interceptedMethods,
367           minimalPackages,
368           minimalClasses,
369           classesToNotAcquire,
370           packagesToNotAcquire,
371           classesToNotInstrument,
372           packagesToNotInstrument,
373           classesToNotInstrumentRegex);
374     }
375   }
376 }
377