Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Qute: fix parsing of string literals and lenient section parameters #42160

Merged
merged 1 commit into from
Jul 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading