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