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 }