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  package org.apache.commons.jexl3;
18  
19  import static org.apache.commons.jexl3.introspection.JexlPermissions.RESTRICTED;
20  import static org.apache.commons.jexl3.introspection.JexlPermissions.UNRESTRICTED;
21  import static org.junit.jupiter.api.Assertions.assertArrayEquals;
22  import static org.junit.jupiter.api.Assertions.assertEquals;
23  import static org.junit.jupiter.api.Assertions.assertFalse;
24  import static org.junit.jupiter.api.Assertions.assertInstanceOf;
25  import static org.junit.jupiter.api.Assertions.assertNotNull;
26  import static org.junit.jupiter.api.Assertions.assertNull;
27  import static org.junit.jupiter.api.Assertions.assertThrows;
28  import static org.junit.jupiter.api.Assertions.assertTrue;
29  import static org.junit.jupiter.api.Assertions.fail;
30  
31  import java.io.Closeable;
32  import java.io.File;
33  import java.lang.reflect.Method;
34  import java.math.BigDecimal;
35  import java.util.Arrays;
36  import java.util.Collections;
37  import java.util.HashMap;
38  import java.util.Iterator;
39  import java.util.List;
40  import java.util.Map;
41  import java.util.Objects;
42  import java.util.concurrent.atomic.AtomicLong;
43  
44  import org.apache.commons.jexl3.internal.Debugger;
45  import org.apache.commons.jexl3.internal.Engine32;
46  import org.apache.commons.jexl3.internal.Scope;
47  import org.apache.commons.jexl3.internal.TemplateEngine;
48  import org.apache.commons.jexl3.introspection.JexlPermissions;
49  import org.apache.commons.jexl3.introspection.JexlSandbox;
50  import org.apache.commons.jexl3.parser.ASTJexlScript;
51  import org.apache.commons.jexl3.parser.JexlScriptParser;
52  import org.apache.commons.jexl3.parser.Parser;
53  import org.apache.commons.jexl3.parser.StringProvider;
54  import org.junit.jupiter.api.Assertions;
55  import org.junit.jupiter.api.Test;
56  
57  /**
58   * Test cases for reported issue between JEXL-300 and JEXL-399.
59   */
60  public class Issues400Test {
61  
62      public static class VinzCaller {
63          private final JexlContext context;
64  
65          VinzCaller(final JexlContext context) {
66              this.context = context;
67          }
68  
69          public Object execute(final JexlScript script) {
70              return script.execute(context);
71          }
72      }
73  
74      public static class VinzContext extends MapContext {
75          public String member(final String m, final String u) {
76              return m + '.' + u;
77          }
78      }
79  
80      /**
81       * Any function in a context can be used as a method of its first parameter. Overloads are respected.
82       */
83      public static class XuContext extends MapContext {
84  
85          public String join(final int[] list, final String str) {
86              return join(Arrays.stream(list).iterator(), str);
87          }
88  
89          public String join(final Iterable<?> list, final String str) {
90              return join(list.iterator(), str);
91          }
92  
93          public String join(final Iterator<?> iterator, final String str) {
94              if (!iterator.hasNext()) {
95                  return "";
96              }
97              final StringBuilder strb = new StringBuilder(256);
98              strb.append(iterator.next().toString());
99              while (iterator.hasNext()) {
100                 strb.append(str);
101                 strb.append(Objects.toString(iterator.next(), "?"));
102             }
103             return strb.toString();
104         }
105     }
106 
107     private static void run404(final JexlEngine jexl, final String src, final Object... a) {
108         final JexlScript script = jexl.createScript(src, "a", "b");
109         if (!src.endsWith(";")) {
110             assertEquals(script.getSourceText(), script.getParsedText());
111         }
112         final Object result = script.execute(null, a);
113         assertEquals(42, result);
114     }
115 
116     @Test
117     void test402() {
118         final JexlContext jc = new MapContext();
119         // @formatter:off
120       final String[] sources = {
121         "if (true) { return }",
122         "if (true) { 3; return }",
123         "(x->{ 3; return })()"
124       };
125       // @formatter:on
126         final JexlEngine jexl = new JexlBuilder().create();
127         for (final String source : sources) {
128             final JexlScript e = jexl.createScript(source);
129             final Object o = e.execute(jc);
130             assertNull(o);
131         }
132     }
133 
134     @Test
135     void test403() {
136         // @formatter:off
137         final String[] strings = {
138             "  map1.`${item.a}` = 1;\n",
139             "  map1[`${item.a}`] = 1;\n",
140             "  map1[item.a] = 1;\n"
141          };
142         // @formatter:on
143         for (final String setmap : strings) {
144             // @formatter:off
145             final String src = "var a = {'a': 1};\n" +
146                 "var list = [a, a];\n" +
147                 "let map1 = {:};\n" +
148                 "for (let item : list) {\n" +
149                 setmap +
150                 "}\n " +
151                 "map1";
152             // @formatter:on
153             final JexlEngine jexl = new JexlBuilder().cache(64).create();
154             final JexlScript script = jexl.createScript(src);
155             for (int i = 0; i < 2; ++i) {
156                 final Object result = script.execute(null);
157                 assertInstanceOf(Map.class, result);
158                 final Map<?, ?> map = (Map<?, ?>) result;
159                 assertEquals(1, map.size());
160                 final Object val = jexl.createScript("m -> m[1]").execute(null, map);
161                 assertEquals(1, val);
162             }
163         }
164     }
165 
166     @Test
167     void test404a() {
168         final JexlEngine jexl = new JexlBuilder().cache(64).strict(true).safe(false).create();
169         Map<String, Object> a = Collections.singletonMap("b", 42);
170         // access is constant
171         for (final String src : new String[]{"a.b", "a?.b", "a['b']", "a?['b']", "a?.`b`"}) {
172             run404(jexl, src, a);
173             run404(jexl, src + ";", a);
174         }
175         // access is variable
176         for (final String src : new String[]{"a[b]", "a?[b]", "a?.`${b}`"}) {
177             run404(jexl, src, a, "b");
178             run404(jexl, src + ";", a, "b");
179         }
180         // add a 3rd access
181         final Map<String, Object> b = Collections.singletonMap("c", 42);
182         a = Collections.singletonMap("b", b);
183         for (final String src : new String[]{"a[b].c", "a?[b]?['c']", "a?.`${b}`.c"}) {
184             run404(jexl, src, a, "b");
185         }
186     }
187 
188     @Test
189     void test404b() {
190         // @formatter:off
191       final JexlEngine jexl = new JexlBuilder()
192           .cache(64)
193           .strict(true)
194           .safe(false)
195           .create();
196       // @formatter:on
197         final Map<String, Object> b = Collections.singletonMap("c", 42);
198         final Map<String, Object> a = Collections.singletonMap("b", b);
199         JexlScript script;
200         Object result;
201         script = jexl.createScript("a?['B']?['C']", "a");
202         result = script.execute(null, a);
203         assertEquals(script.getSourceText(), script.getParsedText());
204         assertNull(result);
205         script = jexl.createScript("a?['b']?['C']", "a");
206         assertEquals(script.getSourceText(), script.getParsedText());
207         result = script.execute(null, a);
208         assertNull(result);
209         script = jexl.createScript("a?['b']?['c']", "a");
210         assertEquals(script.getSourceText(), script.getParsedText());
211         result = script.execute(null, a);
212         assertEquals(42, result);
213         script = jexl.createScript("a?['B']?['C']?: 1042", "a");
214         assertEquals(script.getSourceText(), script.getParsedText());
215         result = script.execute(null, a);
216         assertEquals(1042, result);
217         // can still do ternary, note the space between ? and [
218         script = jexl.createScript("a? ['B']:['C']", "a");
219         result = script.execute(null, a);
220         assertArrayEquals(new String[]{"B"}, (String[]) result);
221         script = jexl.createScript("a?['b'] ?: ['C']", "a");
222         result = script.execute(null, a);
223         assertEquals(b, result);
224         script = jexl.createScript("a?['B'] ?: ['C']", "a");
225         result = script.execute(null, a);
226         assertArrayEquals(new String[]{"C"}, (String[]) result);
227     }
228 
229     @Test
230     void test406a() {
231         // @formatter:off
232         final JexlEngine jexl = new JexlBuilder()
233             .cache(64)
234             .strict(true)
235             .safe(false)
236             .create();
237         // @formatter:on
238 
239         final JexlContext context = new XuContext();
240         // @formatter:off
241         final List<String> list = Arrays.asList(
242             "[1, 2, 3, 4, ...].join('-')", // List<Integer>
243             "[1, 2, 3, 4,].join('-')", // int[]
244             "(1 .. 4).join('-')", // iterable<Integer>
245             "join([1, 2, 3, 4, ...], '-')",
246             "join([1, 2, 3, 4], '-')",
247             "join((1 .. 4), '-')");
248         // @formatter:on
249         for (final String src : list) {
250             final JexlScript script = jexl.createScript(src);
251             final Object result = script.execute(context);
252             assertEquals("1-2-3-4", result, src);
253         }
254 
255         final String src0 = "x.join('*')";
256         final JexlScript script0 = jexl.createScript(src0, "x");
257         final String src1 = "join(x, '*')";
258         final JexlScript script1 = jexl.createScript(src1, "x");
259         for (final Object x : Arrays.asList(Arrays.asList(1, 2, 3, 4), new int[]{1, 2, 3, 4})) {
260             Object result = script0.execute(context, x);
261             assertEquals("1*2*3*4", result, src0);
262             result = script1.execute(context, x);
263             assertEquals("1*2*3*4", result, src1);
264         }
265     }
266 
267     @Test
268     void test407() {
269         // Java version
270         final double r = 99.0d + 7.82d - 99.0d - 7.82d;
271         assertEquals(0d, r, 8.e-15); // Not zero, IEEE 754
272         // jexl
273         final JexlEngine jexl = new JexlBuilder().create();
274         final JexlScript script = jexl.createScript("a + b - a - b", "a", "b");
275         // using doubles, same as Java
276         Number result = (Number) script.execute(null, 99.0d, 7.82d);
277         assertEquals(0d, result.doubleValue(), 8.e-15);
278         // using BigdDecimal, more precise, still not zero
279         result = (Number) script.execute(null, new BigDecimal("99.0"), new BigDecimal("7.82"));
280         assertEquals(0d, result.doubleValue(), 3.e-32);
281     }
282 
283     @Test
284     void test412() {
285         final Map<Object, Object> ctl = new HashMap<>();
286         ctl.put("one", 1);
287         ctl.put("two", 2);
288         final String fnsrc0 = "function f(x) { x }\n" + "let one = 'one', two = 'two';\n";
289         // @formatter:off
290         final List<String> list = Arrays.asList(
291             "{ one : f(1), two:f(2) }",
292             "{ one: f(1), two: f(2) }",
293             "{ one: f(1), two:f(2) }",
294             "{ one :f(1), two:f(2) }");
295         // @formatter:on
296         for (final String map0 : list) {
297             final String fnsrc = fnsrc0 + map0;
298             final JexlContext jc = new MapContext();
299             final JexlEngine jexl = new JexlBuilder().create();
300             final JexlScript e = jexl.createScript(fnsrc);
301             final Object o = e.execute(jc);
302             assertInstanceOf(Map.class, o);
303             final Map<?, ?> map = (Map<?, ?>) o;
304             assertEquals(map, ctl);
305         }
306     }
307 
308     @Test
309     void test413a() {
310         final JexlBuilder builder = new JexlBuilder();
311         final JexlEngine jexl = builder.create();
312         final JexlScript script = jexl.createScript("var c = 42; var f = y -> c += y; f(z)", "z");
313         final Number result = (Number) script.execute(null, 12);
314         assertEquals(54, result);
315     }
316 
317     @Test
318     void test413b() {
319         final JexlBuilder builder = new JexlBuilder();
320         final JexlOptions options = builder.options();
321         options.setConstCapture(true);
322         options.setLexical(true);
323         final JexlEngine jexl = builder.create();
324         final JexlScript script = jexl.createScript("var c = 42; var f = y -> c += y; f(z)", "z");
325         final JexlException.Variable xvar = assertThrows(JexlException.Variable.class, () -> script.execute(null, 12), "c should be const");
326         assertEquals("c", xvar.getVariable());
327     }
328 
329     @Test
330     void test413c() {
331         final JexlBuilder builder = new JexlBuilder();
332         final JexlEngine jexl = builder.create();
333         final JexlScript script = jexl.createScript("#pragma jexl.options '+constCapture'\nvar c = 42; var f = y -> c += y; f(z)", "z");
334         final JexlException.Variable xvar = assertThrows(JexlException.Variable.class, () -> script.execute(null, 12), "c should be const");
335         assertEquals("c", xvar.getVariable());
336     }
337 
338     @Test
339     void test413d() {
340         final JexlBuilder builder = new JexlBuilder().features(new JexlFeatures().constCapture(true));
341         final JexlEngine jexl = builder.create();
342         final JexlException.Parsing xparse = assertThrows(
343                 JexlException.Parsing.class,
344                 () -> jexl.createScript("var c = 42; var f = y -> c += y; f(z)", "z"),
345                 "c should be const");
346         assertTrue(xparse.getMessage().contains("const"));
347     }
348 
349     @Test
350     void test415() {
351         final JexlBuilder builder = new JexlBuilder().features(new JexlFeatures().constCapture(true));
352         final JexlEngine jexl = builder.create();
353         JexlScript script;
354         Object result;
355         script = jexl.createScript("`#${c}`", "c");
356         result = script.execute(null, 42);
357         assertEquals("#42", result.toString());
358         script = jexl.createScript("`$${c}`", "c");
359         result = script.execute(null, 42);
360         assertEquals("$42", result.toString());
361         script = jexl.createScript("`$#{c}`", "c");
362         result = script.execute(null, 42);
363         assertEquals("$42", result.toString());
364         script = jexl.createScript("`##{c}`", "c");
365         result = script.execute(null, 42);
366         assertEquals("#42", result.toString());
367         script = jexl.createScript("`--##{c}`", "c");
368         result = script.execute(null, 42);
369         assertEquals("--#42", result.toString());
370     }
371 
372     @Test
373     void test419() throws NoSuchMethodException {
374         // check RESTRICTED permissions denies call to System::currentTimeMillis()
375         final Method currentTimeMillis = System.class.getMethod("currentTimeMillis");
376         assertFalse(RESTRICTED.allow(currentTimeMillis));
377         // compose using a positive class permission to allow just System::currentTimeMillis()
378         final JexlPermissions permissions = RESTRICTED.compose("java.lang { +System { currentTimeMillis(); } }");
379         // check no side effect on compose
380         assertTrue(permissions.allow(currentTimeMillis));
381         assertFalse(RESTRICTED.allow(currentTimeMillis));
382 
383         // An engine with the System class as namespace and the positive permissions
384         final JexlEngine jexl = new JexlBuilder().namespaces(Collections.singletonMap("sns", System.class)).permissions(permissions).create();
385 
386         final AtomicLong result = new AtomicLong();
387         assertEquals(0, result.get());
388         final long now = System.currentTimeMillis();
389         // calling System::currentTimeMillis() is allowed and behaves as expected
390         jexl.createScript("result.set(sns:currentTimeMillis())", "result").execute(null, result);
391         assertTrue(result.get() >= now);
392 
393         // we still cant call anything else
394         final JexlScript script = jexl.createScript("sns:gc()");
395         final JexlException.Method method = assertThrows(JexlException.Method.class, () -> script.execute(null));
396         assertEquals("gc", method.getMethod());
397 
398     }
399 
400     @Test
401     void testDocBreakContinue() {
402         final JexlBuilder builder = new JexlBuilder().features(new JexlFeatures().constCapture(true));
403         final JexlEngine jexl = builder.create();
404         JexlScript script;
405         Object result;
406         // @formatter:off
407         final String srcContinue = "let text = '';\n" +
408             "for (let i : (4..2)) { if (i == 3) continue; text += i; }\n" +
409             "text;";
410         // @formatter:on
411         script = jexl.createScript(srcContinue);
412         result = script.execute(null);
413         assertEquals("42", result);
414         // @formatter:off
415         final String srcBreak = "let i = 33;\n" +
416             "while (i < 66) { if (i == 42) { break; } i += 1; }\n" +
417             "i;";
418         // @formatter:on
419         script = jexl.createScript(srcBreak);
420         result = script.execute(null);
421         assertEquals(42, result);
422     }
423 
424     @Test
425     void testNamespaceVsTernary0() {
426         final VinzContext ctxt = new VinzContext();
427         ctxt.set("Users", "USERS");
428         final JexlEngine jexl = new JexlBuilder().safe(false).strict(true).silent(false).create();
429         // @formatter:off
430         JexlScript script = jexl.createScript("() -> {\n"
431             + "  var fn = (user) -> {\n"
432             + "     user ? user : member(Users, 'user');\n"
433             + "  }\n"
434             + "}");
435         // @formatter:on
436         Object r = script.execute(ctxt);
437         assertNotNull(r);
438         script = (JexlScript) r;
439         r = script.execute(ctxt);
440         assertEquals("USERS.user", r);
441     }
442 
443     @Test
444     void testNamespaceVsTernary1() {
445         final VinzContext ctxt = new VinzContext();
446         ctxt.set("Users", "USERS");
447         ctxt.set("vinz", new VinzCaller(ctxt));
448         final JexlEngine jexl = new JexlBuilder().safe(false).strict(true).silent(false).create();
449         // @formatter:off
450         final JexlScript script = jexl.createScript(
451             "vinz.execute(() -> {\n"
452             + "  var test = 42;\n"
453             + "  var user = useTest ? test : member(Users, 'user');\n"
454             + "})\n" , "useTest");
455         // @formatter:on
456         Object r = script.execute(ctxt, false);
457         assertNotNull(r);
458         assertEquals("USERS.user", r);
459         r = script.execute(ctxt, true);
460         assertNotNull(r);
461         assertEquals(42, r);
462     }
463 
464     public static class Ns429 {
465         public int f(final int x) {
466             return x * 10000 + 42;
467         }
468     }
469 
470     @Test
471     void test429a() {
472         final MapContext ctxt = new MapContext();
473         final JexlFeatures features = JexlFeatures.createDefault();
474         final JexlEngine jexl = new JexlBuilder().features(features).safe(false).strict(true).silent(false).create();
475         final JexlScript f = jexl.createScript("x -> x");
476         ctxt.set("f", f);
477         String src = "#pragma jexl.namespace.b " + Ns429.class.getName() + "\n" + "b ? b : f(2);";
478         JexlScript script = jexl.createScript(src, "b");
479         assertEquals(1, (int) script.execute(ctxt, 1));
480 
481         src = "#pragma jexl.namespace.b " + Ns429.class.getName() + "\n" + "b ? b:f(2) : 1;";
482         script = jexl.createScript(src, "b");
483         assertEquals(20042, (int) script.execute(ctxt, 1));
484     }
485 
486     @Test
487     void test429b() {
488         final MapContext ctxt = new MapContext();
489         ctxt.set("b", 1);
490         final JexlFeatures features = JexlFeatures.createDefault();
491         features.namespaceIdentifier(true);
492         final JexlEngine jexl = new JexlBuilder().features(features).safe(false).strict(true).silent(false).create();
493         final JexlScript f = jexl.createScript("x -> x");
494         ctxt.set("f", f);
495         String src = "#pragma jexl.namespace.b " + Ns429.class.getName() + "\n" + "b ? b : f(2);";
496         JexlScript script = jexl.createScript(src);
497         assertEquals(1, (int) script.execute(ctxt));
498 
499         src = "#pragma jexl.namespace.b " + Ns429.class.getName() + "\n" + "b ? b:f(2) : 1;";
500         script = jexl.createScript(src);
501         assertEquals(20042, (int) script.execute(ctxt));
502     }
503 
504     @Test
505     void test431a() {
506         final JexlEngine jexl = new JexlBuilder().create();
507         final String src = "let x = 0; try { x += 19 } catch (let error) { return 169 } try { x += 23 } catch (let error) { return 169 }";
508         final JexlScript script = jexl.createScript(src);
509         assertNotNull(script);
510         final Object result = script.execute(null);
511         assertEquals(42, result);
512     }
513 
514     Closeable foo() {
515         return null;
516     }
517 
518     @Test
519     void test431b() {
520         final JexlEngine jexl = new JexlBuilder().create();
521         final String src = "let x = 0; try(let error) { x += 19 } catch (let error) { return 169 } try { x += 23 } catch (let error) { return 169 }";
522         final JexlScript script = jexl.createScript(src);
523         assertNotNull(script);
524         final Object result = script.execute(null);
525         assertEquals(42, result);
526     }
527 
528     @Test
529     void test431c() {
530         final JexlEngine jexl = new JexlBuilder().create();
531         final String src = "let xx = 0; try { xx += 19 } catch (let xx) { return 169 }";
532         try {
533             final JexlScript script = jexl.createScript(src);
534             fail("xx is already defined in scope");
535         } catch (final JexlException.Parsing parsing) {
536             assertTrue(parsing.getDetail().contains("xx"));
537         }
538     }
539 
540     @Test
541     void test433() {
542         final JexlEngine jexl = new JexlBuilder().create();
543         final String src = "let condition = true; if (condition) { return; }";
544         final JexlScript script = jexl.createScript(src);
545         assertNotNull(script);
546         final Object result = script.execute(null);
547         assertNull(result);
548         final Debugger debugger = new Debugger();
549         assertTrue(debugger.debug(script));
550         final String dbgStr = debugger.toString();
551         assertTrue(JexlTestCase.equalsIgnoreWhiteSpace(src, dbgStr));
552     }
553 
554     @Test
555     void test434() {
556         final JexlEngine jexl = new JexlBuilder().safe(false).strict(true).create();
557         final String src = "let foo = null; let value = foo?[bar]";
558         final JexlScript script = jexl.createScript(src);
559         assertNotNull(script);
560         final Object result = script.execute(null);
561         assertNull(result);
562     }
563 
564     public static class Arithmetic435 extends JexlArithmetic {
565         public Arithmetic435(final boolean strict) {
566             super(strict);
567         }
568 
569         public Object empty(final String type) {
570             if ("list".equals(type)) {
571                 return Collections.emptyList();
572             }
573             return null;
574         }
575     }
576 
577     @Test
578     void test435() {
579         final JexlArithmetic arithmetic = new Arithmetic435(true);
580         final JexlEngine jexl = new JexlBuilder().arithmetic(arithmetic).create();
581         final String src = "empty('list')";
582         final JexlScript script = jexl.createScript(src);
583         assertNotNull(script);
584         final Object result = script.execute(null);
585         assertInstanceOf(List.class, result);
586     }
587 
588     @Test
589     void test436a() {
590         final String[] srcs = {"let i = null; ++i", "let i; ++i;", "let i; i--;", "let i; i++;"};
591         run436(null, srcs);
592     }
593 
594     @Test
595     void test436b() {
596         final String[] srcs = {"var i = null; ++i", "var i; ++i;", "var i; i--;", "var i; i++;"};
597         run436(null, srcs);
598     }
599 
600     @Test
601     void test436c() {
602         final JexlContext ctxt = new MapContext();
603         ctxt.set("i", null);
604         final String[] srcs = {"++i", "++i;", "i--;", "i++;"};
605         run436(null, srcs);
606     }
607 
608     void run436(final JexlContext ctxt, final String[] srcs) {
609         final JexlEngine jexl = new JexlBuilder().create();
610         for (final String src : srcs) {
611             final JexlScript script = jexl.createScript(src);
612             assertThrows(JexlException.Operator.class, () -> script.execute(ctxt));
613         }
614     }
615 
616     @Test
617     void test437a() {
618         final JexlEngine jexl = new JexlBuilder().create();
619         final String src = "let values = [...]\n"
620                 + "function append(const value) {\n"
621                 + "  values.add(value)\n"
622                 + "}\n"
623                 + "\n"
624                 + "append(1)\n"
625                 + "append(2)\n"
626                 + "return values ";
627         final JexlScript script = jexl.createScript(src);
628         assertNotNull(script);
629         final Object result = script.execute(null);
630         assertInstanceOf(List.class, result);
631         final List<?> values = (List<?>) result;
632         assertEquals(2, values.size());
633     }
634 
635     @Test
636     void test437b() {
637         final JexlFeatures features = JexlFeatures.createDefault().ambiguousStatement(true);
638         assertTrue(features.supportsAmbiguousStatement());
639         final JexlEngine jexl = new JexlBuilder().features(features).create();
640         final String src = "let values = [...]"
641                 + "function append(const value) {"
642                 + "  values.add(value)"
643                 + "}"
644                 + "append(1)"
645                 + "append(2)"
646                 + "return values ";
647         final JexlScript script = jexl.createScript(src);
648         assertNotNull(script);
649         final Object result = script.execute(null);
650         assertInstanceOf(List.class, result);
651         final List<?> values = (List<?>) result;
652         assertEquals(2, values.size());
653     }
654 
655     /**
656      * The set of characters that may be followed by a '='.
657      */
658     static final char[] EQ_FRIEND;
659 
660     static {
661         final char[] eq = {'!', ':', '<', '>', '^', '|', '&', '+', '-', '/', '*', '~', '='};
662         Arrays.sort(eq);
663         EQ_FRIEND = eq;
664     }
665 
666     /**
667      * Transcodes a SQL-inspired expression to a JEXL expression.
668      *
669      * @param expr the expression to transcode
670      * @return the resulting expression
671      */
672     private static String transcodeSQLExpr(final CharSequence expr) {
673         final StringBuilder strb = new StringBuilder(expr.length());
674         final int end = expr.length();
675         char previous = 0;
676         for (int i = 0; i < end; ++i) {
677             final char c = expr.charAt(i);
678             if (previous == '<') {
679                 // previous char a '<' now followed by '>'
680                 if (c == '>') {
681                     // replace '<>' with '!='
682                     strb.append("!=");
683                     previous = c;
684                     continue;
685                 }
686                 strb.append('<');
687             }
688             if (c != '<') {
689                 if (c == '=') {
690                     // replace '=' with '==' when it does not follow a 'friend'
691                     if (Arrays.binarySearch(EQ_FRIEND, previous) >= 0) {
692                         strb.append(c);
693                     } else {
694                         strb.append("==");
695                     }
696                 } else {
697                     strb.append(c);
698                     if (c == '"' || c == '\'') {
699                         // read string, escape '\'
700                         boolean escape = false;
701                         for (i += 1; i < end; ++i) {
702                             final char ec = expr.charAt(i);
703                             strb.append(ec);
704                             if (ec == '\\') {
705                                 escape = !escape;
706                             } else if (escape) {
707                                 escape = false;
708                             } else if (ec == c) {
709                                 break;
710                             }
711                         }
712                     }
713                 }
714             }
715             previous = c;
716         }
717         return strb.toString();
718     }
719 
720     public static class SQLParser implements JexlScriptParser {
721         final Parser parser;
722 
723         public SQLParser() {
724             parser = new Parser(new StringProvider(";"));
725         }
726 
727         @Override
728         public ASTJexlScript parse(final JexlInfo info, final JexlFeatures features, final String src, final Scope scope) {
729             return parser.parse(info, features, transcodeSQLExpr(src), scope);
730         }
731 
732         @Override
733         public ASTJexlScript jxltParse(final JexlInfo info, final JexlFeatures features, final String src, final Scope scope) {
734             return new Parser(parser).parse(info, features, transcodeSQLExpr(src), scope);
735         }
736     }
737 
738 
739     @Test
740     void testSQLTranspose() {
741         final String[] e = {"a<>b", "a = 2", "a.b.c <> '1<>0'"};
742         final String[] j = {"a!=b", "a == 2", "a.b.c != '1<>0'"};
743         for (int i = 0; i < e.length; ++i) {
744             final String je = transcodeSQLExpr(e[i]);
745             Assertions.assertEquals(j[i], je);
746         }
747     }
748 
749     @Test
750     void testSQLNoChange() {
751         final String[] e = {"a <= 2", "a >= 2", "a := 2", "a + 3 << 4 > 5",};
752         for (final String element : e) {
753             final String je = transcodeSQLExpr(element);
754             Assertions.assertEquals(element, je);
755         }
756     }
757 
758     @Test
759     void test438() {// no local, no lambda, no loops, no-side effects
760         final JexlFeatures f = new JexlFeatures().localVar(false).lambda(false).loops(false).sideEffect(false).sideEffectGlobal(false);
761         final JexlBuilder builder = new JexlBuilder().parserFactory(SQLParser::new).cache(32).features(f);
762         final JexlEngine sqle = builder.create();
763         Assertions.assertTrue((boolean) sqle.createScript("a <> 25", "a").execute(null, 24));
764         Assertions.assertFalse((boolean) sqle.createScript("a <> 25", "a").execute(null, 25));
765         Assertions.assertFalse((boolean) sqle.createScript("a = 25", "a").execute(null, 24));
766         Assertions.assertTrue((boolean) sqle.createScript("a != 25", "a").execute(null, 24));
767         Assertions.assertTrue((boolean) sqle.createScript("a = 25", "a").execute(null, 25));
768         Assertions.assertFalse((boolean) sqle.createScript("a != 25", "a").execute(null, 25));
769     }
770 
771     @Test
772     void testIssue441() {
773         final JexlEngine jexl = new JexlBuilder().create();
774         String ctl = "\nab\nc`d\n";
775         final JexlExpression e = jexl.createExpression("`\nab\nc\\`d\n`");
776         Object o = e.evaluate(null);
777         Assertions.assertEquals(ctl, o);
778 
779         final JexlContext context = new MapContext();
780         context.set("name", "Hello");
781         final String code = "return `${name + '\\n' + name}`;";
782         final JexlScript script = jexl.createScript(code);
783         o = script.execute(context);
784         ctl = "Hello\nHello";
785         Assertions.assertEquals(ctl, o);
786     }
787 
788     @Test
789     void testIssue442() {
790         final JexlEngine jexl = new JexlBuilder().create();
791         final JexlContext context = new MapContext();
792         final String code = "var x = 'hello';\n" +
793                 "function test(z) {\n" +
794                 //"x + ' ' + z\n"+
795                 "`${x} ${z}`;\n" +
796                 "}\n" +
797                 "test('world');";
798         final JexlScript script = jexl.createScript(code);
799         final Object result = script.execute(context);
800         Assertions.assertEquals("hello world", result);
801     }
802 
803 
804     @Test
805     void testIssue447() {
806         final JexlEngine jexl = new JexlBuilder().create();
807         final String src = "const c = `${a}\n?= ${b}`; function foo(const left, const right) { `${left}\n?== ${right}` } c+foo(a, b)";
808         final JexlScript script = jexl.createScript(src, "a", "b");
809         final Object result = script.execute(null, "a", "b");
810         Assertions.assertEquals("a\n?= ba\n?== b", result);
811         String[] locals =  script.getLocalVariables();
812         Assertions.assertArrayEquals(new String[]{"c", "foo"}, locals);
813         final String TEST447 = "src/test/scripts/test447.jexl";
814         final File src447 = new File(TEST447);
815         final JexlScript script447 = jexl.createScript(src447);
816         final Object result447 = script447.execute(null);
817         Assertions.assertInstanceOf(List.class, result447);
818         @SuppressWarnings("unchecked")
819         final List<Boolean> list = (List<Boolean>) result447;
820         for (final Boolean item : list) {
821             Assertions.assertTrue(item);
822         }
823     }
824 
825     public static class BrkContext extends MapContext {
826         public BrkContext() {
827             super();
828             set("SYSTEM", System.class);
829             set("UNRESTRICTED", UNRESTRICTED);
830         }
831 
832         public static Object brk(Object debug) {
833             return debug;
834         }
835 
836     }
837 
838     @Test
839     void test450a() {
840         JexlEngine jexl0 = new JexlBuilder().silent(false).permissions(JexlPermissions.RESTRICTED).create();
841         assertThrows(JexlException.Method.class, ()->jexl0.newInstance(
842             "org.apache.commons.jexl3.internal.introspection.Uberspect", null, null),
843             "should not be able to create Uberspect with RESTRICTED");
844         JexlPermissions perm = new JexlPermissions.ClassPermissions(org.apache.commons.jexl3.internal.introspection.Uberspect.class);
845         JexlEngine jexl1 = new JexlBuilder().silent(false).permissions(perm).create();
846         assertNotNull(jexl1.newInstance(
847                         "org.apache.commons.jexl3.internal.introspection.Uberspect", null, null),
848                 "should able to create Uberspect with Uberspect permission");
849 
850     }
851 
852     @Test
853     void test450b() {
854         // cannot load System with RESTRICTED
855         assertThrows(JexlException.Method.class,
856                 () -> run450b(JexlPermissions.RESTRICTED), "should not be able to load System with RESTRICTED");
857         // can load System with UNRESTRICTED
858         assertEquals(java.lang.System.class, run450b(UNRESTRICTED));
859         // need to explicitly allow Uberspect and the current class loader to load System
860         JexlPermissions perm = new JexlPermissions.ClassPermissions(
861                getClass().getClassLoader().getClass(), org.apache.commons.jexl3.internal.introspection.Uberspect.class);
862         assertEquals(java.lang.System.class, run450b(perm));
863     }
864 
865     private static Object run450b(JexlPermissions perm) {
866         JexlEngine jexl = new JexlBuilder().silent(false).permissions(perm).create();
867         String uscript = "new('org.apache.commons.jexl3.internal.introspection.Uberspect', null, null, perm).getClassLoader().loadClass('java.lang.System')";
868         JexlScript u0 = jexl.createScript(uscript, "perm");
869         return u0.execute(null, perm);
870     }
871 
872     @Test
873     void test450c() {
874         // can reach and invoke System::currentTimeMillis with UNRESTRICTED
875         assertNotNull(run450c(UNRESTRICTED));
876         // need explicit permissions to ClassPermissions and Uberspect to reach and invoke System::currentTimeMillis
877         JexlPermissions perm = new JexlPermissions.ClassPermissions(
878             JexlPermissions.ClassPermissions.class,
879             org.apache.commons.jexl3.internal.introspection.Uberspect.class);
880         assertNotNull(run450c(perm));
881         // cannot reach and invoke System::currentTimeMillis with RESTRICTED
882         assertThrows(JxltEngine.Exception.class,
883                 () -> run450c(JexlPermissions.RESTRICTED), "should not be able to load System with RESTRICTED");
884     }
885 
886     private static Object run450c(JexlPermissions perm) {
887         JexlBuilder builder = new JexlBuilder().silent(false).permissions(perm);
888         Object result = new TemplateEngine(new Engine32(builder),false, 2, '$', '#').createExpression(
889             "${x = new ('org.apache.commons.jexl3.internal.introspection.Uberspect', null, null, UNRESTRICTED);" +
890             "sys = x?.getClassLoader()?.loadClass('java.lang.System') ?: SYSTEM;" + // fail to create uberspect with java 8
891             "p = new('org.apache.commons.jexl3.introspection.JexlPermissions$ClassPermissions', [sys]);" +
892             "c = new('org.apache.commons.jexl3.internal.introspection.Uberspect', null, null, p);" +
893             "z = c.getMethod(sys,'currentTimeMillis').invoke(x,null);}"
894         ).evaluate(new BrkContext());
895         return result;
896     }
897 
898     @Test
899     void test450() {
900         assertNotNull(run450(JexlPermissions.UNRESTRICTED),
901                 "should be able to reach and invoke System::currentTimeMillis with UNRESTRICTED");
902         assertNotNull(run450(new JexlPermissions.ClassPermissions(org.apache.commons.jexl3.internal.TemplateEngine.class)),
903                 "should be able to reach and invoke System::currentTimeMillis with TemplateEngine permission");
904         assertThrows(JexlException.Method.class,
905                 () -> run450(RESTRICTED),
906                 "should not be able to reach and invoke System::currentTimeMillis with RESTRICTED");
907     }
908 
909     public static class Engine33 extends Engine32 {
910         public Engine33() {
911             this(createBuilder());
912         }
913         public Engine33(JexlBuilder builder) {
914             super(builder);
915         }
916         static JexlBuilder createBuilder() {
917             JexlPermissions perm = new JexlPermissions.ClassPermissions(
918                     Issues400Test.class.getClassLoader().getClass(),
919                     JexlPermissions.ClassPermissions.class,
920                     org.apache.commons.jexl3.internal.TemplateEngine.class,
921                     org.apache.commons.jexl3.internal.introspection.Uberspect.class);
922             return new JexlBuilder().safe(false).silent(false).permissions(perm);
923         }
924     }
925 
926     private static Object run450(JexlPermissions perm) {
927         JexlEngine jexl = new JexlBuilder().silent(false).strict(true).safe(false).permissions(perm).create();
928         return jexl.createScript("new('org.apache.commons.jexl3.internal.TemplateEngine'," +
929             "new('org.apache.commons.jexl3.Issues400Test$Engine33'),false,256,'$'.charAt(0),'#'.charAt(0))" +
930                 ".createExpression(" +
931                     "\"#{x = new ('org.apache.commons.jexl3.internal.introspection.Uberspect', null, null);" +
932                     "sys = x?.getClassLoader().loadClass('java.lang.System') ?: SYSTEM;" + // fail to load System on Java 8
933                     "p = new('org.apache.commons.jexl3.introspection.JexlPermissions$ClassPermissions', [sys]);" +
934                     "c = new('org.apache.commons.jexl3.internal.introspection.Uberspect', null, null, p);" +
935                     "z = c.getMethod(sys,'currentTimeMillis').invoke(x,null);}\")" +
936                     ".evaluate(new('org.apache.commons.jexl3.Issues400Test$BrkContext'))").execute(null);
937     }
938 
939     @Test
940     void test451() {
941         JexlEngine jexl = new JexlBuilder().create();
942         assertEquals("42",
943                 jexl.createScript("o.toString()", "o").execute(null, "42"));
944         JexlPermissions perms = RESTRICTED.compose("java.lang { +Class { getSimpleName(); } }");
945         JexlSandbox sandbox = new JexlSandbox(false, true);
946         sandbox.permissions(Object.class.getName(), true, true, false, false);
947         sandbox.allow(String.class.getName()).execute("toString");
948         final JexlEngine jexl451 = new JexlBuilder().safe(false).silent(false).permissions(perms).sandbox(sandbox).create();
949         // sandbox allows String::toString
950         assertEquals("42",
951                 jexl451.createScript("o.toString()", "o").execute(null, "42"));
952         // sandbox forbids getClass
953         assertThrows(JexlException.Method.class,
954                 () -> jexl451.createScript("oo.getClass()", "oo").execute(null, "42"));
955         // sandbox allows reading properties, permissions allow getClass
956         assertEquals(String.class,
957                 jexl451.createScript("o.class", "o").execute(null, "42"));
958         // sandbox allows reading properties, permissions allow getSimpleName
959         assertEquals("Object",
960                 jexl451.createScript("o.class.simpleName", "o").execute(null, new Object()));
961         // sandbox allows reading properties, permissions forbids getClassLoader
962         assertThrows(JexlException.Property.class,
963                 () -> jexl451.createScript("o.class.classLoader", "o").execute(null, new Object()));
964     }
965 }
966