From 0df5ef7bdabbbf6028d56c6f95af43c6ca11c500 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Thu, 16 Oct 2025 16:58:00 -0400 Subject: [PATCH 1/2] Handle quote escaping in HelperStringTokenizer --- .../jinjava/util/HelperStringTokenizer.java | 7 ++++- .../util/HelperStringTokenizerTest.java | 26 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/hubspot/jinjava/util/HelperStringTokenizer.java b/src/main/java/com/hubspot/jinjava/util/HelperStringTokenizer.java index 4cc1013c1..0fade94c3 100644 --- a/src/main/java/com/hubspot/jinjava/util/HelperStringTokenizer.java +++ b/src/main/java/com/hubspot/jinjava/util/HelperStringTokenizer.java @@ -36,6 +36,7 @@ public class HelperStringTokenizer extends AbstractIterator { private boolean useComma = false; private char quoteChar = 0; private boolean inQuote = false; + private boolean isEscaped = false; public HelperStringTokenizer(String s) { value = s.toCharArray(); @@ -71,7 +72,8 @@ protected String computeNext() { private String makeToken() { char c = value[currPost++]; - if (c == '"' || c == '\'') { + + if ((c == '"' || c == '\'') && !isEscaped) { if (inQuote) { if (quoteChar == c) { inQuote = false; @@ -81,6 +83,9 @@ private String makeToken() { quoteChar = c; } } + + isEscaped = (c == '\\' && !isEscaped); + if ((Character.isWhitespace(c) || (useComma && c == ',')) && !inQuote) { return newToken(); } diff --git a/src/test/java/com/hubspot/jinjava/util/HelperStringTokenizerTest.java b/src/test/java/com/hubspot/jinjava/util/HelperStringTokenizerTest.java index 7a75d05a5..275074d29 100644 --- a/src/test/java/com/hubspot/jinjava/util/HelperStringTokenizerTest.java +++ b/src/test/java/com/hubspot/jinjava/util/HelperStringTokenizerTest.java @@ -112,4 +112,30 @@ public void itDoesntReturnTrailingNull() { .containsExactly("product", "in", "collections.frontpage.products") .doesNotContainNull(); } + + @Test + public void itHandlesEscapedQuotesWithinQuotedStrings() { + assertThat( + new HelperStringTokenizer("'hi','y\\'all don\\'t'").splitComma(true).allTokens() + ) + .containsExactly("'hi'", "'y\\'all don\\'t'"); + } + + @Test + public void itHandlesEscapedDoubleQuotesWithinQuotedStrings() { + assertThat( + new HelperStringTokenizer("\"hi\",\"say \\\"hello\\\"\"") + .splitComma(true) + .allTokens() + ) + .containsExactly("\"hi\"", "\"say \\\"hello\\\"\""); + } + + @Test + public void itHandlesEscapedBackslashes() { + assertThat( + new HelperStringTokenizer("'path\\\\to\\file'").splitComma(true).allTokens() + ) + .containsExactly("'path\\\\to\\file'"); + } } From a5d85adcfcd934e42b375b03f14bace78ee10d14 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Thu, 16 Oct 2025 17:10:23 -0400 Subject: [PATCH 2/2] Use unquoteAndUnescape instead to properly handling unescaping --- .../jinjava/interpret/JinjavaInterpreter.java | 2 +- .../com/hubspot/jinjava/lib/tag/BlockTag.java | 2 +- .../hubspot/jinjava/util/WhitespaceUtils.java | 1 - .../hubspot/jinjava/lib/tag/CycleTagTest.java | 7 +++++++ .../jinjava/lib/tag/eager/EagerCycleTagTest.java | 16 +++++++++++++++- 5 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java b/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java index 3f698eaa4..0c2f5b145 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java +++ b/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java @@ -692,7 +692,7 @@ public Object resolveObject(String variable, int lineNumber, int startPosition) return ""; } if (WhitespaceUtils.isQuoted(variable)) { - return WhitespaceUtils.unquote(variable); + return WhitespaceUtils.unquoteAndUnescape(variable); } else { Object val = retraceVariable(variable, lineNumber, startPosition); if (val == null) { diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/BlockTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/BlockTag.java index 0d73d9cd8..24cabb2cd 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/BlockTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/BlockTag.java @@ -70,7 +70,7 @@ public OutputNode interpretOutput(TagNode tagNode, JinjavaInterpreter interprete ); } - String blockName = WhitespaceUtils.unquote(tagData.next()); + String blockName = WhitespaceUtils.unquoteAndUnescape(tagData.next()); interpreter.addBlock( blockName, diff --git a/src/main/java/com/hubspot/jinjava/util/WhitespaceUtils.java b/src/main/java/com/hubspot/jinjava/util/WhitespaceUtils.java index d18ce584d..b3a7c6e17 100644 --- a/src/main/java/com/hubspot/jinjava/util/WhitespaceUtils.java +++ b/src/main/java/com/hubspot/jinjava/util/WhitespaceUtils.java @@ -104,7 +104,6 @@ public static String unquote(String s) { return s.trim(); } - // TODO see if all usages of unquote can use this method instead public static String unquoteAndUnescape(String s) { if (Strings.isNullOrEmpty(s)) { return ""; diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/CycleTagTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/CycleTagTest.java index 58a2c2e49..28b062077 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/CycleTagTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/CycleTagTest.java @@ -32,4 +32,11 @@ public void itDefaultsMultipleNullToImageUsingAs() { "{% for item in [0,1] %}{% cycle {{foo}},{{bar}} as var %}{% cycle var %}{% endfor %}"; assertThat(interpreter.render(template)).isEqualTo("{{foo}}{{bar}}"); } + + @Test + public void itHandlesEscapedQuotes() { + String template = + "{% for item in [0,1] %}{% cycle 'a','class=\\'foo bar\\'' %}.{% endfor %}"; + assertThat(interpreter.render(template)).isEqualTo("a.class='foo bar'."); + } } diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerCycleTagTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerCycleTagTest.java index 64a014188..fc9edc409 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerCycleTagTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerCycleTagTest.java @@ -10,6 +10,7 @@ import com.hubspot.jinjava.lib.tag.Tag; import com.hubspot.jinjava.mode.EagerExecutionMode; import com.hubspot.jinjava.tree.parse.TagToken; +import java.util.List; import java.util.Optional; import org.junit.After; import org.junit.Before; @@ -65,9 +66,22 @@ public void itAddCycleTagAsADeferredToken() { @Test public void itHandlesDeferredCycle() { - interpreter.getContext().put("deferred", DeferredValue.instance()); String template = "{% set l = [] %}{% for item in deferred %}{% cycle l.append(deferred),5 %}{% endfor %}{{ l }}"; assertThat(interpreter.render(template)).isEqualTo(template); } + + @Test + public void iitHandlesEscapedQuotesInVariable() { + String template = + "{% set class = \"class='foo bar'\" %}{% for item in deferred %}{% cycle 'item-1',class %}.{% endfor %}"; + String firstPass = interpreter.render(template); + assertThat(firstPass) + .isEqualTo( + "{% for item in deferred %}{% cycle 'item-1','class=\\'foo bar\\'' %}.{% endfor %}" + ); + interpreter.getContext().put("deferred", List.of(0, 1)); + String secondPass = interpreter.render(firstPass); + assertThat(secondPass).isEqualTo("item-1.class='foo bar'."); + } }