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