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    *      http://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      /** The source. */
59      private String src;
60      /** The source size. */
61      private int size;
62      /** The @NoJexl execution-time map. */
63      private Map<String, Permissions.NoJexlPackage> packages;
64      /** The set of wildcard imports. */
65      private Set<String> wildcards;
66  
67      /**
68       * Basic ctor.
69       */
70      public PermissionsParser() {
71          // nothing besides default member initialization
72      }
73  
74      /**
75       * Clears this parser internals.
76       */
77      private void clear() {
78          src = null; size = 0; packages = null; wildcards = null;
79      }
80  
81      /**
82       * Parses permissions from a source.
83       * @param wildcards the set of allowed packages
84       * @param packages the map of restricted elements
85       * @param srcs the sources
86       * @return the permissions map
87       */
88      synchronized Permissions parse(final Set<String> wildcards, final Map<String, Permissions.NoJexlPackage> packages,
89              final String... srcs) {
90          try {
91              if (srcs == null || srcs.length == 0) {
92                  return Permissions.UNRESTRICTED;
93              }
94              this.packages = packages;
95              this.wildcards = wildcards;
96              for (final String source : srcs) {
97                  this.src = source;
98                  this.size = source.length();
99                  readPackages();
100             }
101             return new Permissions(wildcards, packages);
102         } finally {
103             clear();
104         }
105     }
106 
107     /**
108      * Parses permissions from a source.
109      * @param srcs the sources
110      * @return the permissions map
111      */
112     public Permissions parse(final String... srcs) {
113         return parse(new LinkedHashSet<>(), new ConcurrentHashMap<>(), srcs);
114     }
115 
116     /**
117      * Reads a class permission.
118      * @param njpackage the owning package
119      * @param nojexl whether the restriction is explicitly denying (true) or allowing (false) members
120      * @param outer the outer class (if any)
121      * @param inner the inner class name (if any)
122      * @param offset the initial parsing position in the source
123      * @return the new parsing position
124      */
125     private int readClass(final Permissions.NoJexlPackage njpackage, final boolean nojexl, final String outer, final String inner, final int offset) {
126         final StringBuilder temp = new StringBuilder();
127         Permissions.NoJexlClass njclass = null;
128         String njname = null;
129         String identifier = inner;
130         boolean deny = nojexl;
131         int i = offset;
132         int j = -1;
133         boolean isMethod = false;
134         while(i < size) {
135             final char c = src.charAt(i);
136             // if no parsing progress can be made, we are in error
137             if (j >= i) {
138                 throw new IllegalStateException(unexpected(c, i));
139             }
140             j = i;
141             // get rid of space
142             if (Character.isWhitespace(c)) {
143                 i = readSpaces(i + 1);
144                 continue;
145             }
146             // eol comment
147             if (c == '#') {
148                 i = readEol(i + 1);
149                 continue;
150             }
151             // end of class ?
152             if (njclass != null && c == '}') {
153                 i += 1;
154                 break;
155             }
156             // read an identifier, the class name
157             if (identifier == null) {
158                 // negative or positive set ?
159                 if (c == '-') {
160                     i += 1;
161                 } else if (c == '+') {
162                     deny = false;
163                     i += 1;
164                 }
165                 final int next = readIdentifier(temp, i);
166                 if (i != next) {
167                     identifier = temp.toString();
168                     temp.setLength(0);
169                     i = next;
170                     continue;
171                 }
172             }
173             // parse a class:
174             if (njclass == null) {
175                 // we must have read the class ('identifier {'...)
176                 if (identifier == null || c != '{') {
177                     throw new IllegalStateException(unexpected(c, i));
178                 }
179                 // if we have a class, it has a name
180                 njclass = deny ? new Permissions.NoJexlClass() : new Permissions.JexlClass();
181                 njname = outer != null ? outer + "$" + identifier : identifier;
182                 njpackage.addNoJexl(njname, njclass);
183                 identifier = null;
184             } else if (identifier != null)  {
185                 // class member mode
186                 if (c == '{') {
187                     // inner class
188                     i = readClass(njpackage, deny, njname, identifier, i - 1);
189                     identifier = null;
190                     continue;
191                 }
192                 if (c == ';') {
193                     // field or method?
194                     if (isMethod) {
195                         njclass.methodNames.add(identifier);
196                         isMethod = false;
197                     } else {
198                         njclass.fieldNames.add(identifier);
199                     }
200                     identifier = null;
201                 } else if (c == '(' && !isMethod) {
202                     // method; only one opening parenthesis allowed
203                     isMethod = true;
204                 } else if (c != ')' || src.charAt(i - 1) != '(') {
205                     // closing parenthesis following opening one was expected
206                     throw new IllegalStateException(unexpected(c, i));
207                 }
208             }
209             i += 1;
210         }
211         // empty class means allow or deny all
212         if (njname != null && njclass.isEmpty()) {
213             njpackage.addNoJexl(njname, njclass instanceof Permissions.JexlClass
214                 ? Permissions.JEXL_CLASS
215                 : Permissions.NOJEXL_CLASS);
216 
217         }
218         return i;
219     }
220 
221     /**
222      * Reads a comment till end-of-line.
223      * @param offset initial position
224      * @return position after comment
225      */
226     private int readEol(final int offset) {
227         int i = offset;
228         while (i < size) {
229             final char c = src.charAt(i);
230             if (c == '\n') {
231                 break;
232             }
233             i += 1;
234         }
235         return i;
236     }
237 
238     /**
239      * Reads an identifier (optionally dot-separated).
240      * @param id the builder to fill the identifier character with
241      * @param offset the initial reading position
242      * @return the position after the identifier
243      */
244     private int readIdentifier(final StringBuilder id, final int offset) {
245         return readIdentifier(id, offset, false, false);
246     }
247 
248     /**
249      * Reads an identifier (optionally dot-separated).
250      * @param id the builder to fill the identifier character with
251      * @param offset the initial reading position
252      * @param dot whether dots (.) are allowed
253      * @param star whether stars (*) are allowed
254      * @return the position after the identifier
255      */
256     private int readIdentifier(final StringBuilder id, final int offset, final boolean dot, final boolean star) {
257         int begin = -1;
258         boolean starf = star;
259         int i = offset;
260         char c = 0;
261         while (i < size) {
262             c = src.charAt(i);
263             // accumulate identifier characters
264             if (Character.isJavaIdentifierStart(c) && begin < 0) {
265                 begin = i;
266                 id.append(c);
267             } else if (Character.isJavaIdentifierPart(c) && begin >= 0) {
268                 id.append(c);
269             } else if (dot && c == '.') {
270                 if (src.charAt(i - 1) == '.') {
271                     throw new IllegalStateException(unexpected(c, i));
272                 }
273                 id.append('.');
274                 begin = -1;
275             } else if (starf && c == '*') {
276                 id.append('*');
277                 starf = false; // only one star
278             } else {
279                 break;
280             }
281             i += 1;
282         }
283         // cant end with a dot
284         if (dot && c == '.') {
285             throw new IllegalStateException(unexpected(c, i));
286         }
287         return i;
288     }
289 
290     /**
291      * Reads a package permission.
292      */
293     private void readPackages() {
294         final StringBuilder temp = new StringBuilder();
295         Permissions.NoJexlPackage njpackage = null;
296         int i = 0;
297         int j = -1;
298         String pname = null;
299         while (i < size) {
300             final char c = src.charAt(i);
301             // if no parsing progress can be made, we are in error
302             if (j >= i) {
303                 throw new IllegalStateException(unexpected(c, i));
304             }
305             j = i;
306             // get rid of space
307             if (Character.isWhitespace(c)) {
308                 i = readSpaces(i + 1);
309                 continue;
310             }
311             // eol comment
312             if (c == '#') {
313                 i = readEol(i + 1);
314                 continue;
315             }
316             // read the package qualified name
317             if (pname == null) {
318                 final int next = readIdentifier(temp, i, true, true);
319                 if (i != next) {
320                     pname = temp.toString();
321                     temp.setLength(0);
322                     i = next;
323                     // consume it if it is a wildcard declaration
324                     if (pname.endsWith(".*")) {
325                         wildcards.add(pname);
326                         pname = null;
327                     }
328                     continue;
329                 }
330             }
331             // package mode
332             if (njpackage == null) {
333                 if (c == '{') {
334                     njpackage = packages.compute(pname,
335                         (n, p) -> new Permissions.NoJexlPackage(p == null? null : p.nojexl)
336                     );
337                     i += 1;
338                 }
339             } else if (c == '}') {
340                 // empty means whole package
341                 if (njpackage.isEmpty()) {
342                     packages.put(pname, Permissions.NOJEXL_PACKAGE);
343                 }
344                 njpackage = null; // can restart anew
345                 pname = null;
346                 i += 1;
347             } else {
348                 i = readClass(njpackage, true,null, null, i);
349             }
350         }
351     }
352 
353     /**
354      * Reads spaces.
355      * @param offset initial position
356      * @return position after spaces
357      */
358     private int readSpaces(final int offset) {
359         int i = offset;
360         while (i < size) {
361             final char c = src.charAt(i);
362             if (!Character.isWhitespace(c)) {
363                 break;
364             }
365             i += 1;
366         }
367         return offset;
368     }
369 
370     /**
371      * Compose a parsing error message.
372      * @param c the offending character
373      * @param i the offset position
374      * @return the error message
375      */
376     private String unexpected(final char c, final int i) {
377         return "unexpected '" + c + "'" + "@" + i;
378     }
379 }