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