1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.hoststubgen.hosthelper;
17 
18 import java.io.PrintStream;
19 import java.lang.StackWalker.Option;
20 import java.lang.reflect.Method;
21 import java.lang.reflect.Modifier;
22 import java.util.HashMap;
23 
24 import javax.annotation.concurrent.GuardedBy;
25 
26 /**
27  * Utilities used in the host side test environment.
28  */
29 public class HostTestUtils {
HostTestUtils()30     private HostTestUtils() {
31     }
32 
33     /**
34      * Same as ASM's Type.getInternalName(). Copied here, to avoid having a reference to ASM
35      * in this JAR.
36      */
getInternalName(final Class<?> clazz)37     public static String getInternalName(final Class<?> clazz) {
38         return clazz.getName().replace('.', '/');
39     }
40 
41     public static final String CLASS_INTERNAL_NAME = getInternalName(HostTestUtils.class);
42 
43     /** If true, we won't print method call log. */
44     private static final boolean SKIP_METHOD_LOG = "1".equals(System.getenv(
45             "HOSTTEST_SKIP_METHOD_LOG"));
46 
47     /** If true, we won't print class load log. */
48     private static final boolean SKIP_CLASS_LOG = "1".equals(System.getenv(
49             "HOSTTEST_SKIP_CLASS_LOG"));
50 
51     /** If true, we won't perform non-stub method direct call check. */
52     private static final boolean SKIP_NON_STUB_METHOD_CHECK = "1".equals(System.getenv(
53             "HOSTTEST_SKIP_NON_STUB_METHOD_CHECK"));
54 
55 
56     /**
57      * Method call log will be printed to it.
58      */
59     public static PrintStream logPrintStream = System.out;
60 
61     /**
62      * Called from methods with FilterPolicy.Throw.
63      */
onThrowMethodCalled()64     public static void onThrowMethodCalled() {
65         // TODO: Maybe add call tracking?
66         throw new RuntimeException(
67                 "This method is not yet supported under the Ravenwood deviceless testing "
68                         + "environment; consider requesting support from the API owner or "
69                         + "consider using Mockito; more details at go/ravenwood-docs");
70     }
71 
72     /**
73      * Trampoline method for method-call-hook.
74      */
callMethodCallHook( Class<?> methodClass, String methodName, String methodDescriptor, String callbackMethod )75     public static void callMethodCallHook(
76             Class<?> methodClass,
77             String methodName,
78             String methodDescriptor,
79             String callbackMethod
80     ) {
81         callStaticMethodByName(callbackMethod, "method call hook", methodClass,
82                 methodName, methodDescriptor);
83     }
84 
85     /**
86      * I can be used as
87      * {@code --default-method-call-hook
88      * com.android.hoststubgen.hosthelper.HostTestUtils.logMethodCall}.
89      *
90      * It logs every single methods called.
91      */
logMethodCall( Class<?> methodClass, String methodName, String methodDescriptor )92     public static void logMethodCall(
93             Class<?> methodClass,
94             String methodName,
95             String methodDescriptor
96     ) {
97         if (SKIP_METHOD_LOG) {
98             return;
99         }
100         logPrintStream.println("# method called: " + methodClass.getCanonicalName() + "."
101                 + methodName + methodDescriptor);
102     }
103 
104     private static final StackWalker sStackWalker =
105             StackWalker.getInstance(Option.RETAIN_CLASS_REFERENCE);
106 
107     /**
108      * Return a {@link StackWalker} that supports {@link StackWalker#getCallerClass()}.
109      */
getStackWalker()110     public static StackWalker getStackWalker() {
111         return sStackWalker;
112     }
113 
114     /**
115      * Cache used by {@link #isClassAllowedToCallNonStubMethods}.
116      */
117     @GuardedBy("sAllowedClasses")
118     private static final HashMap<Class, Boolean> sAllowedClasses = new HashMap();
119 
120     /**
121      * Return true if a given class is allowed to access non-stub methods -- that is, if the class
122      * is in the hoststubgen generated JARs. (not in the test jar.)
123      */
isClassAllowedToCallNonStubMethods(Class<?> clazz)124     private static boolean isClassAllowedToCallNonStubMethods(Class<?> clazz) {
125         synchronized (sAllowedClasses) {
126             var cached = sAllowedClasses.get(clazz);
127             if (cached != null) {
128                 return cached;
129             }
130         }
131         // All processed classes have this annotation.
132         var allowed = clazz.getAnnotation(HostStubGenKeptInImpl.class) != null;
133 
134         // Java classes should be able to access any methods. (via callbacks, etc.)
135         if (!allowed) {
136             if (clazz.getPackageName().startsWith("java.")
137                     || clazz.getPackageName().startsWith("javax.")) {
138                 allowed = true;
139             }
140         }
141         synchronized (sAllowedClasses) {
142             sAllowedClasses.put(clazz, allowed);
143         }
144         return allowed;
145     }
146 
147     /**
148      * Called when non-stub methods are called. We do a host-unsupported method direct call check
149      * in here.
150      */
onNonStubMethodCalled( String methodClass, String methodName, String methodDescriptor, Class<?> callerClass)151     public static void onNonStubMethodCalled(
152             String methodClass,
153             String methodName,
154             String methodDescriptor,
155             Class<?> callerClass) {
156         if (SKIP_NON_STUB_METHOD_CHECK) {
157             return;
158         }
159         if (isClassAllowedToCallNonStubMethods(callerClass)) {
160             return; // Generated class is allowed to call framework class.
161         }
162         logPrintStream.println("! " + methodClass + "." + methodName + methodDescriptor
163                 + " called by " + callerClass.getCanonicalName());
164     }
165 
166     /**
167      * Called when any top level class (not nested classes) in the impl jar is loaded.
168      *
169      * When HostStubGen inject a class-load hook, it's always a call to this method, with the
170      * actual method name as the second argument.
171      *
172      * This method discovers the hook method with reflections and call it.
173      *
174      * TODO: Add a unit test.
175      */
onClassLoaded(Class<?> loadedClass, String callbackMethod)176     public static void onClassLoaded(Class<?> loadedClass, String callbackMethod) {
177         logPrintStream.println("! Class loaded: " + loadedClass.getCanonicalName()
178                 + " calling hook " + callbackMethod);
179 
180         callStaticMethodByName(callbackMethod, "class load hook", loadedClass);
181     }
182 
callStaticMethodByName(String classAndMethodName, String description, Object... args)183     private static void callStaticMethodByName(String classAndMethodName,
184             String description, Object... args) {
185         // Forward the call to callbackMethod.
186         final int lastPeriod = classAndMethodName.lastIndexOf(".");
187 
188         if ((lastPeriod) < 0 || (lastPeriod == classAndMethodName.length() - 1)) {
189             throw new HostTestException(String.format(
190                     "Unable to find %s: malformed method name \"%s\"",
191                     description,
192                     classAndMethodName));
193         }
194 
195         final String className = classAndMethodName.substring(0, lastPeriod);
196         final String methodName = classAndMethodName.substring(lastPeriod + 1);
197 
198         Class<?> clazz = null;
199         try {
200             clazz = Class.forName(className);
201         } catch (Exception e) {
202             throw new HostTestException(String.format(
203                     "Unable to find %s: Class %s not found",
204                     description,
205                     className), e);
206         }
207         if (!Modifier.isPublic(clazz.getModifiers())) {
208             throw new HostTestException(String.format(
209                     "Unable to find %s: Class %s must be public",
210                     description,
211                     className));
212         }
213 
214         Class<?>[] argTypes = new Class[args.length];
215         for (int i = 0; i < args.length; i++) {
216             argTypes[i] = args[i].getClass();
217         }
218 
219         Method method = null;
220         try {
221             method = clazz.getMethod(methodName, argTypes);
222         } catch (Exception e) {
223             throw new HostTestException(String.format(
224                     "Unable to find %s: class %s doesn't have method %s"
225                             + " (method must take exactly one parameter of type Class,"
226                             + " and public static)",
227                     description, className, methodName), e);
228         }
229         if (!(Modifier.isPublic(method.getModifiers())
230                 && Modifier.isStatic(method.getModifiers()))) {
231             throw new HostTestException(String.format(
232                     "Unable to find %s: Method %s in class %s must be public static",
233                     description, methodName, className));
234         }
235         try {
236             method.invoke(null, args);
237         } catch (Exception e) {
238             throw new HostTestException(String.format(
239                     "Unable to invoke %s %s.%s",
240                     description, className, methodName), e);
241         }
242     }
243 
244     /**
245      * I can be used as
246      * {@code --default-class-load-hook
247      * com.android.hoststubgen.hosthelper.HostTestUtils.logClassLoaded}.
248      *
249      * It logs every loaded class.
250      */
logClassLoaded(Class<?> clazz)251     public static void logClassLoaded(Class<?> clazz) {
252         if (SKIP_CLASS_LOG) {
253             return;
254         }
255         logPrintStream.println("# class loaded: " + clazz.getCanonicalName());
256     }
257 }
258