1 /*
2  * Copyright (c) 2012, 2018, Oracle and/or its affiliates. All rights reserved.
3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4  *
5  * This code is free software; you can redistribute it and/or modify it
6  * under the terms of the GNU General Public License version 2 only, as
7  * published by the Free Software Foundation.
8  *
9  * This code is distributed in the hope that it will be useful, but WITHOUT
10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
12  * version 2 for more details (a copy is included in the LICENSE file that
13  * accompanied this code).
14  *
15  * You should have received a copy of the GNU General Public License version
16  * 2 along with this work; if not, write to the Free Software Foundation,
17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18  *
19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20  * or visit www.oracle.com if you need additional information or have any
21  * questions.
22  */
23 
24 /* @test
25  * @summary test access checking by java.lang.invoke.MethodHandles.Lookup
26  * @compile AccessControlTest.java AccessControlTest_subpkg/Acquaintance_remote.java
27  * @run testng/othervm test.java.lang.invoke.AccessControlTest
28  */
29 
30 package test.java.lang.invoke;
31 
32 import java.lang.invoke.*;
33 import java.lang.reflect.*;
34 import java.lang.reflect.Modifier;
35 import java.util.*;
36 import org.testng.*;
37 import org.testng.annotations.*;
38 
39 import static java.lang.invoke.MethodHandles.*;
40 import static java.lang.invoke.MethodHandles.Lookup.*;
41 import static java.lang.invoke.MethodType.*;
42 import static org.testng.Assert.*;
43 
44 import test.java.lang.invoke.AccessControlTest_subpkg.Acquaintance_remote;
45 
46 
47 /**
48  * Test many combinations of Lookup access and cross-class lookupStatic.
49  * @author jrose
50  */
51 public class AccessControlTest {
52     static final Class<?> THIS_CLASS = AccessControlTest.class;
53     // How much output?
54     static int verbosity = 0;
55     static {
56         String vstr = System.getProperty(THIS_CLASS.getSimpleName()+".verbosity");
57         if (vstr == null)
58             vstr = System.getProperty(THIS_CLASS.getName()+".verbosity");
59         if (vstr != null)  verbosity = Integer.parseInt(vstr);
60     }
61 
62     private class LookupCase implements Comparable<LookupCase> {
63         final Lookup   lookup;
64         final Class<?> lookupClass;
65         final int      lookupModes;
LookupCase(Lookup lookup)66         public LookupCase(Lookup lookup) {
67             this.lookup = lookup;
68             this.lookupClass = lookup.lookupClass();
69             this.lookupModes = lookup.lookupModes();
70             assert(lookupString().equals(lookup.toString()));
71             numberOf(lookupClass().getClassLoader()); // assign CL#
72         }
LookupCase(Class<?> lookupClass, int lookupModes)73         public LookupCase(Class<?> lookupClass, int lookupModes) {
74             this.lookup = null;
75             this.lookupClass = lookupClass;
76             this.lookupModes = lookupModes;
77             numberOf(lookupClass().getClassLoader()); // assign CL#
78         }
79 
lookupClass()80         public final Class<?> lookupClass() { return lookupClass; }
lookupModes()81         public final int      lookupModes() { return lookupModes; }
82 
lookup()83         public Lookup lookup() { lookup.getClass(); return lookup; }
84 
85         @Override
compareTo(LookupCase that)86         public int compareTo(LookupCase that) {
87             Class<?> c1 = this.lookupClass();
88             Class<?> c2 = that.lookupClass();
89             if (c1 != c2) {
90                 int cmp = c1.getName().compareTo(c2.getName());
91                 if (cmp != 0)  return cmp;
92                 cmp = numberOf(c1.getClassLoader()) - numberOf(c2.getClassLoader());
93                 assert(cmp != 0);
94                 return cmp;
95             }
96             return -(this.lookupModes() - that.lookupModes());
97         }
98 
99         @Override
equals(Object that)100         public boolean equals(Object that) {
101             return (that instanceof LookupCase && equals((LookupCase)that));
102         }
equals(LookupCase that)103         public boolean equals(LookupCase that) {
104             return (this.lookupClass() == that.lookupClass() &&
105                     this.lookupModes() == that.lookupModes());
106         }
107 
108         @Override
hashCode()109         public int hashCode() {
110             return lookupClass().hashCode() + (lookupModes() * 31);
111         }
112 
113         /** Simulate all assertions in the spec. for Lookup.toString. */
lookupString()114         private String lookupString() {
115             String name = lookupClass.getName();
116             String suffix = "";
117             if (lookupModes == 0)
118                 suffix = "/noaccess";
119             else if (lookupModes == PUBLIC)
120                 suffix = "/public";
121              else if (lookupModes == (PUBLIC|UNCONDITIONAL))
122                 suffix = "/publicLookup";
123             else if (lookupModes == (PUBLIC|MODULE))
124                 suffix = "/module";
125             else if (lookupModes == (PUBLIC|MODULE|PACKAGE))
126                 suffix = "/package";
127             else if (lookupModes == (PUBLIC|MODULE|PACKAGE|PRIVATE))
128                 suffix = "/private";
129             else if (lookupModes == (PUBLIC|MODULE|PACKAGE|PRIVATE|PROTECTED))
130                 suffix = "";
131             else
132                 suffix = "/#"+Integer.toHexString(lookupModes);
133             return name+suffix;
134         }
135 
136         /** Simulate all assertions from the spec. for Lookup.in:
137          * <hr>
138          * Creates a lookup on the specified new lookup class.
139          * [A1] The resulting object will report the specified
140          * class as its own {@link #lookupClass lookupClass}.
141          * <p>
142          * [A2] However, the resulting {@code Lookup} object is guaranteed
143          * to have no more access capabilities than the original.
144          * In particular, access capabilities can be lost as follows:<ul>
145          * <li> [A3] If the old lookup class is in a named module, and the new
146          * lookup class is in a different module {@code M}, then no members, not
147          * even public members in {@code M}'s exported packages, will be accessible.
148          * The exception to this is when this lookup is publicLookup, in which case
149          * public access is not lost.
150          * <li> [A4] If the old lookup class is in an unnamed module, and the new
151          * lookup class is a different module then module access is lost.
152          * <li> [A5] If the new lookup class differs from the old one then UNCONDITIONAL
153          * is lost. If the new lookup class is not within the same package member as the
154          * old one, protected members will not be accessible by virtue of inheritance.
155          * (Protected members may continue to be accessible because of package sharing.)
156          * <li> [A6] If the new lookup class is in a different package than the old one,
157          * protected and default (package) members will not be accessible.
158          * <li> [A7] If the new lookup class is not within the same package member
159          * as the old one, private members will not be accessible.
160          * <li> [A8] If the new lookup class is not accessible to the old lookup class,
161          * then no members, not even public members, will be accessible.
162          * <li> [A9] (In all other cases, public members will continue to be accessible.)
163          * </ul>
164          * Other than the above cases, the new lookup will have the same
165          * access capabilities as the original. [A10]
166          * <hr>
167          */
in(Class<?> c2)168         public LookupCase in(Class<?> c2) {
169             Class<?> c1 = lookupClass();
170             int m1 = lookupModes();
171             int changed = 0;
172             // for the purposes of access control then treat classes in different unnamed
173             // modules as being in the same module.
174             boolean sameModule = (c1.getModule() == c2.getModule()) ||
175                                  (!c1.getModule().isNamed() && !c2.getModule().isNamed());
176             boolean samePackage = (c1.getClassLoader() == c2.getClassLoader() &&
177                                    c1.getPackageName().equals(c2.getPackageName()));
178             boolean sameTopLevel = (topLevelClass(c1) == topLevelClass(c2));
179             boolean sameClass = (c1 == c2);
180             assert(samePackage  || !sameTopLevel);
181             assert(sameTopLevel || !sameClass);
182             boolean accessible = sameClass;
183             if ((m1 & PACKAGE) != 0)  accessible |= samePackage;
184             if ((m1 & PUBLIC ) != 0)  accessible |= (c2.getModifiers() & PUBLIC) != 0;
185             if (!sameModule) {
186                 if (c1.getModule().isNamed() && (m1 & UNCONDITIONAL) == 0) {
187                     accessible = false;  // [A3]
188                 } else {
189                     changed |= (MODULE|PACKAGE|PRIVATE|PROTECTED);    // [A3] [A4]
190                 }
191             }
192             if (!accessible) {
193                 // Different package and no access to c2; lose all access.
194                 changed |= (PUBLIC|MODULE|PACKAGE|PRIVATE|PROTECTED);  // [A8]
195             }
196             if (!samePackage) {
197                 // Different package; loose PACKAGE and lower access.
198                 changed |= (PACKAGE|PRIVATE|PROTECTED);  // [A6]
199             }
200             if (!sameTopLevel) {
201                 // Different top-level class.  Lose PRIVATE and PROTECTED access.
202                 changed |= (PRIVATE|PROTECTED);  // [A5] [A7]
203             }
204             if (!sameClass) {
205                 changed |= (UNCONDITIONAL);     // [A5]
206             } else {
207                 assert(changed == 0);       // [A10] (no deprivation if same class)
208             }
209             if (accessible)  assert((changed & PUBLIC) == 0);  // [A9]
210             int m2 = m1 & ~changed;
211             LookupCase l2 = new LookupCase(c2, m2);
212             assert(l2.lookupClass() == c2); // [A1]
213             assert((m1 | m2) == m1);        // [A2] (no elevation of access)
214             return l2;
215         }
216 
217         @Override
toString()218         public String toString() {
219             String s = lookupClass().getSimpleName();
220             String lstr = lookupString();
221             int sl = lstr.indexOf('/');
222             if (sl >= 0)  s += lstr.substring(sl);
223             ClassLoader cld = lookupClass().getClassLoader();
224             if (cld != THIS_LOADER)  s += "/loader#"+numberOf(cld);
225             return s;
226         }
227 
228         /** Predict the success or failure of accessing this method. */
willAccess(Method m)229         public boolean willAccess(Method m) {
230             Class<?> c1 = lookupClass();
231             Class<?> c2 = m.getDeclaringClass();
232 
233             // publicLookup has access to all public types/members of types in unnamed modules
234             if ((lookupModes & UNCONDITIONAL) != 0
235                 && (lookupModes & PUBLIC) != 0
236                 && !c2.getModule().isNamed()
237                 && Modifier.isPublic(c2.getModifiers())
238                 && Modifier.isPublic(m.getModifiers()))
239                 return true;
240 
241             LookupCase lc = this.in(c2);
242             int m1 = lc.lookupModes();
243             int m2 = fixMods(m.getModifiers());
244             // allow private lookup on nestmates. Otherwise, privacy is strictly enforced
245             if (c1 != c2 && ((m2 & PRIVATE) == 0 || !c1.isNestmateOf(c2))) {
246                 m1 &= ~PRIVATE;
247             }
248             // protected access is sometimes allowed
249             if ((m2 & PROTECTED) != 0) {
250                 int prev = m2;
251                 m2 |= PACKAGE;  // it acts like a package method also
252                 if ((lookupModes() & PROTECTED) != 0 &&
253                     c2.isAssignableFrom(c1))
254                     m2 |= PUBLIC;  // from a subclass, it acts like a public method also
255             }
256             if (verbosity >= 2)
257                 System.out.format("%s willAccess %s m1=0x%h m2=0x%h => %s%n", this, lc, m1, m2, ((m2 & m1) != 0));
258             return (m2 & m1) != 0;
259         }
260 
261         /** Predict the success or failure of accessing this class. */
willAccessClass(Class<?> c2, boolean load)262         public boolean willAccessClass(Class<?> c2, boolean load) {
263             Class<?> c1 = lookupClass();
264             if (load && c2.getClassLoader() != null) {
265                 if (c1.getClassLoader() == null) {
266                     // not visible
267                     return false;
268                 }
269             }
270 
271             // publicLookup has access to all public types/members of types in unnamed modules
272             if ((lookupModes & UNCONDITIONAL) != 0
273                 && (lookupModes & PUBLIC) != 0
274                 && (!c2.getModule().isNamed())
275                 && Modifier.isPublic(c2.getModifiers()))
276                 return true;
277 
278             LookupCase lc = this.in(c2);
279             int m1 = lc.lookupModes();
280             boolean r = false;
281             if (m1 == 0) {
282                 r = false;
283             } else {
284                 int m2 = fixMods(c2.getModifiers());
285                 if ((m2 & PUBLIC) != 0) {
286                     r = true;
287                 } else if ((m1 & PACKAGE) != 0 && c1.getPackage() == c2.getPackage()) {
288                     r = true;
289                 }
290             }
291             if (verbosity >= 2) {
292                 System.out.println(this+" willAccessClass "+lc+" c1="+c1+" c2="+c2+" => "+r);
293             }
294             return r;
295         }
296     }
297 
topLevelClass(Class<?> cls)298     private static Class<?> topLevelClass(Class<?> cls) {
299         Class<?> c = cls;
300         for (Class<?> ec; (ec = c.getEnclosingClass()) != null; )
301             c = ec;
302         assert(c.getEnclosingClass() == null);
303         assert(c == cls || cls.getEnclosingClass() != null);
304         return c;
305     }
306 
307     private final TreeSet<LookupCase> CASES = new TreeSet<>();
308     private final TreeMap<LookupCase,TreeSet<LookupCase>> CASE_EDGES = new TreeMap<>();
309     private final ArrayList<ClassLoader> LOADERS = new ArrayList<>();
310     private final ClassLoader THIS_LOADER = this.getClass().getClassLoader();
311     { if (THIS_LOADER != null)  LOADERS.add(THIS_LOADER); }  // #1
312 
lookupCase(String name)313     private LookupCase lookupCase(String name) {
314         for (LookupCase lc : CASES) {
315             if (lc.toString().equals(name))
316                 return lc;
317         }
318         throw new AssertionError(name);
319     }
320 
numberOf(ClassLoader cl)321     private int numberOf(ClassLoader cl) {
322         if (cl == null)  return 0;
323         int i = LOADERS.indexOf(cl);
324         if (i < 0) {
325             i = LOADERS.size();
326             LOADERS.add(cl);
327         }
328         return i+1;
329     }
330 
addLookupEdge(LookupCase l1, Class<?> c2, LookupCase l2)331     private void addLookupEdge(LookupCase l1, Class<?> c2, LookupCase l2) {
332         TreeSet<LookupCase> edges = CASE_EDGES.get(l2);
333         if (edges == null)  CASE_EDGES.put(l2, edges = new TreeSet<>());
334         if (edges.add(l1)) {
335             Class<?> c1 = l1.lookupClass();
336             assert(l2.lookupClass() == c2); // [A1]
337             int m1 = l1.lookupModes();
338             int m2 = l2.lookupModes();
339             assert((m1 | m2) == m1);        // [A2] (no elevation of access)
340             LookupCase expect = l1.in(c2);
341             if (!expect.equals(l2))
342                 System.out.println("*** expect "+l1+" => "+expect+" but got "+l2);
343             assertEquals(l2, expect);
344         }
345     }
346 
makeCases(Lookup[] originalLookups)347     private void makeCases(Lookup[] originalLookups) {
348         // make initial set of lookup test cases
349         CASES.clear(); LOADERS.clear(); CASE_EDGES.clear();
350         ArrayList<Class<?>> classes = new ArrayList<>();
351         for (Lookup l : originalLookups) {
352             CASES.add(new LookupCase(l));
353             classes.remove(l.lookupClass());  // no dups please
354             classes.add(l.lookupClass());
355         }
356         System.out.println("loaders = "+LOADERS);
357         int rounds = 0;
358         for (int lastCount = -1; lastCount != CASES.size(); ) {
359             lastCount = CASES.size();  // if CASES grow in the loop we go round again
360             for (LookupCase lc1 : CASES.toArray(new LookupCase[0])) {
361                 for (Class<?> c2 : classes) {
362                     LookupCase lc2 = new LookupCase(lc1.lookup().in(c2));
363                     addLookupEdge(lc1, c2, lc2);
364                     CASES.add(lc2);
365                 }
366             }
367             rounds++;
368         }
369         System.out.println("filled in "+CASES.size()+" cases from "+originalLookups.length+" original cases in "+rounds+" rounds");
370         if (false) {
371             System.out.println("CASES: {");
372             for (LookupCase lc : CASES) {
373                 System.out.println(lc);
374                 Set<LookupCase> edges = CASE_EDGES.get(lc);
375                 if (edges != null)
376                     for (LookupCase prev : edges) {
377                         System.out.println("\t"+prev);
378                     }
379             }
380             System.out.println("}");
381         }
382     }
383 
test()384     @Test public void test() {
385         makeCases(lookups());
386         if (verbosity > 0) {
387             verbosity += 9;
388             Method pro_in_self = targetMethod(THIS_CLASS, PROTECTED, methodType(void.class));
389             testOneAccess(lookupCase("AccessControlTest/public"),  pro_in_self, "find");
390             testOneAccess(lookupCase("Remote_subclass/public"),    pro_in_self, "find");
391             testOneAccess(lookupCase("Remote_subclass"),           pro_in_self, "find");
392             verbosity -= 9;
393         }
394         Set<Class<?>> targetClassesDone = new HashSet<>();
395         for (LookupCase targetCase : CASES) {
396             Class<?> targetClass = targetCase.lookupClass();
397             if (!targetClassesDone.add(targetClass))  continue;  // already saw this one
398             String targetPlace = placeName(targetClass);
399             if (targetPlace == null)  continue;  // Object, String, not a target
400             for (int targetAccess : ACCESS_CASES) {
401                 MethodType methodType = methodType(void.class);
402                 Method method = targetMethod(targetClass, targetAccess, methodType);
403                 // Try to access target method from various contexts.
404                 for (LookupCase sourceCase : CASES) {
405                     testOneAccess(sourceCase, method, "findClass");
406                     testOneAccess(sourceCase, method, "accessClass");
407                     testOneAccess(sourceCase, method, "find");
408                     testOneAccess(sourceCase, method, "unreflect");
409                 }
410             }
411         }
412         System.out.println("tested "+testCount+" access scenarios; "+testCountFails+" accesses were denied");
413     }
414 
415     private int testCount, testCountFails;
416 
testOneAccess(LookupCase sourceCase, Method method, String kind)417     private void testOneAccess(LookupCase sourceCase, Method method, String kind) {
418         Class<?> targetClass = method.getDeclaringClass();
419         String methodName = method.getName();
420         MethodType methodType = methodType(method.getReturnType(), method.getParameterTypes());
421         boolean isFindOrAccessClass = "findClass".equals(kind) || "accessClass".equals(kind);
422         boolean willAccess = isFindOrAccessClass ?
423                 sourceCase.willAccessClass(targetClass, "findClass".equals(kind)) : sourceCase.willAccess(method);
424         boolean didAccess = false;
425         ReflectiveOperationException accessError = null;
426         try {
427             switch (kind) {
428             case "accessClass":
429                 sourceCase.lookup().accessClass(targetClass);
430                 break;
431             case "findClass":
432                 sourceCase.lookup().findClass(targetClass.getName());
433                 break;
434             case "find":
435                 if ((method.getModifiers() & Modifier.STATIC) != 0)
436                     sourceCase.lookup().findStatic(targetClass, methodName, methodType);
437                 else
438                     sourceCase.lookup().findVirtual(targetClass, methodName, methodType);
439                 break;
440             case "unreflect":
441                 sourceCase.lookup().unreflect(method);
442                 break;
443             default:
444                 throw new AssertionError(kind);
445             }
446             didAccess = true;
447         } catch (ReflectiveOperationException ex) {
448             accessError = ex;
449         }
450         if (willAccess != didAccess) {
451             System.out.println(sourceCase+" => "+targetClass.getSimpleName()+(isFindOrAccessClass?"":"."+methodName+methodType));
452             System.out.println("fail "+(isFindOrAccessClass?kind:"on "+method)+" ex="+accessError);
453             assertEquals(willAccess, didAccess);
454         }
455         testCount++;
456         if (!didAccess)  testCountFails++;
457     }
458 
targetMethod(Class<?> targetClass, int targetAccess, MethodType methodType)459     static Method targetMethod(Class<?> targetClass, int targetAccess, MethodType methodType) {
460         assert targetAccess != MODULE;
461         String methodName = accessName(targetAccess)+placeName(targetClass);
462         if (verbosity >= 2)
463             System.out.println(targetClass.getSimpleName()+"."+methodName+methodType);
464         try {
465             Method method = targetClass.getDeclaredMethod(methodName, methodType.parameterArray());
466             assertEquals(method.getReturnType(), methodType.returnType());
467             int haveMods = method.getModifiers();
468             assert(Modifier.isStatic(haveMods));
469             assert(targetAccess == fixMods(haveMods));
470             return method;
471         } catch (NoSuchMethodException ex) {
472             throw new AssertionError(methodName, ex);
473         }
474     }
475 
placeName(Class<?> cls)476     static String placeName(Class<?> cls) {
477         // return "self", "sibling", "nestmate", etc.
478         if (cls == AccessControlTest.class)  return "self";
479         String cln = cls.getSimpleName();
480         int under = cln.lastIndexOf('_');
481         if (under < 0)  return null;
482         return cln.substring(under+1);
483     }
accessName(int acc)484     static String accessName(int acc) {
485         switch (acc) {
486         case PUBLIC:     return "pub_in_";
487         case PROTECTED:  return "pro_in_";
488         case PACKAGE:    return "pkg_in_";
489         case PRIVATE:    return "pri_in_";
490         }
491         assert(false);
492         return "?";
493     }
494     // MODULE not a test case at this time
495     private static final int[] ACCESS_CASES = {
496         PUBLIC, PACKAGE, PRIVATE, PROTECTED
497     };
498     /** Return one of the ACCESS_CASES. */
fixMods(int mods)499     static int fixMods(int mods) {
500         mods &= (PUBLIC|PRIVATE|PROTECTED);
501         switch (mods) {
502         case PUBLIC: case PRIVATE: case PROTECTED: return mods;
503         case 0:  return PACKAGE;
504         }
505         throw new AssertionError(mods);
506     }
507 
lookups()508     static Lookup[] lookups() {
509         ArrayList<Lookup> tem = new ArrayList<>();
510         Collections.addAll(tem,
511                            AccessControlTest.lookup_in_self(),
512                            Inner_nestmate.lookup_in_nestmate(),
513                            AccessControlTest_sibling.lookup_in_sibling());
514         if (true) {
515             Collections.addAll(tem,Acquaintance_remote.lookups());
516         } else {
517             try {
518                 Class<?> remc = Class.forName("test.java.lang.invoke.AccessControlTest_subpkg.Acquaintance_remote");
519                 Lookup[] remls = (Lookup[]) remc.getMethod("lookups").invoke(null);
520                 Collections.addAll(tem, remls);
521             } catch (ReflectiveOperationException ex) {
522                 throw new LinkageError("reflection failed", ex);
523             }
524         }
525         tem.add(publicLookup());
526         tem.add(publicLookup().in(String.class));
527         tem.add(publicLookup().in(List.class));
528         return tem.toArray(new Lookup[0]);
529     }
530 
lookup_in_self()531     static Lookup lookup_in_self() {
532         return MethodHandles.lookup();
533     }
pub_in_self()534     public static      void pub_in_self() { }
pro_in_self()535     protected static   void pro_in_self() { }
pkg_in_self()536     static /*package*/ void pkg_in_self() { }
pri_in_self()537     private static     void pri_in_self() { }
538 
539     static class Inner_nestmate {
lookup_in_nestmate()540         static Lookup lookup_in_nestmate() {
541             return MethodHandles.lookup();
542         }
pub_in_nestmate()543         public static      void pub_in_nestmate() { }
pro_in_nestmate()544         protected static   void pro_in_nestmate() { }
pkg_in_nestmate()545         static /*package*/ void pkg_in_nestmate() { }
pri_in_nestmate()546         private static     void pri_in_nestmate() { }
547     }
548 }
549 class AccessControlTest_sibling {
lookup_in_sibling()550     static Lookup lookup_in_sibling() {
551         return MethodHandles.lookup();
552     }
pub_in_sibling()553     public static      void pub_in_sibling() { }
pro_in_sibling()554     protected static   void pro_in_sibling() { }
pkg_in_sibling()555     static /*package*/ void pkg_in_sibling() { }
pri_in_sibling()556     private static     void pri_in_sibling() { }
557 }
558 
559 // This guy tests access from outside the package:
560 /*
561 package test.java.lang.invoke.AccessControlTest_subpkg;
562 public class Acquaintance_remote {
563     public static Lookup[] lookups() { ...
564     }
565     ...
566 }
567 */
568