This is an automated email from the ASF dual-hosted git repository.

jhyde pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/calcite.git

commit e08ca8c5df5dee154ae1a1e6adfa2f9ce693ccf7
Author: Julian Hyde <[email protected]>
AuthorDate: Fri Apr 8 08:51:12 2022 +0200

    [CALCITE-5088] JsonBuilder should escape backslashes in JSON strings
    
    This would cause JsonParseException when parsing model JSON;
    also the cause of "[CALCITE-4893] JsonParseException happens
    when externalizing expressions with escape character from
    JSON".
    
    In RelWriterTest, add test case from [CALCITE-4893].
---
 .../java/org/apache/calcite/util/JsonBuilder.java  | 61 ++++++++++++++++++++--
 .../org/apache/calcite/plan/RelWriterTest.java     | 18 +++++++
 .../java/org/apache/calcite/util/UtilTest.java     | 41 +++++++++------
 3 files changed, 100 insertions(+), 20 deletions(-)

diff --git a/core/src/main/java/org/apache/calcite/util/JsonBuilder.java 
b/core/src/main/java/org/apache/calcite/util/JsonBuilder.java
index b9fdec835..8c9cd7787 100644
--- a/core/src/main/java/org/apache/calcite/util/JsonBuilder.java
+++ b/core/src/main/java/org/apache/calcite/util/JsonBuilder.java
@@ -32,6 +32,43 @@ import java.util.Map;
  * {@link String}, {@link Boolean}, {@link Long}).
  */
 public class JsonBuilder {
+  /** Maps control characters (0 .. 31) to JSON escaped strings.
+   * Tab, newline, form feed and carriage return are mapped to '\t', '\n',
+   * '\f', 'r' respectively; others are mapped to '\\u00xx' for some 'xx'. */
+  private static final ImmutableList<String> ESCAPED = ImmutableList.of(
+      "\\u0000",
+      "\\u0001",
+      "\\u0002",
+      "\\u0003",
+      "\\u0004",
+      "\\u0005",
+      "\\u0006",
+      "\\u0007",
+      "\\u0008",
+      "\\t", // tab, ASCII 9 x09
+      "\\n", // newline, ASCII 10 x0A
+      "\\u000B",
+      "\\f", // form feed, ASCII 12 x0C
+      "\\r", // carriage return, ASCII 13 x0D
+      "\\u000E",
+      "\\u000F",
+      "\\u0010",
+      "\\u0011",
+      "\\u0012",
+      "\\u0013",
+      "\\u0014",
+      "\\u0015",
+      "\\u0016",
+      "\\u0017",
+      "\\u0018",
+      "\\u0019",
+      "\\u001A",
+      "\\u001B",
+      "\\u001C",
+      "\\u001D",
+      "\\u001E",
+      "\\u001F");
+
   /**
    * Creates a JSON object (represented by a {@link Map}).
    */
@@ -91,17 +128,31 @@ public class JsonBuilder {
     } else if (o instanceof List) {
       appendList(buf, indent, (List<?>) o);
     } else if (o instanceof String) {
-      buf.append('"')
-          .append(
-              ((String) o).replace("\"", "\\\"")
-                  .replace("\n", "\\n"))
-          .append('"');
+      appendString(buf, (String) o);
     } else {
       assert o instanceof Number || o instanceof Boolean;
       buf.append(o);
     }
   }
 
+  private void appendString(StringBuilder buf, String s) {
+    buf.append('"');
+    final int n = s.length();
+    for (int i = 0; i < n; i++) {
+      char c = s.charAt(i);
+      if (c < 32) {
+        buf.append(ESCAPED.get(c));
+      } else if (c == '\\') {
+        buf.append("\\\\");
+      } else if (c == '"') {
+        buf.append("\\\"");
+      } else {
+        buf.append(c);
+      }
+    }
+    buf.append('"');
+  }
+
   private void appendMap(
       StringBuilder buf, int indent, Map<String, @Nullable Object> map) {
     if (map.isEmpty()) {
diff --git a/core/src/test/java/org/apache/calcite/plan/RelWriterTest.java 
b/core/src/test/java/org/apache/calcite/plan/RelWriterTest.java
index b75d09f70..f2277fdc4 100644
--- a/core/src/test/java/org/apache/calcite/plan/RelWriterTest.java
+++ b/core/src/test/java/org/apache/calcite/plan/RelWriterTest.java
@@ -709,6 +709,24 @@ class RelWriterTest {
             + "    LogicalTableScan(table=[[hr, emps]])\n"));
   }
 
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-4893";>[CALCITE-4893]
+   * JsonParseException happens when externalizing expressions with escape
+   * character from JSON</a>. */
+  @Test void testEscapeCharacter() {
+    final Function<RelBuilder, RelNode> relFn = b -> b
+        .scan("EMP")
+        .project(
+            b.call(new MockSqlOperatorTable.SplitFunction(),
+                b.field("ENAME"), b.literal("\r")))
+        .build();
+    final String expected = ""
+        + "LogicalProject($f0=[SPLIT($1, '\r')])\n"
+        + "  LogicalTableScan(table=[[scott, EMP]])\n";
+    relFn(relFn)
+        .assertThatPlan(isLinux(expected));
+  }
+
   @Test void testJsonToRex() {
     // Test simple literal without inputs
     final String jsonString1 = "{\n"
diff --git a/core/src/test/java/org/apache/calcite/util/UtilTest.java 
b/core/src/test/java/org/apache/calcite/util/UtilTest.java
index ff1c05147..6e7fd73f3 100644
--- a/core/src/test/java/org/apache/calcite/util/UtilTest.java
+++ b/core/src/test/java/org/apache/calcite/util/UtilTest.java
@@ -1588,29 +1588,40 @@ class UtilTest {
     map.put("foo", 1);
     map.put("baz", true);
     map.put("bar", "can't");
+    final String tricky = "string with doublequote\", singlequote', "
+        + "backslash\\, percent20%20, plus+, ampersand&, linefeed\n"
+        + ", carriage return\r, lfcr\n"
+        + "\r.";
+    map.put("tricky", tricky);
     List<Object> list = builder.list();
     map.put("list", list);
     list.add(2);
     list.add(3);
     list.add(builder.list());
     list.add(builder.map());
+    list.add(tricky);
     list.add(null);
     map.put("nullValue", null);
-    assertEquals(
-        "{\n"
-            + "  \"foo\": 1,\n"
-            + "  \"baz\": true,\n"
-            + "  \"bar\": \"can't\",\n"
-            + "  \"list\": [\n"
-            + "    2,\n"
-            + "    3,\n"
-            + "    [],\n"
-            + "    {},\n"
-            + "    null\n"
-            + "  ],\n"
-            + "  \"nullValue\": null\n"
-            + "}",
-        builder.toJsonString(map));
+    final String expected = "{\n"
+        + "  \"foo\": 1,\n"
+        + "  \"baz\": true,\n"
+        + "  \"bar\": \"can't\",\n"
+        + "  \"tricky\": \"string with doublequote\\\", singlequote', "
+        + "backslash\\\\, percent20%20, plus+, ampersand&, linefeed\\n"
+        + ", carriage return\\r, lfcr\\n\\r.\",\n"
+        + "  \"list\": [\n"
+        + "    2,\n"
+        + "    3,\n"
+        + "    [],\n"
+        + "    {},\n"
+        + "    \"string with doublequote\\\", singlequote', backslash\\\\, "
+        + "percent20%20, plus+, ampersand&, linefeed\\n"
+        + ", carriage return\\r, lfcr\\n\\r.\",\n"
+        + "    null\n"
+        + "  ],\n"
+        + "  \"nullValue\": null\n"
+        + "}";
+    assertThat(builder.toJsonString(map), is(expected));
   }
 
   @Test void testCompositeMap() {

Reply via email to