From 0858c39badbc397e6c5b869d4e0632d0c5d4fe96 Mon Sep 17 00:00:00 2001 From: Roberto Cortez Date: Fri, 13 Sep 2024 19:38:29 +0100 Subject: [PATCH] Expression STRICT_ESCAPES mode --- .../common/expression/Expression.java | 96 +++++++++++++-- .../common/expression/ExpressionTestCase.java | 110 ++++++++++++++++++ 2 files changed, 194 insertions(+), 12 deletions(-) diff --git a/expression/src/main/java/io/smallrye/common/expression/Expression.java b/expression/src/main/java/io/smallrye/common/expression/Expression.java index 555a04d3..faed18ed 100644 --- a/expression/src/main/java/io/smallrye/common/expression/Expression.java +++ b/expression/src/main/java/io/smallrye/common/expression/Expression.java @@ -260,6 +260,9 @@ private static Node parseString(Itr itr, final boolean allowExpr, final boolean } // check to see if it's a dangling $ if (!itr.hasNext()) { + if (flags.contains(Flag.NO_$$)) { + continue; + } if (!flags.contains(Flag.LENIENT_SYNTAX)) { // TP 2 throw invalidExpressionSyntax(itr.getStr(), idx); @@ -269,16 +272,16 @@ private static Node parseString(Itr itr, final boolean allowExpr, final boolean start = itr.getNextIdx(); continue; } - // enqueue what we have acquired so far - if (idx > start) { - // TP 4 - list.add(new LiteralNode(itr.getStr(), start, idx)); - } // next char should be an expression starter of some sort idx = itr.getNextIdx(); ch = itr.next(); switch (ch) { case '{': { + // enqueue what we have acquired so far + if (idx > start) { + // TP 4 + list.add(new LiteralNode(itr.getStr(), start, idx - 1)); + } // ${ boolean general = flags.contains(Flag.GENERAL_EXPANSION) && itr.hasNext() && itr.peekNext() == '{'; // consume double-{ @@ -407,18 +410,47 @@ private static Node parseString(Itr itr, final boolean allowExpr, final boolean } case '$': { // $$ - if (flags.contains(Flag.MINI_EXPRS)) { - // TP 13 - list.add(new ExpressionNode(false, LiteralNode.DOLLAR, Node.NULL)); + if (flags.contains(Flag.NO_$$)) { + if (itr.hasNext()) { + switch (itr.peekNext()) { + case '{': + list.add(new LiteralNode(itr.getStr(), start, idx)); + break; + case '$': + list.add(new LiteralNode(itr.getStr(), start, idx)); + itr.prev(); + break; + default: + list.add(new LiteralNode(itr.getStr(), start, itr.getNextIdx())); + break; + } + } else { + list.add(new LiteralNode(itr.getStr(), start, itr.getNextIdx())); + } } else { - // just resolve $$ to $ - // TP 14 - list.add(LiteralNode.DOLLAR); + // enqueue what we have acquired so far + if (idx > start) { + // TP 4 + list.add(new LiteralNode(itr.getStr(), start, idx - 1)); + } + if (flags.contains(Flag.MINI_EXPRS)) { + // TP 13 + list.add(new ExpressionNode(false, LiteralNode.DOLLAR, Node.NULL)); + } else { + // just resolve $$ to $ + // TP 14 + list.add(LiteralNode.DOLLAR); + } } start = itr.getNextIdx(); continue; } case '}': { + // enqueue what we have acquired so far + if (idx > start) { + // TP 4 + list.add(new LiteralNode(itr.getStr(), start, idx - 1)); + } // $} if (flags.contains(Flag.MINI_EXPRS)) { // TP 15 @@ -452,6 +484,11 @@ private static Node parseString(Itr itr, final boolean allowExpr, final boolean //throw Assert.unreachableCode(); } case ':': { + // enqueue what we have acquired so far + if (idx > start) { + // TP 4 + list.add(new LiteralNode(itr.getStr(), start, idx - 1)); + } // $: if (flags.contains(Flag.MINI_EXPRS)) { // $: is an expression @@ -485,7 +522,22 @@ private static Node parseString(Itr itr, final boolean allowExpr, final boolean } //throw Assert.unreachableCode(); } + case '\\': { + if (flags.contains(Flag.NO_$$)) { + if (itr.hasNext() && itr.peekNext() == '$') { + list.add(new LiteralNode(itr.getStr(), start, idx)); + start = itr.getNextIdx(); + itr.next(); + continue; + } + } + } default: { + // enqueue what we have acquired so far + if (idx > start) { + // TP 4 + list.add(new LiteralNode(itr.getStr(), start, idx - 1)); + } // $ followed by anything else if (flags.contains(Flag.MINI_EXPRS)) { // TP 25 @@ -551,7 +603,22 @@ private static Node parseString(Itr itr, final boolean allowExpr, final boolean //throw Assert.unreachableCode(); } case '\\': { - if (flags.contains(Flag.ESCAPES)) { + if (flags.contains(Flag.NO_$$)) { + int escape = itr.getNextIdx(); + if (itr.hasNext() && itr.peekNext() == '$') { + itr.next(); + if (itr.hasNext() && itr.peekNext() == '{') { + list.add(new LiteralNode(itr.getStr(), start, escape - 1)); + list.add(new LiteralNode(itr.getStr(), escape, itr.getNextIdx())); + start = itr.getNextIdx(); + } else if (itr.hasNext() && itr.peekNext() == '$') { + list.add(new LiteralNode(itr.getStr(), start, escape)); + start = itr.getPrevIdx(); + itr.prev(); + } + } + continue; + } else if (flags.contains(Flag.ESCAPES)) { if (idx > start) { list.add(new LiteralNode(itr.getStr(), start, idx)); start = idx; @@ -694,6 +761,11 @@ public enum Flag { * character. */ ESCAPES, + /** + * Escaping $ with $$ or /$ only applies when { follows + * the initial escaped $. + */ + NO_$$, /** * Treat expressions containing a double-colon delimiter as special, encoding the entire content into the key. */ diff --git a/expression/src/test/java/io/smallrye/common/expression/ExpressionTestCase.java b/expression/src/test/java/io/smallrye/common/expression/ExpressionTestCase.java index 53046ca1..80605f3c 100644 --- a/expression/src/test/java/io/smallrye/common/expression/ExpressionTestCase.java +++ b/expression/src/test/java/io/smallrye/common/expression/ExpressionTestCase.java @@ -4,6 +4,8 @@ import static io.smallrye.common.expression.Expression.Flag.NO_SMART_BRACES; import static org.junit.jupiter.api.Assertions.*; +import java.util.EnumSet; + import org.junit.jupiter.api.Test; import io.smallrye.common.constraint.Assert; @@ -693,4 +695,112 @@ void expressions() { b.append(c.getExpandedDefault()); })); } + + @Test + void no$$() { + doubleDollarExpressions(EnumSet.of(NO_$$)); + doubleDollarExpressions(EnumSet.of(NO_$$, MINI_EXPRS)); + + assertEquals("", Expression.compile("$a", NO_$$, MINI_EXPRS).evaluate((c, b) -> { + assertEquals("a", c.getKey()); + })); + assertEquals("$$a", Expression.compile("$$a", NO_$$, MINI_EXPRS).evaluate((c, b) -> { + })); + + doubleDollarExpressions(EnumSet.of(NO_$$, ESCAPES)); + doubleDollarExpressions(EnumSet.of(NO_$$, ESCAPES, MINI_EXPRS)); + } + + private void doubleDollarExpressions(EnumSet flags) { + assertEquals("$", Expression.compile("$", flags).evaluate((c, b) -> { + })); + assertEquals("$$", Expression.compile("$$", flags).evaluate((c, b) -> { + })); + assertEquals("\\$", Expression.compile("\\$", flags).evaluate((c, b) -> { + })); + assertEquals("\\$$", Expression.compile("\\$$", flags).evaluate((c, b) -> { + })); + assertEquals("$$foo", Expression.compile("$$foo", flags).evaluate((c, b) -> { + })); + assertEquals("foo$$", Expression.compile("foo$$", flags).evaluate((c, b) -> { + })); + assertEquals("$$foo", Expression.compile("$$foo", flags).evaluate((c, b) -> { + })); + assertEquals("foo$$bar", Expression.compile("foo$$bar", flags).evaluate((c, b) -> { + })); + assertEquals("${foo}", Expression.compile("$${foo}", flags).evaluate((c, b) -> { + })); + assertEquals("$${foo}", Expression.compile("$$${foo}", flags).evaluate((c, b) -> { + })); + assertEquals("$${foo}$", Expression.compile("$$${foo}$", flags).evaluate((c, b) -> { + })); + assertEquals("$${foo}$$", Expression.compile("$$${foo}$$", flags).evaluate((c, b) -> { + })); + assertEquals("foo${bar}", Expression.compile("foo$${bar}", flags).evaluate((c, b) -> { + })); + assertEquals("foo$${bar}", Expression.compile("foo$$${bar}", flags).evaluate((c, b) -> { + })); + assertEquals("foo$$$${bar}", Expression.compile("foo$$$$${bar}", flags).evaluate((c, b) -> { + })); + assertEquals("foo$$$${bar}$$$baz", Expression.compile("foo$$$$${bar}$$$baz", flags).evaluate((c, b) -> { + })); + assertEquals("foo$$$$", Expression.compile("foo$$$$", flags).evaluate((c, b) -> { + })); + assertEquals("${foo:bar}", Expression.compile("$${foo:bar}", flags).evaluate((c, b) -> { + })); + assertEquals("$${foo:bar}", Expression.compile("$$${foo:bar}", flags).evaluate((c, b) -> { + })); + assertEquals("${foo:}", Expression.compile("$${foo:${bar}}", flags).evaluate((c, b) -> { + })); + assertEquals("${foo:${bar}}", Expression.compile("$${foo:$${bar}}", flags).evaluate((c, b) -> { + })); + + assertEquals("", Expression.compile("${foo}", flags).evaluate((c, b) -> { + assertEquals("foo", c.getKey()); + })); + assertEquals("", Expression.compile("${foo}${bar}", flags).evaluate((c, b) -> { + if ("foo".equals(c.getKey())) + assertEquals("foo", c.getKey()); + if ("bar".equals(c.getKey())) + assertEquals("bar", c.getKey()); + })); + assertEquals("foobar", Expression.compile("foo${foo}${bar}bar", flags).evaluate((c, b) -> { + if ("foo".equals(c.getKey())) + assertEquals("foo", c.getKey()); + if ("bar".equals(c.getKey())) + assertEquals("bar", c.getKey()); + })); + assertEquals("foo${foo}bar", Expression.compile("foo$${foo}${bar}bar", flags).evaluate((c, b) -> { + if ("bar".equals(c.getKey())) + assertEquals("bar", c.getKey()); + })); + assertEquals("foo${foo}bar", Expression.compile("foo$${foo${bar}}bar", flags).evaluate((c, b) -> { + if ("bar".equals(c.getKey())) + assertEquals("bar", c.getKey()); + })); + assertEquals("", Expression.compile("${}", flags).evaluate((c, b) -> { + assertEquals("", c.getKey()); + })); + assertEquals("", Expression.compile("${:}", flags).evaluate((c, b) -> { + assertEquals("", c.getKey()); + })); + + assertEquals("${foo}", Expression.compile("\\${foo}", flags).evaluate((c, b) -> { + })); + assertEquals("${foo}bar", Expression.compile("\\${foo}bar", flags).evaluate((c, b) -> { + })); + assertEquals("\\$\\{%s}", Expression.compile("\\$\\{%s}", flags).evaluate((c, b) -> { + })); + assertEquals("foo${bar}", Expression.compile("foo\\${bar}", flags).evaluate((c, b) -> { + })); + assertEquals("foo\\${bar}", Expression.compile("foo\\\\${bar}", flags).evaluate((c, b) -> { + })); + + assertEquals("foo\\${bar}", Expression.compile("foo\\$${bar}", flags).evaluate((c, b) -> { + })); + assertEquals("foo$${bar}", Expression.compile("foo$\\${bar}", flags).evaluate((c, b) -> { + })); + assertEquals("foo$$\\{bar}", Expression.compile("foo$$\\{bar}", flags).evaluate((c, b) -> { + })); + } }