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