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  import java.util.function.Function;
45  
46  import org.apache.commons.jexl3.internal.Debugger;
47  import org.apache.commons.jexl3.internal.Engine32;
48  import org.apache.commons.jexl3.internal.Scope;
49  import org.apache.commons.jexl3.internal.TemplateEngine;
50  import org.apache.commons.jexl3.introspection.JexlPermissions;
51  import org.apache.commons.jexl3.introspection.JexlSandbox;
52  import org.apache.commons.jexl3.parser.ASTJexlScript;
53  import org.apache.commons.jexl3.parser.JexlScriptParser;
54  import org.apache.commons.jexl3.parser.Parser;
55  import org.apache.commons.jexl3.parser.StringProvider;
56  import org.junit.jupiter.api.BeforeAll;
57  import org.junit.jupiter.api.Test;
58  
59  /**
60   * Test cases for reported issue between JEXL-300 and JEXL-399.
61   */
62  public class Issues400Test {
63  
64      public static class VinzCaller {
65          private final JexlContext context;
66  
67          VinzCaller(final JexlContext context) {
68              this.context = context;
69          }
70  
71          public Object execute(final JexlScript script) {
72              return script.execute(context);
73          }
74      }
75  
76      public static class VinzContext extends MapContext {
77          public String member(final String m, final String u) {
78              return m + '.' + u;
79          }
80      }
81  
82      /**
83       * Any function in a context can be used as a method of its first parameter. Overloads are respected.
84       */
85      public static class XuContext extends MapContext {
86  
87          public String join(final int[] list, final String str) {
88              return join(Arrays.stream(list).iterator(), str);
89          }
90  
91          public String join(final Iterable<?> list, final String str) {
92              return join(list.iterator(), str);
93          }
94  
95          public String join(final Iterator<?> iterator, final String str) {
96              if (!iterator.hasNext()) {
97                  return "";
98              }
99              final StringBuilder strb = new StringBuilder(256);
100             strb.append(iterator.next().toString());
101             while (iterator.hasNext()) {
102                 strb.append(str);
103                 strb.append(Objects.toString(iterator.next(), "?"));
104             }
105             return strb.toString();
106         }
107     }
108 
109     private static void run404(final JexlEngine jexl, final String src, final Object... a) {
110         final JexlScript script = jexl.createScript(src, "a", "b");
111         if (!src.endsWith(";")) {
112             assertEquals(script.getSourceText(), script.getParsedText());
113         }
114         final Object result = script.execute(null, a);
115         assertEquals(42, result);
116     }
117 
118     @BeforeAll
119     static void setUpClass() {
120         JexlBuilder.setDefaultPermissions(JexlTestCase.TEST_PERMS);
121     }
122 
123     @Test
124     void test402() {
125         final JexlContext jc = new MapContext();
126         // @formatter:off
127       final String[] sources = {
128         "if (true) { return }",
129         "if (true) { 3; return }",
130         "(x->{ 3; return })()"
131       };
132       // @formatter:on
133         final JexlEngine jexl = new JexlBuilder().create();
134         for (final String source : sources) {
135             final JexlScript e = jexl.createScript(source);
136             final Object o = e.execute(jc);
137             assertNull(o);
138         }
139     }
140 
141     @Test
142     void test403() {
143         // @formatter:off
144         final String[] strings = {
145             "  map1.`${item.a}` = 1;\n",
146             "  map1[`${item.a}`] = 1;\n",
147             "  map1[item.a] = 1;\n"
148          };
149         // @formatter:on
150         for (final String setmap : strings) {
151             // @formatter:off
152             final String src = "var a = {'a': 1};\n" +
153                 "var list = [a, a];\n" +
154                 "let map1 = {:};\n" +
155                 "for (let item : list) {\n" +
156                 setmap +
157                 "}\n " +
158                 "map1";
159             // @formatter:on
160             final JexlEngine jexl = new JexlBuilder().cache(64).create();
161             final JexlScript script = jexl.createScript(src);
162             for (int i = 0; i < 2; ++i) {
163                 final Object result = script.execute(null);
164                 assertInstanceOf(Map.class, result);
165                 final Map<?, ?> map = (Map<?, ?>) result;
166                 assertEquals(1, map.size());
167                 final Object val = jexl.createScript("m -> m[1]").execute(null, map);
168                 assertEquals(1, val);
169             }
170         }
171     }
172 
173     @Test
174     void test404a() {
175         final JexlEngine jexl = new JexlBuilder().cache(64).strict(true).safe(false).create();
176         Map<String, Object> a = Collections.singletonMap("b", 42);
177         // access is constant
178         for (final String src : new String[]{"a.b", "a?.b", "a['b']", "a?['b']", "a?.`b`"}) {
179             run404(jexl, src, a);
180             run404(jexl, src + ";", a);
181         }
182         // access is variable
183         for (final String src : new String[]{"a[b]", "a?[b]", "a?.`${b}`"}) {
184             run404(jexl, src, a, "b");
185             run404(jexl, src + ";", a, "b");
186         }
187         // add a 3rd access
188         final Map<String, Object> b = Collections.singletonMap("c", 42);
189         a = Collections.singletonMap("b", b);
190         for (final String src : new String[]{"a[b].c", "a?[b]?['c']", "a?.`${b}`.c"}) {
191             run404(jexl, src, a, "b");
192         }
193     }
194 
195     @Test
196     void test404b() {
197         // @formatter:off
198       final JexlEngine jexl = new JexlBuilder()
199           .cache(64)
200           .strict(true)
201           .safe(false)
202           .create();
203       // @formatter:on
204         final Map<String, Object> b = Collections.singletonMap("c", 42);
205         final Map<String, Object> a = Collections.singletonMap("b", b);
206         JexlScript script;
207         Object result;
208         script = jexl.createScript("a?['B']?['C']", "a");
209         result = script.execute(null, a);
210         assertEquals(script.getSourceText(), script.getParsedText());
211         assertNull(result);
212         script = jexl.createScript("a?['b']?['C']", "a");
213         assertEquals(script.getSourceText(), script.getParsedText());
214         result = script.execute(null, a);
215         assertNull(result);
216         script = jexl.createScript("a?['b']?['c']", "a");
217         assertEquals(script.getSourceText(), script.getParsedText());
218         result = script.execute(null, a);
219         assertEquals(42, result);
220         script = jexl.createScript("a?['B']?['C']?: 1042", "a");
221         assertEquals(script.getSourceText(), script.getParsedText());
222         result = script.execute(null, a);
223         assertEquals(1042, result);
224         // can still do ternary, note the space between ? and [
225         script = jexl.createScript("a? ['B']:['C']", "a");
226         result = script.execute(null, a);
227         assertArrayEquals(new String[]{"B"}, (String[]) result);
228         script = jexl.createScript("a?['b'] ?: ['C']", "a");
229         result = script.execute(null, a);
230         assertEquals(b, result);
231         script = jexl.createScript("a?['B'] ?: ['C']", "a");
232         result = script.execute(null, a);
233         assertArrayEquals(new String[]{"C"}, (String[]) result);
234     }
235 
236     /**
237      * The safe-navigation bit of an array access must be keyed to the bracket (child) position,
238      * not to the count of '?[' seen (JEXL-security f020). A mixed chain like 'a[b]?[c]' must mark
239      * only the second child as safe; round-tripping through the Debugger proves the right child
240      * carries the '?'.
241      */
242     @Test
243     void testSafeNavBitIndexing() {
244         // @formatter:off
245         final JexlEngine jexl = new JexlBuilder()
246             .cache(64)
247             .strict(true)
248             .safe(false)
249             .create();
250         // @formatter:on
251         // Debugger reconstructs '?[' from isSafeChild(i); mixed chains must round-trip verbatim.
252         final String[] sources = {
253             "a[b]?[c]",
254             "a?[b][c]",
255             "a[b][c]?[d]",
256             "a?[b]?[c]",
257             "a[b][c]"
258         };
259         for (final String src : sources) {
260             final JexlScript script = jexl.createScript(src, "a", "b", "c", "d");
261             assertEquals(src, script.getParsedText(), src);
262         }
263         // and the leading-non-safe / trailing-safe split behaves at evaluation time:
264         // a is present, a[b] is null -> the trailing ?[c] shields the null, yielding null (not a throw).
265         final Map<String, Object> a = Collections.singletonMap("x", 42);
266         final JexlScript shielded = jexl.createScript("a[b]?[c]", "a", "b", "c");
267         assertNull(shielded.execute(null, a, "missing", "c"));
268         // whereas the leading, non-safe [b] on a null base still throws.
269         final JexlScript unshielded = jexl.createScript("a[b]?[c]", "a", "b", "c");
270         assertThrows(JexlException.class, () -> unshielded.execute(null, null, "b", "c"));
271     }
272 
273     @Test
274     void test406a() {
275         // @formatter:off
276         final JexlEngine jexl = new JexlBuilder()
277             .cache(64)
278             .strict(true)
279             .safe(false)
280             .create();
281         // @formatter:on
282 
283         final JexlContext context = new XuContext();
284         // @formatter:off
285         final List<String> list = Arrays.asList(
286             "[1, 2, 3, 4, ...].join('-')", // List<Integer>
287             "[1, 2, 3, 4,].join('-')", // int[]
288             "(1 .. 4).join('-')", // iterable<Integer>
289             "join([1, 2, 3, 4, ...], '-')",
290             "join([1, 2, 3, 4], '-')",
291             "join((1 .. 4), '-')");
292         // @formatter:on
293         for (final String src : list) {
294             final JexlScript script = jexl.createScript(src);
295             final Object result = script.execute(context);
296             assertEquals("1-2-3-4", result, src);
297         }
298 
299         final String src0 = "x.join('*')";
300         final JexlScript script0 = jexl.createScript(src0, "x");
301         final String src1 = "join(x, '*')";
302         final JexlScript script1 = jexl.createScript(src1, "x");
303         for (final Object x : Arrays.asList(Arrays.asList(1, 2, 3, 4), new int[]{1, 2, 3, 4})) {
304             Object result = script0.execute(context, x);
305             assertEquals("1*2*3*4", result, src0);
306             result = script1.execute(context, x);
307             assertEquals("1*2*3*4", result, src1);
308         }
309     }
310 
311     @Test
312     void test407() {
313         // Java version
314         final double r = 99.0d + 7.82d - 99.0d - 7.82d;
315         assertEquals(0d, r, 8.e-15); // Not zero, IEEE 754
316         // jexl
317         final JexlEngine jexl = new JexlBuilder().create();
318         final JexlScript script = jexl.createScript("a + b - a - b", "a", "b");
319         // using doubles, same as Java
320         Number result = (Number) script.execute(null, 99.0d, 7.82d);
321         assertEquals(0d, result.doubleValue(), 8.e-15);
322         // using BigdDecimal, more precise, still not zero
323         result = (Number) script.execute(null, new BigDecimal("99.0"), new BigDecimal("7.82"));
324         assertEquals(0d, result.doubleValue(), 3.e-32);
325     }
326 
327     @Test
328     void test412() {
329         final Map<Object, Object> ctl = new HashMap<>();
330         ctl.put("one", 1);
331         ctl.put("two", 2);
332         final String fnsrc0 = "function f(x) { x }\n" + "let one = 'one', two = 'two';\n";
333         // @formatter:off
334         final List<String> list = Arrays.asList(
335             "{ one : f(1), two:f(2) }",
336             "{ one: f(1), two: f(2) }",
337             "{ one: f(1), two:f(2) }",
338             "{ one :f(1), two:f(2) }");
339         // @formatter:on
340         for (final String map0 : list) {
341             final String fnsrc = fnsrc0 + map0;
342             final JexlContext jc = new MapContext();
343             final JexlEngine jexl = new JexlBuilder().create();
344             final JexlScript e = jexl.createScript(fnsrc);
345             final Object o = e.execute(jc);
346             assertInstanceOf(Map.class, o);
347             final Map<?, ?> map = (Map<?, ?>) o;
348             assertEquals(map, ctl);
349         }
350     }
351 
352     @Test
353     void test413a() {
354         final JexlBuilder builder = new JexlBuilder();
355         final JexlEngine jexl = builder.create();
356         final JexlScript script = jexl.createScript("var c = 42; var f = y -> c += y; f(z)", "z");
357         final Number result = (Number) script.execute(null, 12);
358         assertEquals(54, result);
359     }
360 
361     @Test
362     void test413b() {
363         final JexlBuilder builder = new JexlBuilder();
364         final JexlOptions options = builder.options();
365         options.setConstCapture(true);
366         options.setLexical(true);
367         final JexlEngine jexl = builder.create();
368         final JexlScript script = jexl.createScript("var c = 42; var f = y -> c += y; f(z)", "z");
369         final JexlException.Variable xvar = assertThrows(JexlException.Variable.class,
370             () -> script.execute(null, 12), "c should be const");
371         assertEquals("c", xvar.getVariable());
372     }
373 
374     @Test
375     void test413c() {
376         final JexlBuilder builder = new JexlBuilder();
377         final JexlEngine jexl = builder.create();
378         final String pragma = "#pragma jexl.options '+constCapture'\n"
379             + "var c = 42; var f = y -> c += y; f(z)";
380         final JexlScript script = jexl.createScript(pragma, "z");
381         final JexlException.Variable xvar = assertThrows(
382             JexlException.Variable.class, () -> script.execute(null, 12), "c should be const");
383         assertEquals("c", xvar.getVariable());
384     }
385 
386     @Test
387     void test413d() {
388         final JexlBuilder builder = new JexlBuilder().features(new JexlFeatures().constCapture(true));
389         final JexlEngine jexl = builder.create();
390         final JexlException.Parsing xparse = assertThrows(JexlException.Parsing.class,
391             () -> jexl.createScript("var c = 42; var f = y -> c += y; f(z)", "z"),
392             "c should be const");
393         assertTrue(xparse.getMessage().contains("const"));
394     }
395 
396     @Test
397     void test415() {
398         final JexlBuilder builder = new JexlBuilder().features(new JexlFeatures().constCapture(true));
399         final JexlEngine jexl = builder.create();
400         JexlScript script;
401         Object result;
402         script = jexl.createScript("`#${c}`", "c");
403         result = script.execute(null, 42);
404         assertEquals("#42", result.toString());
405         script = jexl.createScript("`$${c}`", "c");
406         result = script.execute(null, 42);
407         assertEquals("$42", result.toString());
408         script = jexl.createScript("`$#{c}`", "c");
409         result = script.execute(null, 42);
410         assertEquals("$42", result.toString());
411         script = jexl.createScript("`##{c}`", "c");
412         result = script.execute(null, 42);
413         assertEquals("#42", result.toString());
414         script = jexl.createScript("`--##{c}`", "c");
415         result = script.execute(null, 42);
416         assertEquals("--#42", result.toString());
417     }
418 
419     @Test
420     void test419() throws NoSuchMethodException {
421         // check RESTRICTED permissions denies call to System::currentTimeMillis()
422         final Method currentTimeMillis = System.class.getMethod("currentTimeMillis");
423         assertFalse(RESTRICTED.allow(currentTimeMillis));
424         // compose using a positive class permission to allow just System::currentTimeMillis()
425         final JexlPermissions permissions = RESTRICTED.compose("java.lang { +System { currentTimeMillis(); } }");
426         // check no side effect on compose
427         assertTrue(permissions.allow(currentTimeMillis));
428         assertFalse(RESTRICTED.allow(currentTimeMillis));
429 
430         // An engine with the System class as namespace and the positive permissions
431         final JexlEngine jexl = new JexlBuilder()
432             .namespaces(Collections.singletonMap("sns", System.class))
433             .permissions(permissions)
434             .create();
435 
436         final AtomicLong result = new AtomicLong();
437         assertEquals(0, result.get());
438         final long now = System.currentTimeMillis();
439         // calling System::currentTimeMillis() is allowed and behaves as expected
440         jexl.createScript("result.set(sns:currentTimeMillis())", "result").execute(null, result);
441         assertTrue(result.get() >= now);
442 
443         // we still cant call anything else
444         final JexlScript script = jexl.createScript("sns:gc()");
445         final JexlException.Method method = assertThrows(JexlException.Method.class, () -> script.execute(null));
446         assertEquals("gc", method.getMethod());
447 
448     }
449 
450     @Test
451     void testDocBreakContinue() {
452         final JexlBuilder builder = new JexlBuilder().features(new JexlFeatures().constCapture(true));
453         final JexlEngine jexl = builder.create();
454         JexlScript script;
455         Object result;
456         // @formatter:off
457         final String srcContinue = "let text = '';\n" +
458             "for (let i : (4..2)) { if (i == 3) continue; text += i; }\n" +
459             "text;";
460         // @formatter:on
461         script = jexl.createScript(srcContinue);
462         result = script.execute(null);
463         assertEquals("42", result);
464         // @formatter:off
465         final String srcBreak = "let i = 33;\n" +
466             "while (i < 66) { if (i == 42) { break; } i += 1; }\n" +
467             "i;";
468         // @formatter:on
469         script = jexl.createScript(srcBreak);
470         result = script.execute(null);
471         assertEquals(42, result);
472     }
473 
474     @Test
475     void testNamespaceVsTernary0() {
476         final VinzContext ctxt = new VinzContext();
477         ctxt.set("Users", "USERS");
478         final JexlEngine jexl = new JexlBuilder().safe(false).strict(true).silent(false).create();
479         // @formatter:off
480         JexlScript script = jexl.createScript("() -> {\n"
481             + "  var fn = (user) -> {\n"
482             + "     user ? user : member(Users, 'user');\n"
483             + "  }\n"
484             + "}");
485         // @formatter:on
486         Object r = script.execute(ctxt);
487         assertNotNull(r);
488         script = (JexlScript) r;
489         r = script.execute(ctxt);
490         assertEquals("USERS.user", r);
491     }
492 
493     @Test
494     void testNamespaceVsTernary1() {
495         final VinzContext ctxt = new VinzContext();
496         ctxt.set("Users", "USERS");
497         ctxt.set("vinz", new VinzCaller(ctxt));
498         final JexlEngine jexl = new JexlBuilder().safe(false).strict(true).silent(false).create();
499         // @formatter:off
500         final JexlScript script = jexl.createScript(
501             "vinz.execute(() -> {\n"
502             + "  var test = 42;\n"
503             + "  var user = useTest ? test : member(Users, 'user');\n"
504             + "})\n" , "useTest");
505         // @formatter:on
506         Object r = script.execute(ctxt, false);
507         assertNotNull(r);
508         assertEquals("USERS.user", r);
509         r = script.execute(ctxt, true);
510         assertNotNull(r);
511         assertEquals(42, r);
512     }
513 
514     public static class Ns429 {
515         public int f(final int x) {
516             return x * 10000 + 42;
517         }
518     }
519 
520     @Test
521     void test429a() {
522         final MapContext ctxt = new MapContext();
523         final JexlFeatures features = JexlFeatures.createDefault();
524         final JexlEngine jexl = new JexlBuilder().features(features).safe(false).strict(true).silent(false).create();
525         final JexlScript f = jexl.createScript("x -> x");
526         ctxt.set("f", f);
527         String src = "#pragma jexl.namespace.b " + Ns429.class.getName() + "\n" + "b ? b : f(2);";
528         JexlScript script = jexl.createScript(src, "b");
529         assertEquals(1, (int) script.execute(ctxt, 1));
530 
531         src = "#pragma jexl.namespace.b " + Ns429.class.getName() + "\n" + "b ? b:f(2) : 1;";
532         script = jexl.createScript(src, "b");
533         assertEquals(20042, (int) script.execute(ctxt, 1));
534     }
535 
536     @Test
537     void test429b() {
538         final MapContext ctxt = new MapContext();
539         ctxt.set("b", 1);
540         final JexlFeatures features = JexlFeatures.createDefault();
541         features.namespaceIdentifier(true);
542         final JexlEngine jexl = new JexlBuilder().features(features).safe(false).strict(true).silent(false).create();
543         final JexlScript f = jexl.createScript("x -> x");
544         ctxt.set("f", f);
545         String src = "#pragma jexl.namespace.b " + Ns429.class.getName() + "\n" + "b ? b : f(2);";
546         JexlScript script = jexl.createScript(src);
547         assertEquals(1, (int) script.execute(ctxt));
548 
549         src = "#pragma jexl.namespace.b " + Ns429.class.getName() + "\n" + "b ? b:f(2) : 1;";
550         script = jexl.createScript(src);
551         assertEquals(20042, (int) script.execute(ctxt));
552     }
553 
554     @Test
555     void test431a() {
556         final JexlEngine jexl = new JexlBuilder().create();
557         final String src = "let x = 0; try { x += 19 } catch (let error) { return 169 } "
558             + "try { x += 23 } catch (let error) { return 169 }";
559         final JexlScript script = jexl.createScript(src);
560         assertNotNull(script);
561         final Object result = script.execute(null);
562         assertEquals(42, result);
563     }
564 
565     Closeable foo() {
566         return null;
567     }
568 
569     @Test
570     void test431b() {
571         final JexlEngine jexl = new JexlBuilder().create();
572         final String src = "let x = 0; try(let error) { x += 19 } catch (let error) { return 169 } "
573             + "try { x += 23 } catch (let error) { return 169 }";
574         final JexlScript script = jexl.createScript(src);
575         assertNotNull(script);
576         final Object result = script.execute(null);
577         assertEquals(42, result);
578     }
579 
580     @Test
581     void test431c() {
582         final JexlEngine jexl = new JexlBuilder().create();
583         final String src = "let xx = 0; try { xx += 19 } catch (let xx) { return 169 }";
584         try {
585             final JexlScript script = jexl.createScript(src);
586             fail("xx is already defined in scope");
587         } catch (final JexlException.Parsing parsing) {
588             assertTrue(parsing.getDetail().contains("xx"));
589         }
590     }
591 
592     @Test
593     void test433() {
594         final JexlEngine jexl = new JexlBuilder().create();
595         final String src = "let condition = true; if (condition) { return; }";
596         final JexlScript script = jexl.createScript(src);
597         assertNotNull(script);
598         final Object result = script.execute(null);
599         assertNull(result);
600         final Debugger debugger = new Debugger();
601         assertTrue(debugger.debug(script));
602         final String dbgStr = debugger.toString();
603         assertTrue(JexlTestCase.equalsIgnoreWhiteSpace(src, dbgStr));
604     }
605 
606     @Test
607     void test434() {
608         final JexlEngine jexl = new JexlBuilder().safe(false).strict(true).create();
609         final String src = "let foo = null; let value = foo?[bar]";
610         final JexlScript script = jexl.createScript(src);
611         assertNotNull(script);
612         final Object result = script.execute(null);
613         assertNull(result);
614     }
615 
616     public static class Arithmetic435 extends JexlArithmetic {
617         public Arithmetic435(final boolean strict) {
618             super(strict);
619         }
620 
621         public Object empty(final String type) {
622             if ("list".equals(type)) {
623                 return Collections.emptyList();
624             }
625             return null;
626         }
627     }
628 
629     @Test
630     void test435() {
631         final JexlArithmetic arithmetic = new Arithmetic435(true);
632         final JexlEngine jexl = new JexlBuilder().arithmetic(arithmetic).create();
633         final String src = "empty('list')";
634         final JexlScript script = jexl.createScript(src);
635         assertNotNull(script);
636         final Object result = script.execute(null);
637         assertInstanceOf(List.class, result);
638     }
639 
640     @Test
641     void test436a() {
642         final String[] srcs = {"let i = null; ++i", "let i; ++i;", "let i; i--;", "let i; i++;"};
643         run436(null, srcs);
644     }
645 
646     @Test
647     void test436b() {
648         final String[] srcs = {"var i = null; ++i", "var i; ++i;", "var i; i--;", "var i; i++;"};
649         run436(null, srcs);
650     }
651 
652     @Test
653     void test436c() {
654         final JexlContext ctxt = new MapContext();
655         ctxt.set("i", null);
656         final String[] srcs = {"++i", "++i;", "i--;", "i++;"};
657         run436(null, srcs);
658     }
659 
660     void run436(final JexlContext ctxt, final String[] srcs) {
661         final JexlEngine jexl = new JexlBuilder().create();
662         for (final String src : srcs) {
663             final JexlScript script = jexl.createScript(src);
664             assertThrows(JexlException.Operator.class, () -> script.execute(ctxt));
665         }
666     }
667 
668     @Test
669     void test437a() {
670         final JexlEngine jexl = new JexlBuilder().create();
671         final String src = "let values = [...]\n"
672             + "function append(const value) {\n"
673             + "  values.add(value)\n"
674             + "}\n"
675             + "\n"
676             + "append(1)\n"
677             + "append(2)\n"
678             + "return values ";
679         final JexlScript script = jexl.createScript(src);
680         assertNotNull(script);
681         final Object result = script.execute(null);
682         assertInstanceOf(List.class, result);
683         final List<?> values = (List<?>) result;
684         assertEquals(2, values.size());
685     }
686 
687     @Test
688     void test437b() {
689         final JexlFeatures features = JexlFeatures.createDefault().ambiguousStatement(true);
690         assertTrue(features.supportsAmbiguousStatement());
691         final JexlEngine jexl = new JexlBuilder().features(features).create();
692         final String src = "let values = [...]"
693             + "function append(const value) {"
694             + "  values.add(value)"
695             + "}"
696             + "append(1)"
697             + "append(2)"
698             + "return values ";
699         final JexlScript script = jexl.createScript(src);
700         assertNotNull(script);
701         final Object result = script.execute(null);
702         assertInstanceOf(List.class, result);
703         final List<?> values = (List<?>) result;
704         assertEquals(2, values.size());
705     }
706 
707     /**
708      * The set of characters that may be followed by a '='.
709      */
710     static final char[] EQ_FRIEND;
711 
712     static {
713         final char[] eq = {'!', ':', '<', '>', '^', '|', '&', '+', '-', '/', '*', '~', '='};
714         Arrays.sort(eq);
715         EQ_FRIEND = eq;
716     }
717 
718     /**
719      * Transcodes a SQL-inspired expression to a JEXL expression.
720      *
721      * @param expr The expression to transcode
722      * @return The resulting expression
723      */
724     private static String transcodeSQLExpr(final CharSequence expr) {
725         final StringBuilder strb = new StringBuilder(expr.length());
726         final int end = expr.length();
727         char previous = 0;
728         for (int i = 0; i < end; ++i) {
729             final char c = expr.charAt(i);
730             if (previous == '<') {
731                 // previous char a '<' now followed by '>'
732                 if (c == '>') {
733                     // replace '<>' with '!='
734                     strb.append("!=");
735                     previous = c;
736                     continue;
737                 }
738                 strb.append('<');
739             }
740             if (c != '<') {
741                 if (c == '=') {
742                     // replace '=' with '==' when it does not follow a 'friend'
743                     if (Arrays.binarySearch(EQ_FRIEND, previous) >= 0) {
744                         strb.append(c);
745                     } else {
746                         strb.append("==");
747                     }
748                 } else {
749                     strb.append(c);
750                     if (c == '"' || c == '\'') {
751                         // read string, escape '\'
752                         boolean escape = false;
753                         for (i += 1; i < end; ++i) {
754                             final char ec = expr.charAt(i);
755                             strb.append(ec);
756                             if (ec == '\\') {
757                                 escape = !escape;
758                             } else if (escape) {
759                                 escape = false;
760                             } else if (ec == c) {
761                                 break;
762                             }
763                         }
764                     }
765                 }
766             }
767             previous = c;
768         }
769         return strb.toString();
770     }
771 
772     public static class SQLParser implements JexlScriptParser {
773         final Parser parser;
774 
775         public SQLParser() {
776             parser = new Parser(new StringProvider(";"));
777         }
778 
779         @Override
780         public ASTJexlScript parse(final JexlInfo info, final JexlFeatures features, final String src, final Scope scope) {
781             return parser.parse(info, features, transcodeSQLExpr(src), scope);
782         }
783 
784         @Override
785         public ASTJexlScript jxltParse(final JexlInfo info, final JexlFeatures features, final String src, final Scope scope) {
786             return new Parser(parser).parse(info, features, transcodeSQLExpr(src), scope);
787         }
788     }
789 
790 
791     @Test
792     void testSQLTranspose() {
793         final String[] e = {"a<>b", "a = 2", "a.b.c <> '1<>0'"};
794         final String[] j = {"a!=b", "a == 2", "a.b.c != '1<>0'"};
795         for (int i = 0; i < e.length; ++i) {
796             final String je = transcodeSQLExpr(e[i]);
797             assertEquals(j[i], je);
798         }
799     }
800 
801     @Test
802     void testSQLNoChange() {
803         final String[] e = {"a <= 2", "a >= 2", "a := 2", "a + 3 << 4 > 5",};
804         for (final String element : e) {
805             final String je = transcodeSQLExpr(element);
806             assertEquals(element, je);
807         }
808     }
809 
810     @Test
811     void test438() {// no local, no lambda, no loops, no-side effects
812         final JexlFeatures f = new JexlFeatures()
813             .localVar(false)
814             .lambda(false)
815             .loops(false)
816             .sideEffect(false)
817             .sideEffectGlobal(false);
818         final JexlBuilder builder = new JexlBuilder().parserFactory(SQLParser::new).cache(32).features(f);
819         final JexlEngine sqle = builder.create();
820         assertTrue((boolean) sqle.createScript("a <> 25", "a").execute(null, 24));
821         assertFalse((boolean) sqle.createScript("a <> 25", "a").execute(null, 25));
822         assertFalse((boolean) sqle.createScript("a = 25", "a").execute(null, 24));
823         assertTrue((boolean) sqle.createScript("a != 25", "a").execute(null, 24));
824         assertTrue((boolean) sqle.createScript("a = 25", "a").execute(null, 25));
825         assertFalse((boolean) sqle.createScript("a != 25", "a").execute(null, 25));
826     }
827 
828     @Test
829     void testIssue441() {
830         final JexlEngine jexl = new JexlBuilder().create();
831         String ctl = "\nab\nc`d\n";
832         final JexlExpression e = jexl.createExpression("`\nab\nc\\`d\n`");
833         Object o = e.evaluate(null);
834         assertEquals(ctl, o);
835 
836         final JexlContext context = new MapContext();
837         context.set("name", "Hello");
838         final String code = "return `${name + '\\n' + name}`;";
839         final JexlScript script = jexl.createScript(code);
840         o = script.execute(context);
841         ctl = "Hello\nHello";
842         assertEquals(ctl, o);
843     }
844 
845     @Test
846     void testIssue442() {
847         final JexlEngine jexl = new JexlBuilder().create();
848         final JexlContext context = new MapContext();
849         final String code = "var x = 'hello';\n" + "function test(z) {\n" +
850             //"x + ' ' + z\n"+
851             "`${x} ${z}`;\n" + "}\n" + "test('world');";
852         final JexlScript script = jexl.createScript(code);
853         final Object result = script.execute(context);
854         assertEquals("hello world", result);
855     }
856 
857 
858     @Test
859     void testIssue447() {
860         final JexlEngine jexl = new JexlBuilder().create();
861         final String src = "const c = `${a}\n?= ${b}`; "
862             + "function foo(const left, const right) { `${left}\n?== ${right}` } "
863             + "c+foo(a, b)";
864         final JexlScript script = jexl.createScript(src, "a", "b");
865         final Object result = script.execute(null, "a", "b");
866         assertEquals("a\n?= ba\n?== b", result);
867         final String[] locals = script.getLocalVariables();
868         assertArrayEquals(new String[]{"c", "foo"}, locals);
869         final String TEST447 = "src/test/scripts/test447.jexl";
870         final File src447 = new File(TEST447);
871         final JexlScript script447 = jexl.createScript(src447);
872         final Object result447 = script447.execute(null);
873         assertInstanceOf(List.class, result447);
874         @SuppressWarnings("unchecked") final List<Boolean> list = (List<Boolean>) result447;
875         for (final Boolean item : list) {
876             assertTrue(item);
877         }
878     }
879 
880     public static class BrkContext extends MapContext {
881         public BrkContext() {
882             set("SYSTEM", System.class);
883             set("UNRESTRICTED", UNRESTRICTED);
884         }
885 
886         public static Object brk(final Object debug) {
887             return debug;
888         }
889 
890     }
891 
892     @Test
893     void test450a() {
894         final JexlEngine jexl0 = new JexlBuilder().silent(false).permissions(JexlPermissions.RESTRICTED).create();
895         assertThrows(JexlException.Method.class,
896             () -> jexl0.newInstance("org.apache.commons.jexl3.internal.introspection.Uberspect", null, null),
897             "should not be able to create Uberspect with RESTRICTED");
898         final JexlPermissions perm = new JexlPermissions.ClassPermissions(
899             org.apache.commons.jexl3.internal.introspection.Uberspect.class);
900         final JexlEngine jexl1 = new JexlBuilder().silent(false).permissions(perm).create();
901         assertNotNull(jexl1.newInstance(
902             "org.apache.commons.jexl3.internal.introspection.Uberspect", null, null),
903             "should able to create Uberspect with Uberspect permission");
904 
905     }
906 
907     @Test
908     void test450b() throws NoSuchMethodException {
909         // cannot load System with RESTRICTED
910         assertThrows(JexlException.Method.class, () -> run450b(JexlPermissions.RESTRICTED),
911             "should not be able to load System with RESTRICTED");
912         // can load System.exit with UNRESTRICTED
913         final Method exitMethod = java.lang.System.class.getMethod("exit", int.class);
914         assertTrue(UNRESTRICTED.allow(exitMethod));
915         assertFalse(RESTRICTED.allow(exitMethod));
916         assertEquals(java.lang.System.class, run450b(UNRESTRICTED));
917         // need to explicitly allow Uberspect and the current class loader to load System
918         final Class<? extends ClassLoader> cloader = getClass().getClassLoader().getClass();
919         final JexlPermissions perm = new JexlPermissions.ClassPermissions(
920             getClass().getClassLoader().getClass(),
921             org.apache.commons.jexl3.internal.introspection.Uberspect.class);
922         assertTrue(perm.allow(cloader), "should be able to access ClassLoader");
923         assertTrue(perm.allow(cloader, cloader.getMethod("loadClass", String.class)), "should be able to access ClassLoader::loadClass");
924         assertEquals(java.lang.System.class, run450b(perm));
925     }
926 
927     private static Object run450b(final JexlPermissions perm) {
928         final JexlEngine jexl = new JexlBuilder().safe(false).strict(true).silent(false).permissions(perm).create();
929         final String uscript = "new('org.apache.commons.jexl3.internal.introspection.Uberspect', "
930             + "null, null, perm).getClassLoader().loadClass('java.lang.System')";
931         final JexlScript u0 = jexl.createScript(uscript, "perm");
932         return u0.execute(null, perm);
933     }
934 
935     @Test
936     void test450c() {
937         // can reach and invoke System::currentTimeMillis with UNRESTRICTED
938         assertNotNull(run450c(UNRESTRICTED));
939         // need explicit permissions to ClassPermissions and Uberspect to reach and invoke
940         // System::currentTimeMillis
941         final JexlPermissions perm = new JexlPermissions.ClassPermissions(
942             getClass().getClassLoader().getClass(),
943             JexlPermissions.ClassPermissions.class,
944             org.apache.commons.jexl3.internal.introspection.Uberspect.class,
945             org.apache.commons.jexl3.internal.introspection.MethodExecutor.class,
946             BrkContext.class);
947         assertNotNull(run450c(perm));
948         // cannot reach and invoke System::currentTimeMillis with RESTRICTED
949         assertThrows(JxltEngine.Exception.class, () -> run450c(JexlPermissions.RESTRICTED),
950             "should not be able to load System with RESTRICTED");
951     }
952 
953     private static Object run450c(final JexlPermissions perm) {
954         final JexlBuilder builder = new JexlBuilder().silent(false).permissions(perm);
955         final Object result = new TemplateEngine(new Engine32(builder), false, 2, '$', '#')
956             .createExpression(
957                 "${x = new ('org.apache.commons.jexl3.internal.introspection.Uberspect', "
958                 + "null, null, UNRESTRICTED);"
959                 + "sys = x?.getClassLoader()?.loadClass('java.lang.System') ?: SYSTEM;"
960                 + "p = new('org.apache.commons.jexl3.introspection.JexlPermissions$ClassPermissions', [sys]);"
961                 + "c = new('org.apache.commons.jexl3.internal.introspection.Uberspect', null, null, p);"
962                 + "z = brk(c.getMethod(sys,'currentTimeMillis')).invoke(x,null);}")
963             .evaluate(new BrkContext());
964         return result;
965     }
966 
967     @Test
968     void test450() throws Exception {
969         final JexlPermissions perms =  Engine33.createPermissions();
970         final Class<? extends ClassLoader> cl = Issues400Test.class.getClassLoader().getClass();
971         assertTrue(perms.allow(cl, cl.getMethod("loadClass", String.class)),
972             "should be able to access ClassLoader::loadClass with specific permissions");
973         assertNotNull(run450(JexlPermissions.UNRESTRICTED),
974             "should be able to reach and invoke System::currentTimeMillis with UNRESTRICTED");
975         assertNotNull(
976             run450(new JexlPermissions.ClassPermissions(perms,
977                 org.apache.commons.jexl3.internal.TemplateEngine.class)),
978             "should be able to reach and invoke System::currentTimeMillis with TemplateEngine permission");
979         assertThrows(JexlException.Method.class, () -> run450(RESTRICTED),
980             "should not be able to reach and invoke System::currentTimeMillis with RESTRICTED");
981     }
982 
983     public static class Engine33 extends Engine32 {
984         public Engine33() {
985             this(createBuilder());
986         }
987 
988         public Engine33(final JexlBuilder builder) {
989             super(builder);
990         }
991 
992         static JexlPermissions createPermissions() {
993             // Need a lot of things on top of JEXL37 to allow execution
994             return new JexlPermissions.ClassPermissions(JexlTestCase.TEST_PERMS,
995                 java.util.Arrays.asList(
996                     Engine33.class.getClassLoader().getClass().getCanonicalName(),
997                     Engine33.class.getCanonicalName(),
998                     JexlPermissions.ClassPermissions.class.getCanonicalName(),
999                     org.apache.commons.jexl3.internal.TemplateEngine.class.getCanonicalName(),
1000                     // the whole-class denial of TemplateEngine (TEST_PERMS) now covers its nested
1001                     // classes: the expression classes the script evaluates need an explicit allow
1002                     // (ClassPermissions matches exact canonical names at the class level)
1003                     org.apache.commons.jexl3.internal.TemplateEngine.class.getCanonicalName() + ".TemplateExpression",
1004                     org.apache.commons.jexl3.internal.TemplateEngine.class.getCanonicalName() + ".JexlBasedExpression",
1005                     org.apache.commons.jexl3.internal.TemplateEngine.class.getCanonicalName() + ".DeferredExpression",
1006                     org.apache.commons.jexl3.internal.introspection.Uberspect.class.getCanonicalName()));
1007         }
1008 
1009         static JexlBuilder createBuilder() {
1010             final JexlPermissions perm = createPermissions();
1011             return new JexlBuilder().safe(false).silent(false).permissions(perm);
1012         }
1013     }
1014 
1015     private static Object run450(final JexlPermissions perm) {
1016         final JexlEngine jexl = new JexlBuilder().silent(false).strict(true).safe(false)
1017             .permissions(perm).create();
1018         final String script = "new('org.apache.commons.jexl3.internal.TemplateEngine',"
1019             + "new('org.apache.commons.jexl3.Issues400Test$Engine33'),false,256,"
1020             + "'$'.charAt(0),'#'.charAt(0))"
1021             + ".createExpression("
1022             + "\"#{x = new ('org.apache.commons.jexl3.internal.introspection.Uberspect', null, null);"
1023             + "sys = x?.getClassLoader().loadClass('java.lang.System') ?: SYSTEM;"
1024             + "p = new('org.apache.commons.jexl3.introspection.JexlPermissions$ClassPermissions', [sys]);"
1025             + "c = new('org.apache.commons.jexl3.internal.introspection.Uberspect', null, null, p);"
1026             + "z = c.getMethod(sys,'currentTimeMillis').invoke(x,null);}\")"
1027             + ".evaluate(new('org.apache.commons.jexl3.Issues400Test$BrkContext'))";
1028         return jexl.createScript(script).execute(null);
1029     }
1030 
1031     @Test
1032     void test451() throws NoSuchMethodException {
1033         final JexlEngine jexl = new JexlBuilder().create();
1034         assertEquals("42", jexl.createScript("o.toString()", "o").execute(null, "42"));
1035         final JexlPermissions perms = RESTRICTED.compose("java.lang { +Class { getSimpleName(); } }");
1036         assertTrue(perms.allow(String.class.getMethod("toString")));
1037         final JexlSandbox sandbox = new JexlSandbox(false, true);
1038         sandbox.permissions(Object.class.getName(), true, true, false, false);
1039         sandbox.allow(String.class.getName()).execute("toString");
1040         final JexlEngine jexl451 = new JexlBuilder().safe(false).silent(false).permissions(perms).sandbox(sandbox).create();
1041         // sandbox allows String::toString
1042         assertEquals("42", jexl451.createScript("o.toString()", "o").execute(null, "42"));
1043         // sandbox forbids getClass
1044         assertThrows(JexlException.Method.class, () -> jexl451.createScript("oo.getClass()", "oo").execute(null, "42"));
1045         // sandbox allows reading properties, permissions allow getClass
1046         assertEquals(String.class, jexl451.createScript("o.class", "o").execute(null, "42"));
1047         // sandbox allows reading properties, permissions allow getSimpleName
1048         assertEquals("Object", jexl451.createScript("o.class.simpleName", "o").execute(null, new Object()));
1049         // sandbox allows reading properties, permissions forbids getClassLoader
1050         assertThrows(JexlException.Property.class,
1051             () -> jexl451.createScript("o.class.classLoader", "o").execute(null, new Object()));
1052     }
1053 
1054     @Test
1055     void testIssue455a() {
1056         final JexlEngine jexl = new JexlBuilder().create();
1057         final String code = "name -> `${name +\n\t\f\r name}`";
1058         final JexlScript script = jexl.createScript(code);
1059         final Object o = script.execute(null, "Hello");
1060         final String ctl = "HelloHello";
1061         assertEquals(ctl, o);
1062     }
1063 
1064     @Test
1065     void testIssue455b() {
1066         final JexlEngine jexl = new JexlBuilder().create();
1067         final String code = "name -> `${name}\n${name}`;";
1068         final JexlScript script = jexl.createScript(code);
1069         final Object o = script.execute(null, "Hello");
1070         final String ctl = "Hello\nHello";
1071         assertEquals(ctl, o);
1072     }
1073 
1074     @Test
1075     void testIssue455c() {
1076         final JexlEngine jexl = new JexlBuilder().create();
1077         final JexlContext context = new MapContext();
1078         context.set("name", "Hello");
1079         final JxltEngine jxlt = jexl.createJxltEngine();
1080         final JxltEngine.Template template = jxlt.createTemplate("<b>\n\t${name\n\t+\r\f name}\n</b>");
1081         final StringWriter writer = new StringWriter();
1082         template.evaluate(context, writer);
1083         assertEquals("<b>\n\tHelloHello\n</b>", writer.toString());
1084     }
1085 
1086     @Test
1087     void testIssue455d() {
1088         final JexlEngine jexl = new JexlBuilder().create();
1089         // 'ref' contains 'greeting' which is the name of the variable to expand
1090         final String code = "`#{${\nref\t}}\n#{${\rref\f}}`;";
1091         final JexlScript script = jexl.createScript(code, "ref", "greeting");
1092         final Object o = script.execute(null, "greeting", "Hello");
1093         final String ctl = "Hello\nHello";
1094         assertEquals(ctl, o);
1095     }
1096 
1097     @Test
1098     void testIssue455e() {
1099         final JexlEngine jexl = new JexlBuilder().create();
1100         // Evaluate nested immediate inside deferred at runtime using a parameterized script
1101         final String src = "(name, suffix) -> `#{name} Hello ${name} ! #{suffix}`";
1102         final JexlScript script = jexl.createScript(src);
1103         final Object result = script.execute(null, "World", "~");
1104         assertEquals("World Hello World ! ~", result);
1105     }
1106 
1107     @Test
1108     void testIssue455f() {
1109         final JexlEngine jexl = new JexlBuilder().create();
1110         // Evaluate nested immediate inside deferred at runtime using a parameterized script
1111         final String src = "(name, suffix) -> `#{name + ' Hello'} ${name + ' !'} #{suffix}`";
1112         final JexlScript script = jexl.createScript(src);
1113         final Object result = script.execute(null, "World", "~");
1114         assertEquals("World Hello World ! ~", result);
1115     }
1116 
1117     @Test
1118     void testIssue455g() {
1119         final JexlEngine jexl = new JexlBuilder().create();
1120         final JxltEngine jxlt = jexl.createJxltEngine();
1121         final JxltEngine.Template template = jxlt.createTemplate("${name} #{suffix}", "name", "suffix");
1122         final StringWriter writer = new StringWriter();
1123         // prepare requires immediate arguments; evaluate needs deferred arguments
1124         template.prepare(null, "World", null).evaluate(null, writer, null, "~");
1125         assertEquals("World ~", writer.toString());
1126     }
1127 
1128     @Test
1129     void testIssue455h() {
1130         final JexlEngine jexl = new JexlBuilder().create();
1131         final JxltEngine jxlt = jexl.createJxltEngine();
1132         final JxltEngine.Template template = jxlt.createTemplate("#{name + ' Hello'} ${name + ' !'} #{suffix}", "name", "suffix");
1133         final StringWriter writer = new StringWriter();
1134         // Prepare only the immediate name argument; evaluate needs both deferred arguments - name and suffix
1135         template.prepare(null, "World").evaluate(null, writer, "World", "~");
1136         assertEquals("World Hello World ! ~", writer.toString());
1137     }
1138 
1139     @Test
1140     void testIssue456a() {
1141         final JexlFeatures features = JexlFeatures.createDefault();
1142         assertThrows(JexlException.Parsing.class, () -> run456(
1143             f -> new JexlBuilder().features(f).strict(true).create(),
1144             features, null));
1145     }
1146 
1147 
1148     @Test
1149     void testIssue456b() {
1150         final JexlFeatures features = JexlFeatures.createDefault().ignoreTemplatePrefix(true);
1151         assertNotNull(run456(
1152             f -> new JexlBuilder().features(f).strict(true).create(),
1153             features, null));
1154     }
1155 
1156     @Test
1157     void testIssue456c() {
1158         final JexlFeatures features = JexlFeatures.createDefault().ignoreTemplatePrefix(true);
1159         assertNotNull(run456(
1160             f -> new JexlBuilder().features(f).strict(false).create(),
1161             features, null));
1162     }
1163 
1164     @Test
1165     void testIssue456d() {
1166         // add a '$$' global context variable
1167         final JexlContext ctxt = new MapContext();
1168         ctxt.set("$$", "");
1169         final JexlFeatures features = JexlFeatures.createDefault().ignoreTemplatePrefix(true);
1170         assertNotNull(run456(
1171             f -> new JexlBuilder().features(f).strict(true).create(),
1172             features, ctxt));
1173     }
1174 
1175     JxltEngine run456(final Function<JexlFeatures, JexlEngine> engineFactory, final JexlFeatures features, final JexlContext ctxt) {
1176         final JexlEngine jexl = engineFactory.apply(features);
1177         final JxltEngine jxlt = jexl.createJxltEngine();
1178         // creation/parse is OK
1179         final JxltEngine.Template template = jxlt.createTemplate("$$ var foo = 'foo';$$ var bar = 'bar';\n${foo + bar}");
1180         final StringWriter writer = new StringWriter();
1181         template.evaluate(ctxt, writer);
1182         assertEquals("foobar", writer.toString());
1183         return jxlt;
1184     }
1185 
1186     @Test
1187     void testIssue36x_456_var() {
1188         final JexlEngine jexl = new JexlBuilder().create();
1189         final JxltEngine jxlt = jexl.createJxltEngine();
1190         // OK
1191         jexl.createScript("var foo = 0;\nfoo = 42;");
1192         try {
1193             jxlt.createTemplate("$$ var foo = 'foo';  if (true) { var foo = 'bar'; var err =&; }");
1194             fail("should have thrown a parsing error in '&'");
1195         } catch (final JexlException xjexl) {
1196             // parsing error in '&'
1197             assertTrue(xjexl.getMessage().contains("&"));
1198         }
1199         // JEXL-456: java.lang.NullPointerException:
1200         // Cannot invoke "org.apache.commons.jexl3.internal.Scope.getCaptureDeclaration(int)" because "blockScope" is null
1201         jexl.createScript("var foo = 0;\nfoo = 42;");
1202         // JEXL-456: same error with the template creation below
1203         jxlt.createTemplate("$$ var foo = 'foo'; foo = 'bar';");
1204     }
1205 
1206     @Test
1207     void testIssue36x_456_let() {
1208         final JexlFeatures features = JexlFeatures.createDefault().ignoreTemplatePrefix(true);
1209         final JexlEngine jexl = new Engine32(new JexlBuilder().features(features).strict(true));
1210         // OK
1211         jexl.createScript("let foo = 0;\nfoo = 42;");
1212         final JxltEngine jxlt = jexl.createJxltEngine();
1213         try {
1214             jxlt.createTemplate("$$ var err =&;");
1215             fail("should have thrown a parsing error in '&'");
1216         } catch (final JexlException xjexl) {
1217             // parsing error in '&'
1218             assertTrue(xjexl.getMessage().contains("&"));
1219         }
1220         // JEXL-456: parsing error in 'foo: variable is already declared'
1221         jexl.createScript("let foo = 0;\nfoo = 42;");
1222         // JEXL-456: same error with the template creation below
1223         final JxltEngine.Template template = jxlt.createTemplate("$$ var foo = 0;\n$$ $$ foo = 42;\n${foo}");
1224         final StringWriter writer = new StringWriter();
1225         template.evaluate(null, writer);
1226         assertEquals("42", writer.toString());
1227     }
1228 
1229 
1230     @Test
1231     void test459() {
1232         final CaptureLog capture = new CaptureLog("test459");
1233         final JexlBuilder builder = new JexlBuilder().logger(capture).safe(true).strict(false).silent(true);
1234         final JexlEngine jexl = builder.create();
1235         final Object[] expected = { true, true, 0, 0 };
1236         final String[] expressions = {
1237           "empty('aaa'.substring(400,500))",
1238           "empty('aaa'.substring(-1, 2))",
1239           "size('aaa'.substring(400,500))",
1240           "size('aaa'.substring(-1, 2))",
1241         };
1242         for (int i = 0; i < expressions.length; ++i) {
1243             final String expr = expressions[i];
1244             final Object control = expected[i];
1245             capture.clear();
1246             final JexlScript script = jexl.createScript(expr);
1247             final Object result = script.execute(null);
1248             assertEquals(control, result);
1249             final List<String> errors = capture.getCapturedMessages();
1250             assertEquals(1, capture.count("warn"));
1251             assertTrue(errors.stream().anyMatch(error -> error.contains("substring")));
1252         }
1253     }
1254 
1255     public static class Namespace460 {
1256         public static int func(final int i) {
1257             return i * 42;
1258         }
1259     }
1260 
1261     @Test
1262     void test460() {
1263         final JexlFeatures f = JexlFeatures.createDefault().namespaceIdentifier(true);
1264         final JexlEngine jexl = new JexlBuilder()
1265           .namespaces(Collections.singletonMap("z", Namespace460.class))
1266           .features(f)
1267           .create();
1268 
1269         final JexlContext jc = new MapContext();
1270         jc.set("x", 1);
1271         jc.set("y", 2);
1272         jc.set("my_function", jexl.createScript("(a) -> { a * 1000 }"));
1273 
1274         JexlScript script = jexl.createScript("x != null ? x : my_function(y)");
1275         Object result = script.execute(jc);
1276         assertEquals(1, result, "Result is not x");
1277         script = jexl.createScript("x != null ? z:func(x) : my_function(y)");
1278         result = script.execute(jc);
1279         assertEquals(42, result);
1280         script = jexl.createScript("y == 1 ?  my_function(x) : z:func(x)");
1281         result = script.execute(jc);
1282         assertEquals(42, result);
1283     }
1284 
1285     @Test
1286     void test462() {
1287         // JEXL-462: under RESTRICTED, java.io.PrintWriter is reachable and its methods are allowed,
1288         // but its constructors are denied - it cannot be instantiated from a script.
1289         final JexlEngine jexl = new JexlBuilder().silent(false).permissions(RESTRICTED).create();
1290         for (final String src : new String[] {
1291                 "new('java.io.PrintWriter', new('java.io.StringWriter'))",
1292                 "import java.io.PrintWriter; new PrintWriter(new('java.io.StringWriter'))" }) {
1293             final JexlScript script = jexl.createScript(src);
1294             final JexlException.Method xctor = assertThrows(JexlException.Method.class,
1295                 () -> script.execute(null), () -> "PrintWriter::new should be denied: " + src);
1296             assertTrue(xctor.getMethod().contains("PrintWriter"));
1297         }
1298         // a PrintWriter method (println) is allowed on a provided instance
1299         final StringWriter sw = new StringWriter();
1300         final java.io.PrintWriter pw = new java.io.PrintWriter(sw);
1301         jexl.createScript("p.println('hello')", "p").execute(null, pw);
1302         pw.flush();
1303         assertTrue(sw.toString().contains("hello"));
1304     }
1305 
1306     @Test
1307     void test462a() {
1308         // JEXL-462: under RESTRICTED, java.io.PrintWriter and java.util.Formatter are reachable and their methods are allowed,
1309         // but their constructors are denied - they cannot be instantiated from a script.
1310         final JexlEngine jexl = new JexlBuilder().silent(false).permissions(RESTRICTED).create();
1311         for (final String src : new String[] {
1312             "new('java.io.PrintWriter', '/tmp/pwn0.php')",
1313             "import java.io.PrintWriter; new PrintWriter('/tmp/pwn1.php')"}) {
1314             final JexlScript script = jexl.createScript(src);
1315             final JexlException.Method xctor = assertThrows(JexlException.Method.class,
1316                 () -> script.execute(null), () -> "PrintWriter::new should be denied: " + src);
1317             assertTrue(xctor.getMethod().contains("PrintWriter"));
1318         }
1319         // a PrintWriter method (println) is allowed on a provided instance
1320         StringWriter sw = new StringWriter();
1321         final java.io.PrintWriter pw = new java.io.PrintWriter(sw);
1322         jexl.createScript("p.println('hello')", "p").execute(null, pw);
1323         pw.flush();
1324         assertTrue(sw.toString().contains("hello"));
1325         for (final String src : new String[] {
1326           "new('java.util.Formatter', '/tmp/pwn2.php')",
1327           "import java.util.Formatter; new Formatter('/tmp/pwn3.php')" }) {
1328             final JexlScript script = jexl.createScript(src);
1329             final JexlException.Method xctor = assertThrows(JexlException.Method.class,
1330               () -> script.execute(null), () -> "Formatter::new should be denied: " + src);
1331             assertTrue(xctor.getMethod().contains("Formatter"));
1332         }
1333 
1334         // a PrintWriter method (println) is allowed on a provided instance
1335         sw = new StringWriter();
1336         final java.util.Formatter pf = new java.util.Formatter(sw);
1337         jexl.createScript("pf.format('%s', 'bonjour')", "pf").execute(null, pf);
1338         assertTrue(sw.toString().contains("bonjour"));
1339     }
1340 
1341     @Test
1342     void test463() {
1343         final JexlEngine jexl = new JexlBuilder()
1344           .silent(false)
1345           .features(JexlFeatures.createDefault())
1346           .permissions(RESTRICTED)
1347           .create();
1348         String src = "addOne  = (f) -> {\n return f + 1;\n}";
1349         JexlScript script = jexl.createScript(src);
1350         assertNotNull(script);
1351         JexlContext ctxt = new MapContext();
1352         Object result = script.execute(ctxt);
1353         assertNotNull(result);
1354         JexlScript rs = (JexlScript) result;
1355         result = rs.execute(ctxt, 1);
1356         assertEquals(2, result);
1357     }
1358 
1359     @Test
1360     void test466() {
1361         final JexlEngine jexl = new JexlBuilder()
1362           .silent(false)
1363           .features(JexlFeatures.createDefault())
1364           .permissions(RESTRICTED)
1365           .create();
1366         JxltEngine jxlt = jexl.createJxltEngine();
1367 
1368         String src = "$$ const a = 'a';\n" + "$$ const b = `${a}` \n" + "b:${b}";
1369         JxltEngine.Template template = jxlt.createTemplate(src);
1370         StringWriter writer = new StringWriter();
1371         template.evaluate(null, writer);
1372         assertEquals("b:a", writer.toString());
1373 
1374         String expr = "forty-two: ${b + a}";
1375         JexlContext ctxt = new MapContext();
1376         ctxt.set("b", 20);
1377         ctxt.set("a", 22);
1378         JxltEngine.Expression expression = jxlt.createExpression(expr);
1379         Object value = expression.evaluate(ctxt);
1380         assertEquals("forty-two: 42", value.toString());
1381     }
1382 }
1383