View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.jexl3.internal.introspection;
19  
20  import java.util.LinkedHashSet;
21  import java.util.Map;
22  import java.util.Set;
23  import java.util.concurrent.ConcurrentHashMap;
24  
25  /**
26   * A crude parser to configure permissions akin to NoJexl annotations.
27   * The syntax recognizes 2 types of permissions:
28   * <ul>
29   * <li>restricting access to packages, classes (and inner classes), methods and fields</li>
30   * <li>allowing access to a wildcard restricted set of packages</li>
31   * </ul>
32   * <p>
33   *  Example:
34   * </p>
35   * <pre>
36   *  my.allowed.packages.*
37   *  another.allowed.package.*
38   *  # nojexl like restrictions
39   *  my.package {
40   *   class0 {...
41   *     class1 {...}
42   *     class2 {
43   *        ...
44   *         class3 {}
45   *     }
46   *     # and eol comment
47   *     class0(); # constructors
48   *     method(); # method is not allowed
49   *     field; # field
50   *   } # end class0
51   *   +class1 {
52   *     method(); // only allowed method of class1
53   *   }
54   * } # end package my.package
55   * </pre>
56   */
57  public class PermissionsParser {
58  
59      /** The source. */
60      private String src;
61  
62      /** The source size. */
63      private int size;
64  
65      /** The @NoJexl execution-time map. */
66      private Map<String, Permissions.NoJexlPackage> packages;
67  
68      /** The set of wildcard imports. */
69      private Set<String> wildcards;
70  
71      /**
72       * Basic ctor.
73       */
74      public PermissionsParser() {
75          // nothing besides default member initialization
76      }
77  
78      /**
79       * Clears this parser internals.
80       */
81      private void clear() {
82          src = null; size = 0; packages = null; wildcards = null;
83      }
84  
85      /**
86       * Parses permissions from a source.
87       *
88       * @param wildcards the set of allowed packages
89       * @param packages the map of restricted elements
90       * @param srcs the sources
91       * @return the permissions map
92       */
93      synchronized Permissions parse(final Set<String> wildcards, final Map<String, Permissions.NoJexlPackage> packages,
94              final String... srcs) {
95          try {
96              if (srcs == null || srcs.length == 0) {
97                  return Permissions.UNRESTRICTED;
98              }
99              this.packages = packages;
100             this.wildcards = wildcards;
101             for (final String source : srcs) {
102                 this.src = source;
103                 this.size = source.length();
104                 readPackages();
105             }
106             return new Permissions(wildcards, packages);
107         } finally {
108             clear();
109         }
110     }
111 
112     /**
113      * Parses permissions from a source.
114      *
115      * @param srcs the sources
116      * @return the permissions map
117      */
118     public Permissions parse(final String... srcs) {
119         return parse(new LinkedHashSet<>(), new ConcurrentHashMap<>(), srcs);
120     }
121 
122     /**
123      * Reads a class permission.
124      *
125      * @param njpackage the owning package
126      * @param nojexl whether the restriction is explicitly denying (true) or allowing (false) members
127      * @param outer the outer class (if any)
128      * @param inner the inner class name (if any)
129      * @param offset the initial parsing position in the source
130      * @return the new parsing position
131      */
132     private int readClass(final Permissions.NoJexlPackage njpackage, final boolean nojexl, final String outer, final String inner, final int offset) {
133         final StringBuilder temp = new StringBuilder();
134         Permissions.NoJexlClass njclass = null;
135         String njname = null;
136         String identifier = inner;
137         boolean deny = nojexl;
138         int i = offset;
139         int j = -1;
140         boolean isMethod = false;
141         while(i < size) {
142             final char c = src.charAt(i);
143             // if no parsing progress can be made, we are in error
144             if (j >= i) {
145                 throw new IllegalStateException(unexpected(c, i));
146             }
147             j = i;
148             // get rid of space
149             if (Character.isWhitespace(c)) {
150                 i = readSpaces(i + 1);
151                 continue;
152             }
153             // eol comment
154             if (c == '#') {
155                 i = readEol(i + 1);
156                 continue;
157             }
158             // end of class ?
159             if (njclass != null && c == '}') {
160                 i += 1;
161                 break;
162             }
163             // read an identifier, the class name
164             if (identifier == null) {
165                 // negative or positive set ?
166                 if (c == '-') {
167                     i += 1;
168                 } else if (c == '+') {
169                     deny = false;
170                     i += 1;
171                 }
172                 final int next = readIdentifier(temp, i);
173                 if (i != next) {
174                     identifier = temp.toString();
175                     temp.setLength(0);
176                     i = next;
177                     continue;
178                 }
179             }
180             // parse a class:
181             if (njclass == null) {
182                 // we must have read the class ('identifier {'...)
183                 if (identifier == null || c != '{') {
184                     throw new IllegalStateException(unexpected(c, i));
185                 }
186                 // if we have a class, it has a name
187                 njclass = deny ? new Permissions.NoJexlClass() : new Permissions.JexlClass();
188                 njname = outer != null ? outer + "$" + identifier : identifier;
189                 njpackage.addNoJexl(njname, njclass);
190                 identifier = null;
191             } else if (identifier != null)  {
192                 // class member mode
193                 if (c == '{') {
194                     // inner class
195                     i = readClass(njpackage, deny, njname, identifier, i - 1);
196                     identifier = null;
197                     continue;
198                 }
199                 if (c == ';') {
200                     // field or method?
201                     if (isMethod) {
202                         njclass.methodNames.add(identifier);
203                         isMethod = false;
204                     } else {
205                         njclass.fieldNames.add(identifier);
206                     }
207                     identifier = null;
208                 } else if (c == '(' && !isMethod) {
209                     // method; only one opening parenthesis allowed
210                     isMethod = true;
211                 } else if (c != ')' || src.charAt(i - 1) != '(') {
212                     // closing parenthesis following opening one was expected
213                     throw new IllegalStateException(unexpected(c, i));
214                 }
215             }
216             i += 1;
217         }
218         // empty class means allow or deny all
219         if (njname != null && njclass.isEmpty()) {
220             njpackage.addNoJexl(njname, njclass instanceof Permissions.JexlClass
221                 ? Permissions.JEXL_CLASS
222                 : Permissions.NOJEXL_CLASS);
223 
224         }
225         return i;
226     }
227 
228     /**
229      * Reads a comment till end-of-line.
230      *
231      * @param offset initial position
232      * @return position after comment
233      */
234     private int readEol(final int offset) {
235         int i = offset;
236         while (i < size) {
237             final char c = src.charAt(i);
238             if (c == '\n') {
239                 break;
240             }
241             i += 1;
242         }
243         return i;
244     }
245 
246     /**
247      * Reads an identifier (optionally dot-separated).
248      *
249      * @param id the builder to fill the identifier character with
250      * @param offset the initial reading position
251      * @return the position after the identifier
252      */
253     private int readIdentifier(final StringBuilder id, final int offset) {
254         return readIdentifier(id, offset, false, false);
255     }
256 
257     /**
258      * Reads an identifier (optionally dot-separated).
259      *
260      * @param id the builder to fill the identifier character with
261      * @param offset the initial reading position
262      * @param dot whether dots (.) are allowed
263      * @param star whether stars (*) are allowed
264      * @return the position after the identifier
265      */
266     private int readIdentifier(final StringBuilder id, final int offset, final boolean dot, final boolean star) {
267         int begin = -1;
268         boolean starf = star;
269         int i = offset;
270         char c = 0;
271         while (i < size) {
272             c = src.charAt(i);
273             // accumulate identifier characters
274             if (Character.isJavaIdentifierStart(c) && begin < 0) {
275                 begin = i;
276                 id.append(c);
277             } else if (Character.isJavaIdentifierPart(c) && begin >= 0) {
278                 id.append(c);
279             } else if (dot && c == '.') {
280                 if (src.charAt(i - 1) == '.') {
281                     throw new IllegalStateException(unexpected(c, i));
282                 }
283                 id.append('.');
284                 begin = -1;
285             } else if (starf && c == '*') {
286                 id.append('*');
287                 starf = false; // only one star
288             } else {
289                 break;
290             }
291             i += 1;
292         }
293         // cant end with a dot
294         if (dot && c == '.') {
295             throw new IllegalStateException(unexpected(c, i));
296         }
297         return i;
298     }
299 
300     /**
301      * Reads a package permission.
302      */
303     private void readPackages() {
304         final StringBuilder temp = new StringBuilder();
305         Permissions.NoJexlPackage njpackage = null;
306         int i = 0;
307         int j = -1;
308         String pname = null;
309         while (i < size) {
310             final char c = src.charAt(i);
311             // if no parsing progress can be made, we are in error
312             if (j >= i) {
313                 throw new IllegalStateException(unexpected(c, i));
314             }
315             j = i;
316             // get rid of space
317             if (Character.isWhitespace(c)) {
318                 i = readSpaces(i + 1);
319                 continue;
320             }
321             // eol comment
322             if (c == '#') {
323                 i = readEol(i + 1);
324                 continue;
325             }
326             // read the package qualified name
327             if (pname == null) {
328                 final int next = readIdentifier(temp, i, true, true);
329                 if (i != next) {
330                     pname = temp.toString();
331                     temp.setLength(0);
332                     i = next;
333                     // consume it if it is a wildcard declaration
334                     if (pname.endsWith(".*")) {
335                         wildcards.add(pname);
336                         pname = null;
337                     }
338                     continue;
339                 }
340             }
341             // package mode
342             if (njpackage == null) {
343                 if (c == '{') {
344                     njpackage = packages.compute(pname,
345                         (n, p) -> new Permissions.NoJexlPackage(p == null? null : p.nojexl)
346                     );
347                     i += 1;
348                 }
349             } else if (c == '}') {
350                 // empty means whole package
351                 if (njpackage.isEmpty()) {
352                     packages.put(pname, Permissions.NOJEXL_PACKAGE);
353                 }
354                 njpackage = null; // can restart anew
355                 pname = null;
356                 i += 1;
357             } else {
358                 i = readClass(njpackage, true,null, null, i);
359             }
360         }
361     }
362 
363     /**
364      * Reads spaces.
365      *
366      * @param offset initial position
367      * @return position after spaces
368      */
369     private int readSpaces(final int offset) {
370         int i = offset;
371         while (i < size) {
372             final char c = src.charAt(i);
373             if (!Character.isWhitespace(c)) {
374                 break;
375             }
376             i += 1;
377         }
378         return offset;
379     }
380 
381     /**
382      * Compose a parsing error message.
383      *
384      * @param c the offending character
385      * @param i the offset position
386      * @return the error message
387      */
388     private String unexpected(final char c, final int i) {
389         return "unexpected '" + c + "'@" + i;
390     }
391 }