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