Fix JSON encoding for Mustache templates.
This pull request replaces the current self-made implementation of JSON encoding special chars with re-using the Jackson JsonStringEncoder. Turns out the previous implementation also missed a few special chars so had to adjust the tests accordingly (looked at RFC 4627 for reference). Note: There's another JSON String encoder on our classpath (org.apache.commons.lang3.StringEscapeUtils) that essentially does the same thing but adds quoting to more characters than the Jackson Encoder above. Relates to #5473
This commit is contained in:
parent
5812753dbc
commit
180403fc32
|
@ -18,6 +18,7 @@
|
||||||
*/
|
*/
|
||||||
package org.elasticsearch.script.mustache;
|
package org.elasticsearch.script.mustache;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.io.JsonStringEncoder;
|
||||||
import com.github.mustachejava.DefaultMustacheFactory;
|
import com.github.mustachejava.DefaultMustacheFactory;
|
||||||
import com.github.mustachejava.MustacheException;
|
import com.github.mustachejava.MustacheException;
|
||||||
|
|
||||||
|
@ -28,40 +29,14 @@ import java.io.Writer;
|
||||||
* A MustacheFactory that does simple JSON escaping.
|
* A MustacheFactory that does simple JSON escaping.
|
||||||
*/
|
*/
|
||||||
public final class JsonEscapingMustacheFactory extends DefaultMustacheFactory {
|
public final class JsonEscapingMustacheFactory extends DefaultMustacheFactory {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void encode(String value, Writer writer) {
|
public void encode(String value, Writer writer) {
|
||||||
try {
|
try {
|
||||||
escape(value, writer);
|
JsonStringEncoder utils = new JsonStringEncoder();
|
||||||
|
writer.write(utils.quoteAsString(value));;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new MustacheException("Failed to encode value: " + value);
|
throw new MustacheException("Failed to encode value: " + value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Writer escape(String value, Writer writer) throws IOException {
|
|
||||||
for (int i = 0; i < value.length(); i++) {
|
|
||||||
final char character = value.charAt(i);
|
|
||||||
if (isEscapeChar(character)) {
|
|
||||||
writer.write('\\');
|
|
||||||
}
|
|
||||||
writer.write(character);
|
|
||||||
}
|
|
||||||
return writer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isEscapeChar(char c) {
|
|
||||||
switch(c) {
|
|
||||||
case '\b':
|
|
||||||
case '\f':
|
|
||||||
case '\n':
|
|
||||||
case '\r':
|
|
||||||
case '"':
|
|
||||||
case '\\':
|
|
||||||
case '\u000B': // vertical tab
|
|
||||||
case '\t':
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,6 @@
|
||||||
*/
|
*/
|
||||||
package org.elasticsearch.script.mustache;
|
package org.elasticsearch.script.mustache;
|
||||||
|
|
||||||
import com.carrotsearch.randomizedtesting.generators.RandomPicks;
|
|
||||||
import org.elasticsearch.common.bytes.BytesReference;
|
import org.elasticsearch.common.bytes.BytesReference;
|
||||||
import org.elasticsearch.common.settings.ImmutableSettings;
|
import org.elasticsearch.common.settings.ImmutableSettings;
|
||||||
import org.elasticsearch.test.ElasticsearchTestCase;
|
import org.elasticsearch.test.ElasticsearchTestCase;
|
||||||
|
@ -38,10 +37,12 @@ import static org.hamcrest.Matchers.equalTo;
|
||||||
*/
|
*/
|
||||||
public class MustacheScriptEngineTest extends ElasticsearchTestCase {
|
public class MustacheScriptEngineTest extends ElasticsearchTestCase {
|
||||||
private MustacheScriptEngineService qe;
|
private MustacheScriptEngineService qe;
|
||||||
|
private JsonEscapingMustacheFactory escaper;
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setup() {
|
public void setup() {
|
||||||
qe = new MustacheScriptEngineService(ImmutableSettings.Builder.EMPTY_SETTINGS);
|
qe = new MustacheScriptEngineService(ImmutableSettings.Builder.EMPTY_SETTINGS);
|
||||||
|
escaper = new JsonEscapingMustacheFactory();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -73,43 +74,98 @@ public class MustacheScriptEngineTest extends ElasticsearchTestCase {
|
||||||
public void testEscapeJson() throws IOException {
|
public void testEscapeJson() throws IOException {
|
||||||
{
|
{
|
||||||
StringWriter writer = new StringWriter();
|
StringWriter writer = new StringWriter();
|
||||||
JsonEscapingMustacheFactory.escape("hello \n world", writer);
|
escaper.encode("hello \n world", writer);
|
||||||
assertThat(writer.toString(), equalTo("hello \\\n world"));
|
assertThat(writer.toString(), equalTo("hello \\n world"));
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
StringWriter writer = new StringWriter();
|
StringWriter writer = new StringWriter();
|
||||||
JsonEscapingMustacheFactory.escape("\n", writer);
|
escaper.encode("\n", writer);
|
||||||
assertThat(writer.toString(), equalTo("\\\n"));
|
assertThat(writer.toString(), equalTo("\\n"));
|
||||||
}
|
}
|
||||||
|
|
||||||
Character[] specialChars = new Character[]{'\f', '\n', '\r', '"', '\\', (char) 11, '\t', '\b' };
|
Character[] specialChars = new Character[]{
|
||||||
|
'\"',
|
||||||
|
'\\',
|
||||||
|
'\u0000',
|
||||||
|
'\u0001',
|
||||||
|
'\u0002',
|
||||||
|
'\u0003',
|
||||||
|
'\u0004',
|
||||||
|
'\u0005',
|
||||||
|
'\u0006',
|
||||||
|
'\u0007',
|
||||||
|
'\u0008',
|
||||||
|
'\u0009',
|
||||||
|
'\u000B',
|
||||||
|
'\u000C',
|
||||||
|
'\u000E',
|
||||||
|
'\u000F',
|
||||||
|
'\u001F'};
|
||||||
|
String[] escapedChars = new String[]{
|
||||||
|
"\\\"",
|
||||||
|
"\\\\",
|
||||||
|
"\\u0000",
|
||||||
|
"\\u0001",
|
||||||
|
"\\u0002",
|
||||||
|
"\\u0003",
|
||||||
|
"\\u0004",
|
||||||
|
"\\u0005",
|
||||||
|
"\\u0006",
|
||||||
|
"\\u0007",
|
||||||
|
"\\u0008",
|
||||||
|
"\\u0009",
|
||||||
|
"\\u000B",
|
||||||
|
"\\u000C",
|
||||||
|
"\\u000E",
|
||||||
|
"\\u000F",
|
||||||
|
"\\u001F"};
|
||||||
int iters = scaledRandomIntBetween(100, 1000);
|
int iters = scaledRandomIntBetween(100, 1000);
|
||||||
for (int i = 0; i < iters; i++) {
|
for (int i = 0; i < iters; i++) {
|
||||||
int rounds = scaledRandomIntBetween(1, 20);
|
int rounds = scaledRandomIntBetween(1, 20);
|
||||||
StringWriter escaped = new StringWriter();
|
StringWriter expect = new StringWriter();
|
||||||
StringWriter writer = new StringWriter();
|
StringWriter writer = new StringWriter();
|
||||||
for (int j = 0; j < rounds; j++) {
|
for (int j = 0; j < rounds; j++) {
|
||||||
String s = getChars();
|
String s = getChars();
|
||||||
writer.write(s);
|
writer.write(s);
|
||||||
escaped.write(s);
|
expect.write(s);
|
||||||
char c = RandomPicks.randomFrom(getRandom(), specialChars);
|
|
||||||
writer.append(c);
|
int charIndex = randomInt(7);
|
||||||
escaped.append('\\');
|
writer.append(specialChars[charIndex]);
|
||||||
escaped.append(c);
|
expect.append(escapedChars[charIndex]);
|
||||||
}
|
}
|
||||||
StringWriter target = new StringWriter();
|
StringWriter target = new StringWriter();
|
||||||
assertThat(escaped.toString(), equalTo(JsonEscapingMustacheFactory.escape(writer.toString(), target).toString()));
|
escaper.encode(writer.toString(), target);
|
||||||
|
assertThat(expect.toString(), equalTo(target.toString()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getChars() {
|
private String getChars() {
|
||||||
String string = randomRealisticUnicodeOfCodepointLengthBetween(0, 10);
|
String string = randomRealisticUnicodeOfCodepointLengthBetween(0, 10);
|
||||||
for (int i = 0; i < string.length(); i++) {
|
for (int i = 0; i < string.length(); i++) {
|
||||||
if (JsonEscapingMustacheFactory.isEscapeChar(string.charAt(i))) {
|
if (isEscapeChar(string.charAt(i))) {
|
||||||
return string.substring(0, i);
|
return string.substring(0, i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return string;
|
return string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* From https://www.ietf.org/rfc/rfc4627.txt:
|
||||||
|
*
|
||||||
|
* All Unicode characters may be placed within the
|
||||||
|
* quotation marks except for the characters that must be escaped:
|
||||||
|
* quotation mark, reverse solidus, and the control characters (U+0000
|
||||||
|
* through U+001F).
|
||||||
|
* */
|
||||||
|
private static boolean isEscapeChar(char c) {
|
||||||
|
switch (c) {
|
||||||
|
case '"':
|
||||||
|
case '\\':
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c < '\u002F')
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue