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 }