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    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.jexl3;
18  
19  import static org.apache.commons.jexl3.introspection.JexlPermissions.RESTRICTED;
20  import static org.junit.jupiter.api.Assertions.assertArrayEquals;
21  import static org.junit.jupiter.api.Assertions.assertEquals;
22  import static org.junit.jupiter.api.Assertions.assertFalse;
23  import static org.junit.jupiter.api.Assertions.assertNotNull;
24  import static org.junit.jupiter.api.Assertions.assertNull;
25  import static org.junit.jupiter.api.Assertions.assertThrows;
26  import static org.junit.jupiter.api.Assertions.assertTrue;
27  
28  import java.lang.reflect.Method;
29  import java.math.BigDecimal;
30  import java.util.Arrays;
31  import java.util.Collections;
32  import java.util.HashMap;
33  import java.util.Iterator;
34  import java.util.List;
35  import java.util.Map;
36  import java.util.Objects;
37  import java.util.concurrent.atomic.AtomicLong;
38  
39  import org.apache.commons.jexl3.introspection.JexlPermissions;
40  import org.junit.jupiter.api.Test;
41  
42  /**
43   * Test cases for reported issue between JEXL-300 and JEXL-399.
44   */
45  public class Issues400Test {
46  
47      public static class VinzCaller {
48          private final JexlContext context;
49  
50          VinzCaller(final JexlContext context) {
51              this.context = context;
52          }
53  
54          public Object execute(final JexlScript script) {
55              return script.execute(context);
56          }
57      }
58  
59      public static class VinzContext extends MapContext {
60          public String member(final String m, final String u) {
61              return m + '.' + u;
62          }
63      }
64  
65      /**
66       * Any function in a context can be used as a method of its first parameter. Overloads are respected.
67       */
68      public static class XuContext extends MapContext {
69  
70          public String join(final int[] list, final String str) {
71              return join(Arrays.stream(list).iterator(), str);
72          }
73  
74          public String join(final Iterable<?> list, final String str) {
75              return join(list.iterator(), str);
76          }
77  
78          public String join(final Iterator<?> iterator, final String str) {
79              if (!iterator.hasNext()) {
80                  return "";
81              }
82              final StringBuilder strb = new StringBuilder(256);
83              strb.append(iterator.next().toString());
84              while (iterator.hasNext()) {
85                  strb.append(str);
86                  strb.append(Objects.toString(iterator.next(), "?"));
87              }
88              return strb.toString();
89          }
90      }
91  
92      private static void run404(final JexlEngine jexl, final String src, final Object... a) {
93          final JexlScript script = jexl.createScript(src, "a", "b");
94          if (!src.endsWith(";")) {
95              assertEquals(script.getSourceText(), script.getParsedText());
96          }
97          final Object result = script.execute(null, a);
98          assertEquals(42, result);
99      }
100 
101     @Test
102     public void test402() {
103         final JexlContext jc = new MapContext();
104       // @formatter:off
105       final String[] sources = {
106         "if (true) { return }",
107         "if (true) { 3; return }",
108         "(x->{ 3; return })()"
109       };
110       // @formatter:on
111         final JexlEngine jexl = new JexlBuilder().create();
112         for (final String source : sources) {
113             final JexlScript e = jexl.createScript(source);
114             final Object o = e.execute(jc);
115             assertNull(o);
116         }
117     }
118 
119     @Test
120     public void test403() {
121         // @formatter:off
122         final String[] strings = {
123             "  map1.`${item.a}` = 1;\n",
124             "  map1[`${item.a}`] = 1;\n",
125             "  map1[item.a] = 1;\n"
126          };
127         // @formatter:on
128         for (final String setmap : strings) {
129             // @formatter:off
130             final String src = "var a = {'a': 1};\n" +
131                 "var list = [a, a];\n" +
132                 "let map1 = {:};\n" +
133                 "for (var item : list) {\n" +
134                 setmap +
135                 "}\n " +
136                 "map1";
137             // @formatter:on
138             final JexlEngine jexl = new JexlBuilder().cache(64).create();
139             final JexlScript script = jexl.createScript(src);
140             for (int i = 0; i < 2; ++i) {
141                 final Object result = script.execute(null);
142                 assertTrue(result instanceof Map);
143                 final Map<?, ?> map = (Map<?, ?>) result;
144                 assertEquals(1, map.size());
145                 assertTrue(map.containsKey(1));
146                 assertTrue(map.containsValue(1));
147             }
148         }
149     }
150 
151     @Test
152     public void test404a() {
153         final JexlEngine jexl = new JexlBuilder().cache(64).strict(true).safe(false).create();
154         Map<String, Object> a = Collections.singletonMap("b", 42);
155         // access is constant
156         for (final String src : new String[] { "a.b", "a?.b", "a['b']", "a?['b']", "a?.`b`" }) {
157             run404(jexl, src, a);
158             run404(jexl, src + ";", a);
159         }
160         // access is variable
161         for (final String src : new String[] { "a[b]", "a?[b]", "a?.`${b}`" }) {
162             run404(jexl, src, a, "b");
163             run404(jexl, src + ";", a, "b");
164         }
165         // add a 3rd access
166         final Map<String, Object> b = Collections.singletonMap("c", 42);
167         a = Collections.singletonMap("b", b);
168         for (final String src : new String[] { "a[b].c", "a?[b]?['c']", "a?.`${b}`.c" }) {
169             run404(jexl, src, a, "b");
170         }
171     }
172 
173     @Test
174     public void test404b() {
175       // @formatter:off
176       final JexlEngine jexl = new JexlBuilder()
177           .cache(64)
178           .strict(true)
179           .safe(false)
180           .create();
181       // @formatter:on
182         final Map<String, Object> b = Collections.singletonMap("c", 42);
183         final Map<String, Object> a = Collections.singletonMap("b", b);
184         JexlScript script;
185         Object result = -42;
186         script = jexl.createScript("a?['B']?['C']", "a");
187         result = script.execute(null, a);
188         assertEquals(script.getSourceText(), script.getParsedText());
189         assertNull(result);
190         script = jexl.createScript("a?['b']?['C']", "a");
191         assertEquals(script.getSourceText(), script.getParsedText());
192         result = script.execute(null, a);
193         assertNull(result);
194         script = jexl.createScript("a?['b']?['c']", "a");
195         assertEquals(script.getSourceText(), script.getParsedText());
196         result = script.execute(null, a);
197         assertEquals(42, result);
198         script = jexl.createScript("a?['B']?['C']?: 1042", "a");
199         assertEquals(script.getSourceText(), script.getParsedText());
200         result = script.execute(null, a);
201         assertEquals(1042, result);
202         // can still do ternary, note the space between ? and [
203         script = jexl.createScript("a? ['B']:['C']", "a");
204         result = script.execute(null, a);
205         assertArrayEquals(new String[] { "B" }, (String[]) result);
206         script = jexl.createScript("a?['b'] ?: ['C']", "a");
207         result = script.execute(null, a);
208         assertEquals(b, result);
209         script = jexl.createScript("a?['B'] ?: ['C']", "a");
210         result = script.execute(null, a);
211         assertArrayEquals(new String[] { "C" }, (String[]) result);
212     }
213 
214     @Test
215     public void test406a() {
216         // @formatter:off
217         final JexlEngine jexl = new JexlBuilder()
218             .cache(64)
219             .strict(true)
220             .safe(false)
221             .create();
222         // @formatter:on
223 
224         final JexlContext context = new XuContext();
225         // @formatter:off
226         final List<String> list = Arrays.asList(
227             "[1, 2, 3, 4, ...].join('-')", // List<Integer>
228             "[1, 2, 3, 4,].join('-')", // int[]
229             "(1 .. 4).join('-')", // iterable<Integer>
230             "join([1, 2, 3, 4, ...], '-')",
231             "join([1, 2, 3, 4], '-')",
232             "join((1 .. 4), '-')");
233         // @formatter:on
234         for (final String src : list) {
235             final JexlScript script = jexl.createScript(src);
236             final Object result = script.execute(context);
237             assertEquals("1-2-3-4", result, src);
238         }
239 
240         final String src0 = "x.join('*')";
241         final JexlScript script0 = jexl.createScript(src0, "x");
242         final String src1 = "join(x, '*')";
243         final JexlScript script1 = jexl.createScript(src1, "x");
244         for (final Object x : Arrays.asList(Arrays.asList(1, 2, 3, 4), new int[] { 1, 2, 3, 4 })) {
245             Object result = script0.execute(context, x);
246             assertEquals("1*2*3*4", result, src0);
247             result = script1.execute(context, x);
248             assertEquals("1*2*3*4", result, src1);
249         }
250     }
251 
252     @Test
253     public void test407() {
254         // Java version
255         final double r = 99.0d + 7.82d - 99.0d - 7.82d;
256         assertEquals(0d, r, 8.e-15); // Not zero, IEEE 754
257         // jexl
258         final JexlEngine jexl = new JexlBuilder().create();
259         final JexlScript script = jexl.createScript("a + b - a - b", "a", "b");
260         // using doubles, same as Java
261         Number result = (Number) script.execute(null, 99.0d, 7.82d);
262         assertEquals(0d, result.doubleValue(), 8.e-15);
263         // using BigdDecimal, more precise, still not zero
264         result = (Number) script.execute(null, new BigDecimal(99.0d), new BigDecimal(7.82d));
265         assertEquals(0d, result.doubleValue(), 3.e-32);
266     }
267 
268     @Test
269     public void test412() {
270         final Map<Object, Object> ctl = new HashMap<>();
271         ctl.put("one", 1);
272         ctl.put("two", 2);
273         final String fnsrc0 = "function f(x) { x }\n" + "let one = 'one', two = 'two';\n";
274         // @formatter:off
275         final List<String> list = Arrays.asList(
276             "{ one : f(1), two:f(2) }",
277             "{ one: f(1), two: f(2) }",
278             "{ one: f(1), two:f(2) }",
279             "{ one :f(1), two:f(2) }");
280         // @formatter:on
281         for (final String map0 : list) {
282             final String fnsrc = fnsrc0 + map0;
283             final JexlContext jc = new MapContext();
284             final JexlEngine jexl = new JexlBuilder().create();
285             final JexlScript e = jexl.createScript(fnsrc);
286             final Object o = e.execute(jc);
287             assertTrue(o instanceof Map);
288             final Map<?, ?> map = (Map<?, ?>) o;
289             assertEquals(map, ctl);
290         }
291     }
292 
293     @Test
294     public void test413a() {
295         final JexlBuilder builder = new JexlBuilder();
296         final JexlEngine jexl = builder.create();
297         final JexlScript script = jexl.createScript("var c = 42; var f = y -> c += y; f(z)", "z");
298         final Number result = (Number) script.execute(null, 12);
299         assertEquals(54, result);
300     }
301 
302     @Test
303     public void test413b() {
304         final JexlBuilder builder = new JexlBuilder();
305         final JexlOptions options = builder.options();
306         options.setConstCapture(true);
307         options.setLexical(true);
308         final JexlEngine jexl = builder.create();
309         final JexlScript script = jexl.createScript("var c = 42; var f = y -> c += y; f(z)", "z");
310         final JexlException.Variable xvar = assertThrows(JexlException.Variable.class, () -> script.execute(null, 12), "c should be const");
311         assertEquals("c", xvar.getVariable());
312     }
313 
314     @Test
315     public void test413c() {
316         final JexlBuilder builder = new JexlBuilder();
317         final JexlEngine jexl = builder.create();
318         final JexlScript script = jexl.createScript("#pragma jexl.options '+constCapture'\nvar c = 42; var f = y -> c += y; f(z)", "z");
319         final JexlException.Variable xvar = assertThrows(JexlException.Variable.class, () -> script.execute(null, 12), "c should be const");
320         assertEquals("c", xvar.getVariable());
321     }
322 
323     @Test
324     public void test413d() {
325         final JexlBuilder builder = new JexlBuilder().features(new JexlFeatures().constCapture(true));
326         final JexlEngine jexl = builder.create();
327         final JexlException.Parsing xparse = assertThrows(JexlException.Parsing.class, () -> jexl.createScript("var c = 42; var f = y -> c += y; f(z)", "z"),
328                 "c should be const");
329         assertTrue(xparse.getMessage().contains("const"));
330     }
331 
332     @Test
333     public void test415() {
334         final JexlBuilder builder = new JexlBuilder().features(new JexlFeatures().constCapture(true));
335         final JexlEngine jexl = builder.create();
336         JexlScript script;
337         Object result;
338         script = jexl.createScript("`#${c}`", "c");
339         result = script.execute(null, 42);
340         assertEquals("#42", result.toString());
341         script = jexl.createScript("`$${c}`", "c");
342         result = script.execute(null, 42);
343         assertEquals("$42", result.toString());
344         script = jexl.createScript("`$#{c}`", "c");
345         result = script.execute(null, 42);
346         assertEquals("$42", result.toString());
347         script = jexl.createScript("`##{c}`", "c");
348         result = script.execute(null, 42);
349         assertEquals("#42", result.toString());
350         script = jexl.createScript("`--##{c}`", "c");
351         result = script.execute(null, 42);
352         assertEquals("--#42", result.toString());
353     }
354 
355     @Test
356     public void test419() throws NoSuchMethodException {
357         // check RESTRICTED permissions denies call to System::currentTimeMillis()
358         final Method currentTimeMillis = System.class.getMethod("currentTimeMillis");
359         assertFalse(RESTRICTED.allow(currentTimeMillis));
360         // compose using a positive class permission to allow just System::currentTimeMillis()
361         final JexlPermissions permissions = RESTRICTED.compose("java.lang { +System { currentTimeMillis(); } }");
362         // check no side effect on compose
363         assertTrue(permissions.allow(currentTimeMillis));
364         assertFalse(RESTRICTED.allow(currentTimeMillis));
365 
366         // An engine with the System class as namespace and the positive permissions
367         final JexlEngine jexl = new JexlBuilder().namespaces(Collections.singletonMap("sns", System.class)).permissions(permissions).create();
368 
369         final AtomicLong result = new AtomicLong();
370         assertEquals(0, result.get());
371         final long now = System.currentTimeMillis();
372         // calling System::currentTimeMillis() is allowed and behaves as expected
373         jexl.createScript("result.set(sns:currentTimeMillis())", "result").execute(null, result);
374         assertTrue(result.get() >= now);
375 
376         // we still cant call anything else
377         final JexlScript script = jexl.createScript("sns:gc()");
378         final JexlException.Method method = assertThrows(JexlException.Method.class, () -> script.execute(null));
379         assertEquals("gc", method.getMethod());
380 
381     }
382 
383     @Test
384     public void testDocBreakContinue() {
385         final JexlBuilder builder = new JexlBuilder().features(new JexlFeatures().constCapture(true));
386         final JexlEngine jexl = builder.create();
387         JexlScript script;
388         Object result;
389         // @formatter:off
390         final String srcContinue = "let text = '';\n" +
391             "for (let i : (4..2)) { if (i == 3) continue; text += i; }\n" +
392             "text;";
393         // @formatter:on
394         script = jexl.createScript(srcContinue);
395         result = script.execute(null);
396         assertEquals("42", result);
397         // @formatter:off
398         final String srcBreak = "let i = 33;\n" +
399             "while (i < 66) { if (i == 42) { break; } i += 1; }\n" +
400             "i;";
401         // @formatter:on
402         script = jexl.createScript(srcBreak);
403         result = script.execute(null);
404         assertEquals(42, result);
405     }
406 
407     @Test
408     public void testNamespaceVsTernary0() {
409         final VinzContext ctxt = new VinzContext();
410         ctxt.set("Users", "USERS");
411         final JexlEngine jexl = new JexlBuilder().safe(false).strict(true).silent(false).create();
412         // @formatter:off
413         JexlScript script = jexl.createScript("() -> {\n"
414             + "  var fn = (user) -> {\n"
415             + "     user ? user : member(Users, 'user');\n"
416             + "  }\n"
417             + "}");
418         // @formatter:on
419         Object r = script.execute(ctxt);
420         assertNotNull(r);
421         script = (JexlScript) r;
422         r = script.execute(ctxt);
423         assertEquals("USERS.user", r);
424     }
425 
426     @Test
427     public void testNamespaceVsTernary1() {
428         final VinzContext ctxt = new VinzContext();
429         ctxt.set("Users", "USERS");
430         ctxt.set("vinz", new VinzCaller(ctxt));
431         final JexlEngine jexl = new JexlBuilder().safe(false).strict(true).silent(false).create();
432         // @formatter:off
433         final JexlScript script = jexl.createScript(
434             "vinz.execute(() -> {\n"
435             + "  var test = 42;\n"
436             + "  var user = useTest ? test : member(Users, 'user');\n"
437             + "})\n" , "useTest");
438         // @formatter:on
439         Object r = script.execute(ctxt, false);
440         assertNotNull(r);
441         assertEquals("USERS.user", r);
442         r = script.execute(ctxt, true);
443         assertNotNull(r);
444         assertEquals(42, r);
445     }
446 }