Skip to content

Commit

Permalink
Qute: fix parsing of string literals and lenient section parameters
Browse files Browse the repository at this point in the history
- fixes #41918
  • Loading branch information
mkouba committed Jul 26, 2024
1 parent 62cdd67 commit d2ff3ef
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,15 @@ static Object getLiteralValue(String literal) {
* <code>false</code> otherwise
*/
static boolean isStringLiteralSeparator(char character) {
return character == '"' || character == '\'';
return isStringLiteralSeparatorSingle(character) || isStringLiteralSeparatorDouble(character);
}

static boolean isStringLiteralSeparatorSingle(char character) {
return character == '\'';
}

static boolean isStringLiteralSeparatorDouble(char character) {
return character == '"';
}

static boolean isStringLiteral(String value) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.regex.Pattern;

import org.jboss.logging.Logger;

Expand Down Expand Up @@ -165,7 +166,7 @@ Template parse() {
} else {
String reason = null;
ErrorCode code = null;
if (state == State.TAG_INSIDE_STRING_LITERAL) {
if (state == State.TAG_INSIDE_STRING_LITERAL_SINGLE || state == State.TAG_INSIDE_STRING_LITERAL_DOUBLE) {
reason = "unterminated string literal";
code = ParserError.UNTERMINATED_STRING_LITERAL;
} else if (state == State.TAG_INSIDE) {
Expand Down Expand Up @@ -249,8 +250,11 @@ private void processCharacter(char character) {
case TAG_INSIDE:
tag(character);
break;
case TAG_INSIDE_STRING_LITERAL:
tagStringLiteral(character);
case TAG_INSIDE_STRING_LITERAL_SINGLE:
tagStringLiteralSingle(character);
break;
case TAG_INSIDE_STRING_LITERAL_DOUBLE:
tagStringLiteralDouble(character);
break;
case COMMENT:
comment(character);
Expand Down Expand Up @@ -339,7 +343,8 @@ private boolean isCdataEnd(char character) {

private void tag(char character) {
if (LiteralSupport.isStringLiteralSeparator(character)) {
state = State.TAG_INSIDE_STRING_LITERAL;
state = LiteralSupport.isStringLiteralSeparatorSingle(character) ? State.TAG_INSIDE_STRING_LITERAL_SINGLE
: State.TAG_INSIDE_STRING_LITERAL_DOUBLE;
buffer.append(character);
} else if (character == END_DELIMITER) {
flushTag();
Expand All @@ -348,8 +353,15 @@ private void tag(char character) {
}
}

private void tagStringLiteral(char character) {
if (LiteralSupport.isStringLiteralSeparator(character)) {
private void tagStringLiteralSingle(char character) {
if (LiteralSupport.isStringLiteralSeparatorSingle(character)) {
state = State.TAG_INSIDE;
}
buffer.append(character);
}

private void tagStringLiteralDouble(char character) {
if (LiteralSupport.isStringLiteralSeparatorDouble(character)) {
state = State.TAG_INSIDE;
}
buffer.append(character);
Expand Down Expand Up @@ -819,7 +831,8 @@ static int getFirstDeterminingEqualsCharPosition(String part) {

static <B extends ErrorInitializer & WithOrigin> Iterator<String> splitSectionParams(String content, B block) {

boolean stringLiteral = false;
boolean stringLiteralSingle = false;
boolean stringLiteralDouble = false;
short composite = 0;
byte brackets = 0;
boolean space = false;
Expand All @@ -830,7 +843,10 @@ static <B extends ErrorInitializer & WithOrigin> Iterator<String> splitSectionPa
char c = content.charAt(i);
if (c == ' ') {
if (!space) {
if (!stringLiteral && composite == 0 && brackets == 0) {
if (!stringLiteralSingle
&& !stringLiteralDouble
&& composite == 0
&& brackets == 0) {
if (buffer.length() > 0) {
parts.add(buffer.toString());
buffer = new StringBuilder();
Expand All @@ -842,19 +858,30 @@ static <B extends ErrorInitializer & WithOrigin> Iterator<String> splitSectionPa
}
} else {
if (composite == 0
&& LiteralSupport.isStringLiteralSeparator(c)) {
stringLiteral = !stringLiteral;
} else if (!stringLiteral
&& isCompositeStart(c) && (i == 0 || space || composite > 0
&& !stringLiteralDouble
&& LiteralSupport.isStringLiteralSeparatorSingle(c)) {
stringLiteralSingle = !stringLiteralSingle;
} else if (composite == 0
&& !stringLiteralSingle
&& LiteralSupport.isStringLiteralSeparatorDouble(c)) {
stringLiteralDouble = !stringLiteralDouble;
} else if (!stringLiteralSingle
&& !stringLiteralDouble
&& isCompositeStart(c)
&& (i == 0 || space || composite > 0
|| (buffer.length() > 0 && buffer.charAt(buffer.length() - 1) == '!'))) {
composite++;
} else if (!stringLiteral
&& isCompositeEnd(c) && composite > 0) {
} else if (!stringLiteralSingle
&& !stringLiteralDouble
&& isCompositeEnd(c)
&& composite > 0) {
composite--;
} else if (!stringLiteral
} else if (!stringLiteralSingle
&& !stringLiteralDouble
&& Parser.isLeftBracket(c)) {
brackets++;
} else if (!stringLiteral
} else if (!stringLiteralSingle
&& !stringLiteralDouble
&& Parser.isRightBracket(c) && brackets > 0) {
brackets--;
}
Expand All @@ -864,7 +891,7 @@ && isCompositeEnd(c) && composite > 0) {
}

if (buffer.length() > 0) {
if (stringLiteral || composite > 0) {
if (stringLiteralSingle || stringLiteralDouble || composite > 0) {
throw block.error("unterminated string literal or composite parameter detected for [{content}]")
.argument("content", content)
.code(ParserError.UNTERMINATED_STRING_LITERAL_OR_COMPOSITE_PARAMETER)
Expand All @@ -874,10 +901,16 @@ && isCompositeEnd(c) && composite > 0) {
parts.add(buffer.toString());
}

// Try to find/replace "standalone" equals signs used as param names separators
// This allows for more lenient parsing of named section parameters, e.g. item.name = 'foo' instead of item.name='foo'
// Try to find/replace/merge:
// 1. "standalone" equals signs used as param names separators
// 2. parts that start/end with an equal sign followed/preceded by a valid Java identifier
// This allows for more lenient parsing of named section parameters
// e.g. `item = 'foo'` or `item= 'foo'` instead of `item='foo'`
for (ListIterator<String> it = parts.listIterator(); it.hasNext();) {
if (it.next().equals("=") && it.previousIndex() != 0 && it.hasNext()) {
String next = it.next();
if (next.equals("=")
&& it.previousIndex() != 0
&& it.hasNext()) {
// move cursor back
it.previous();
String merged = parts.get(it.previousIndex()) + it.next() + it.next();
Expand All @@ -889,12 +922,33 @@ && isCompositeEnd(c) && composite > 0) {
it.remove();
it.previous();
it.remove();
} else if (next.endsWith("=")
&& it.hasNext()
&& EQUAL_ENDS_PATTERN.matcher(next).matches()) {
String merged = next + it.next();
// replace the element with the merged value
it.set(merged);
// move cursor back and remove the element that ended with equals
it.previous();
it.previous();
it.remove();
} else if (next.startsWith("=")
&& it.hasPrevious()
&& EQUAL_STARTS_PATTERN.matcher(next).matches()) {
String merged = next + it.previous();
// replace the element with the merged value
it.set(merged);
// move cursor back and remove the element that started with equals
it.next();
it.remove();
}
}

return parts.iterator();
}

static final Pattern EQUAL_ENDS_PATTERN = Pattern.compile(".*[a-zA-Z0-9_$]=$");
static final Pattern EQUAL_STARTS_PATTERN = Pattern.compile("^=[a-zA-Z0-9_$].*");

static boolean isCompositeStart(char character) {
return character == START_COMPOSITE_PARAM;
}
Expand Down Expand Up @@ -931,7 +985,8 @@ enum State {

TEXT,
TAG_INSIDE,
TAG_INSIDE_STRING_LITERAL,
TAG_INSIDE_STRING_LITERAL_SINGLE,
TAG_INSIDE_STRING_LITERAL_DOUBLE,
TAG_CANDIDATE,
COMMENT,
ESCAPE,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,30 @@ public void testMandatorySectionParas() {
"Parser error: mandatory section parameters not declared for {#include /}: [template]", 1);
}

@Test
public void testSectionParameterWithNestedSingleQuotationMark() {
Engine engine = Engine.builder().addDefaults().build();
assertSectionParams(engine, "{#let id=\"'Foo'\"}", Map.of("id", "\"'Foo'\""));
assertSectionParams(engine, "{#let id=\"'Foo \"}", Map.of("id", "\"'Foo \""));
assertSectionParams(engine, "{#let id=\"'Foo ' \"}", Map.of("id", "\"'Foo ' \""));
assertSectionParams(engine, "{#let id=\"'Foo ' \" bar='baz'}", Map.of("id", "\"'Foo ' \"", "bar", "'baz'"));
assertSectionParams(engine, "{#let my=bad id=(\"'Foo ' \" + 1) bar='baz'}",
Map.of("my", "bad", "id", "(\"'Foo ' \" + 1)", "bar", "'baz'"));
assertSectionParams(engine, "{#let id = 'Foo'}", Map.of("id", "'Foo'"));
assertSectionParams(engine, "{#let id= 'Foo'}", Map.of("id", "'Foo'"));
assertSectionParams(engine, "{#let my = (bad or not) id=1}", Map.of("my", "(bad or not)", "id", "1"));
assertSectionParams(engine, "{#let my= (bad or not) id=1}", Map.of("my", "(bad or not)", "id", "1"));

}

private void assertSectionParams(Engine engine, String content, Map<String, String> expectedParams) {
Template template = engine.parse(content);
SectionNode node = template.findNodes(n -> n.isSection() && n.asSection().name.equals("let")).iterator().next()
.asSection();
Map<String, String> params = node.getBlocks().get(0).parameters;
assertEquals(expectedParams, params);
}

public static class Foo {

public List<Item> getItems() {
Expand Down

0 comments on commit d2ff3ef

Please sign in to comment.